From e99416456afd4aa8bde42016826f9a345291cbf3 Mon Sep 17 00:00:00 2001 From: Matthew Poletiek Date: Tue, 8 Dec 2020 21:03:16 -0600 Subject: Initial Commit --- COPYING | 674 ++ INSTALL | 52 + MANIFEST.in | 8 + README.chirpc | 104 + README.developers | 64 + README.md | 33 + README.rpttool | 23 + build/chirp.app/Contents/Info.plist | 26 + build/chirp.app/Contents/MacOS/chirp | 19 + build/chirp.app/Contents/PkgInfo | 1 + build/chirp.app/Contents/Resources/.placeholder | 0 build/macos/make_pango.sh | 51 + build/make_source_release.sh | 23 + build/version | 1 + chirp/__init__.py | 27 + chirp/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 451 bytes chirp/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 455 bytes chirp/__pycache__/bandplan.cpython-37.pyc | Bin 0 -> 2336 bytes chirp/__pycache__/bandplan.cpython-38.pyc | Bin 0 -> 2386 bytes chirp/__pycache__/bandplan_au.cpython-37.pyc | Bin 0 -> 2387 bytes chirp/__pycache__/bandplan_au.cpython-38.pyc | Bin 0 -> 2469 bytes chirp/__pycache__/bandplan_iaru_r1.cpython-37.pyc | Bin 0 -> 3382 bytes chirp/__pycache__/bandplan_iaru_r1.cpython-38.pyc | Bin 0 -> 3454 bytes chirp/__pycache__/bandplan_iaru_r2.cpython-37.pyc | Bin 0 -> 3401 bytes chirp/__pycache__/bandplan_iaru_r2.cpython-38.pyc | Bin 0 -> 3449 bytes chirp/__pycache__/bandplan_iaru_r3.cpython-37.pyc | Bin 0 -> 2886 bytes chirp/__pycache__/bandplan_iaru_r3.cpython-38.pyc | Bin 0 -> 2934 bytes chirp/__pycache__/bandplan_na.cpython-37.pyc | Bin 0 -> 8043 bytes chirp/__pycache__/bandplan_na.cpython-38.pyc | Bin 0 -> 8053 bytes chirp/__pycache__/bitwise.cpython-37.pyc | Bin 0 -> 34113 bytes chirp/__pycache__/bitwise.cpython-38.pyc | Bin 0 -> 33686 bytes chirp/__pycache__/bitwise_grammar.cpython-37.pyc | Bin 0 -> 4169 bytes chirp/__pycache__/bitwise_grammar.cpython-38.pyc | Bin 0 -> 4262 bytes chirp/__pycache__/chirp_common.cpython-37.pyc | Bin 0 -> 48610 bytes chirp/__pycache__/chirp_common.cpython-38.pyc | Bin 0 -> 49153 bytes chirp/__pycache__/detect.cpython-37.pyc | Bin 0 -> 2248 bytes chirp/__pycache__/detect.cpython-38.pyc | Bin 0 -> 2276 bytes chirp/__pycache__/directory.cpython-37.pyc | Bin 0 -> 5398 bytes chirp/__pycache__/directory.cpython-38.pyc | Bin 0 -> 5430 bytes chirp/__pycache__/elib_intl.cpython-37.pyc | Bin 0 -> 10916 bytes chirp/__pycache__/elib_intl.cpython-38.pyc | Bin 0 -> 10975 bytes chirp/__pycache__/errors.cpython-37.pyc | Bin 0 -> 1600 bytes chirp/__pycache__/errors.cpython-38.pyc | Bin 0 -> 1534 bytes chirp/__pycache__/import_logic.cpython-37.pyc | Bin 0 -> 5766 bytes chirp/__pycache__/import_logic.cpython-38.pyc | Bin 0 -> 5798 bytes chirp/__pycache__/logger.cpython-37.pyc | Bin 0 -> 4863 bytes chirp/__pycache__/logger.cpython-38.pyc | Bin 0 -> 4929 bytes chirp/__pycache__/memmap.cpython-37.pyc | Bin 0 -> 4752 bytes chirp/__pycache__/memmap.cpython-38.pyc | Bin 0 -> 4825 bytes chirp/__pycache__/platform.cpython-37.pyc | Bin 0 -> 14390 bytes chirp/__pycache__/platform.cpython-38.pyc | Bin 0 -> 14513 bytes chirp/__pycache__/pyPEG.cpython-37.pyc | Bin 0 -> 7516 bytes chirp/__pycache__/pyPEG.cpython-38.pyc | Bin 0 -> 7651 bytes chirp/__pycache__/radioreference.cpython-37.pyc | Bin 0 -> 4754 bytes chirp/__pycache__/radioreference.cpython-38.pyc | Bin 0 -> 4780 bytes chirp/__pycache__/settings.cpython-37.pyc | Bin 0 -> 18598 bytes chirp/__pycache__/settings.cpython-38.pyc | Bin 0 -> 18699 bytes chirp/__pycache__/util.cpython-37.pyc | Bin 0 -> 3407 bytes chirp/__pycache__/util.cpython-38.pyc | Bin 0 -> 3424 bytes chirp/bandplan.py | 85 + chirp/bandplan_au.py | 115 + chirp/bandplan_iaru_r1.py | 147 + chirp/bandplan_iaru_r2.py | 145 + chirp/bandplan_iaru_r3.py | 139 + chirp/bandplan_na.py | 270 + chirp/bitwise.py | 1067 ++++ chirp/bitwise_grammar.py | 134 + chirp/chirp_common.py | 1631 +++++ chirp/detect.py | 108 + chirp/directory.py | 229 + chirp/dmrmarc.py | 139 + chirp/drivers/__init__.py | 10 + chirp/drivers/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 448 bytes chirp/drivers/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 452 bytes .../__pycache__/baofeng_wp970i.cpython-37.pyc | Bin 0 -> 20102 bytes .../__pycache__/baofeng_wp970i.cpython-38.pyc | Bin 0 -> 20342 bytes .../__pycache__/boblov_x3plus.cpython-37.pyc | Bin 0 -> 14386 bytes .../__pycache__/boblov_x3plus.cpython-38.pyc | Bin 0 -> 14484 bytes chirp/drivers/__pycache__/btech.cpython-37.pyc | Bin 0 -> 81301 bytes chirp/drivers/__pycache__/btech.cpython-38.pyc | Bin 0 -> 81658 bytes chirp/drivers/__pycache__/ft1500m.cpython-37.pyc | Bin 0 -> 5625 bytes chirp/drivers/__pycache__/ft1500m.cpython-38.pyc | Bin 0 -> 5661 bytes chirp/drivers/__pycache__/ft1802.cpython-37.pyc | Bin 0 -> 6160 bytes chirp/drivers/__pycache__/ft1802.cpython-38.pyc | Bin 0 -> 6181 bytes chirp/drivers/__pycache__/ft2d.cpython-37.pyc | Bin 0 -> 5622 bytes chirp/drivers/__pycache__/ft2d.cpython-38.pyc | Bin 0 -> 5686 bytes chirp/drivers/__pycache__/ft4.cpython-37.pyc | Bin 0 -> 36634 bytes chirp/drivers/__pycache__/ft4.cpython-38.pyc | Bin 0 -> 36973 bytes chirp/drivers/__pycache__/ft7800.cpython-37.pyc | Bin 0 -> 28168 bytes chirp/drivers/__pycache__/ft7800.cpython-38.pyc | Bin 0 -> 28209 bytes chirp/drivers/__pycache__/ft817.cpython-37.pyc | Bin 0 -> 29585 bytes chirp/drivers/__pycache__/ft817.cpython-38.pyc | Bin 0 -> 29893 bytes chirp/drivers/__pycache__/ft818.cpython-37.pyc | Bin 0 -> 8459 bytes chirp/drivers/__pycache__/ft818.cpython-38.pyc | Bin 0 -> 8533 bytes chirp/drivers/__pycache__/ft857.cpython-37.pyc | Bin 0 -> 30149 bytes chirp/drivers/__pycache__/ft857.cpython-38.pyc | Bin 0 -> 30914 bytes chirp/drivers/__pycache__/ftm3200d.cpython-37.pyc | Bin 0 -> 6965 bytes chirp/drivers/__pycache__/ftm3200d.cpython-38.pyc | Bin 0 -> 7027 bytes .../drivers/__pycache__/generic_csv.cpython-37.pyc | Bin 0 -> 12783 bytes .../drivers/__pycache__/generic_csv.cpython-38.pyc | Bin 0 -> 12909 bytes .../drivers/__pycache__/generic_tpe.cpython-37.pyc | Bin 0 -> 1722 bytes .../drivers/__pycache__/generic_tpe.cpython-38.pyc | Bin 0 -> 1754 bytes chirp/drivers/__pycache__/gmrsuv1.cpython-37.pyc | Bin 0 -> 23052 bytes chirp/drivers/__pycache__/gmrsuv1.cpython-38.pyc | Bin 0 -> 23307 bytes chirp/drivers/__pycache__/h777.cpython-37.pyc | Bin 0 -> 15437 bytes chirp/drivers/__pycache__/h777.cpython-38.pyc | Bin 0 -> 15352 bytes chirp/drivers/__pycache__/hobbypcb.cpython-37.pyc | Bin 0 -> 8265 bytes chirp/drivers/__pycache__/hobbypcb.cpython-38.pyc | Bin 0 -> 8410 bytes chirp/drivers/__pycache__/ic208.cpython-37.pyc | Bin 0 -> 6477 bytes chirp/drivers/__pycache__/ic208.cpython-38.pyc | Bin 0 -> 6544 bytes chirp/drivers/__pycache__/ic2100.cpython-37.pyc | Bin 0 -> 6482 bytes chirp/drivers/__pycache__/ic2100.cpython-38.pyc | Bin 0 -> 6540 bytes chirp/drivers/__pycache__/ic2200.cpython-37.pyc | Bin 0 -> 8079 bytes chirp/drivers/__pycache__/ic2200.cpython-38.pyc | Bin 0 -> 8082 bytes chirp/drivers/__pycache__/ic2300.cpython-37.pyc | Bin 0 -> 8695 bytes chirp/drivers/__pycache__/ic2300.cpython-38.pyc | Bin 0 -> 8815 bytes chirp/drivers/__pycache__/ic2720.cpython-37.pyc | Bin 0 -> 4304 bytes chirp/drivers/__pycache__/ic2720.cpython-38.pyc | Bin 0 -> 4325 bytes chirp/drivers/__pycache__/ic2730.cpython-37.pyc | Bin 0 -> 29212 bytes chirp/drivers/__pycache__/ic2730.cpython-38.pyc | Bin 0 -> 29458 bytes chirp/drivers/__pycache__/ic2820.cpython-37.pyc | Bin 0 -> 8633 bytes chirp/drivers/__pycache__/ic2820.cpython-38.pyc | Bin 0 -> 8555 bytes chirp/drivers/__pycache__/ic9x.cpython-37.pyc | Bin 0 -> 11095 bytes chirp/drivers/__pycache__/ic9x.cpython-38.pyc | Bin 0 -> 11193 bytes chirp/drivers/__pycache__/ic9x_icf.cpython-37.pyc | Bin 0 -> 2455 bytes chirp/drivers/__pycache__/ic9x_icf.cpython-38.pyc | Bin 0 -> 2477 bytes .../drivers/__pycache__/ic9x_icf_ll.cpython-37.pyc | Bin 0 -> 3667 bytes .../drivers/__pycache__/ic9x_icf_ll.cpython-38.pyc | Bin 0 -> 3689 bytes chirp/drivers/__pycache__/ic9x_ll.cpython-37.pyc | Bin 0 -> 16894 bytes chirp/drivers/__pycache__/ic9x_ll.cpython-38.pyc | Bin 0 -> 16862 bytes chirp/drivers/__pycache__/icf.cpython-37.pyc | Bin 0 -> 22112 bytes chirp/drivers/__pycache__/icf.cpython-38.pyc | Bin 0 -> 22309 bytes chirp/drivers/__pycache__/icomciv.cpython-37.pyc | Bin 0 -> 17895 bytes chirp/drivers/__pycache__/icomciv.cpython-38.pyc | Bin 0 -> 17942 bytes chirp/drivers/__pycache__/icp7.cpython-37.pyc | Bin 0 -> 5429 bytes chirp/drivers/__pycache__/icp7.cpython-38.pyc | Bin 0 -> 5470 bytes chirp/drivers/__pycache__/icq7.cpython-37.pyc | Bin 0 -> 8288 bytes chirp/drivers/__pycache__/icq7.cpython-38.pyc | Bin 0 -> 8409 bytes chirp/drivers/__pycache__/ict70.cpython-37.pyc | Bin 0 -> 5504 bytes chirp/drivers/__pycache__/ict70.cpython-38.pyc | Bin 0 -> 5543 bytes chirp/drivers/__pycache__/ict7h.cpython-37.pyc | Bin 0 -> 3060 bytes chirp/drivers/__pycache__/ict7h.cpython-38.pyc | Bin 0 -> 3096 bytes chirp/drivers/__pycache__/ict8.cpython-37.pyc | Bin 0 -> 3480 bytes chirp/drivers/__pycache__/ict8.cpython-38.pyc | Bin 0 -> 3522 bytes chirp/drivers/__pycache__/icw32.cpython-37.pyc | Bin 0 -> 5994 bytes chirp/drivers/__pycache__/icw32.cpython-38.pyc | Bin 0 -> 5881 bytes chirp/drivers/__pycache__/icx8x.cpython-37.pyc | Bin 0 -> 5424 bytes chirp/drivers/__pycache__/icx8x.cpython-38.pyc | Bin 0 -> 5393 bytes chirp/drivers/__pycache__/icx8x_ll.cpython-37.pyc | Bin 0 -> 11967 bytes chirp/drivers/__pycache__/icx8x_ll.cpython-38.pyc | Bin 0 -> 11934 bytes chirp/drivers/__pycache__/id31.cpython-37.pyc | Bin 0 -> 8256 bytes chirp/drivers/__pycache__/id31.cpython-38.pyc | Bin 0 -> 8301 bytes chirp/drivers/__pycache__/id51.cpython-37.pyc | Bin 0 -> 2630 bytes chirp/drivers/__pycache__/id51.cpython-38.pyc | Bin 0 -> 2654 bytes chirp/drivers/__pycache__/id51plus.cpython-37.pyc | Bin 0 -> 3630 bytes chirp/drivers/__pycache__/id51plus.cpython-38.pyc | Bin 0 -> 3662 bytes chirp/drivers/__pycache__/id800.cpython-37.pyc | Bin 0 -> 9276 bytes chirp/drivers/__pycache__/id800.cpython-38.pyc | Bin 0 -> 9240 bytes chirp/drivers/__pycache__/id880.cpython-37.pyc | Bin 0 -> 9686 bytes chirp/drivers/__pycache__/id880.cpython-38.pyc | Bin 0 -> 9727 bytes chirp/drivers/__pycache__/idrp.cpython-37.pyc | Bin 0 -> 4165 bytes chirp/drivers/__pycache__/idrp.cpython-38.pyc | Bin 0 -> 4196 bytes .../drivers/__pycache__/kenwood_hmk.cpython-37.pyc | Bin 0 -> 3217 bytes .../drivers/__pycache__/kenwood_hmk.cpython-38.pyc | Bin 0 -> 3259 bytes .../drivers/__pycache__/kenwood_itm.cpython-37.pyc | Bin 0 -> 3254 bytes .../drivers/__pycache__/kenwood_itm.cpython-38.pyc | Bin 0 -> 3308 bytes .../__pycache__/kenwood_live.cpython-37.pyc | Bin 0 -> 45958 bytes .../__pycache__/kenwood_live.cpython-38.pyc | Bin 0 -> 44692 bytes chirp/drivers/__pycache__/mursv1.cpython-37.pyc | Bin 0 -> 18870 bytes chirp/drivers/__pycache__/mursv1.cpython-38.pyc | Bin 0 -> 19056 bytes .../__pycache__/puxing_px888k.cpython-37.pyc | Bin 0 -> 51736 bytes .../__pycache__/puxing_px888k.cpython-38.pyc | Bin 0 -> 52252 bytes .../__pycache__/repeaterbook.cpython-37.pyc | Bin 0 -> 1003 bytes .../__pycache__/repeaterbook.cpython-38.pyc | Bin 0 -> 999 bytes chirp/drivers/__pycache__/template.cpython-37.pyc | Bin 0 -> 3404 bytes chirp/drivers/__pycache__/template.cpython-38.pyc | Bin 0 -> 3446 bytes chirp/drivers/__pycache__/th350.cpython-37.pyc | Bin 0 -> 9051 bytes chirp/drivers/__pycache__/th350.cpython-38.pyc | Bin 0 -> 9100 bytes chirp/drivers/__pycache__/th_uv3r.cpython-37.pyc | Bin 0 -> 6889 bytes chirp/drivers/__pycache__/th_uv3r.cpython-38.pyc | Bin 0 -> 6951 bytes chirp/drivers/__pycache__/th_uv3r25.cpython-37.pyc | Bin 0 -> 5149 bytes chirp/drivers/__pycache__/th_uv3r25.cpython-38.pyc | Bin 0 -> 5186 bytes chirp/drivers/__pycache__/th_uvf8d.cpython-37.pyc | Bin 0 -> 13697 bytes chirp/drivers/__pycache__/th_uvf8d.cpython-38.pyc | Bin 0 -> 13998 bytes chirp/drivers/__pycache__/tk270.cpython-37.pyc | Bin 0 -> 22339 bytes chirp/drivers/__pycache__/tk270.cpython-38.pyc | Bin 0 -> 22193 bytes chirp/drivers/__pycache__/tk760.cpython-37.pyc | Bin 0 -> 21254 bytes chirp/drivers/__pycache__/tk760.cpython-38.pyc | Bin 0 -> 21120 bytes chirp/drivers/__pycache__/tk8102.cpython-37.pyc | Bin 0 -> 11358 bytes chirp/drivers/__pycache__/tk8102.cpython-38.pyc | Bin 0 -> 11348 bytes chirp/drivers/__pycache__/tk8180.cpython-37.pyc | Bin 0 -> 33439 bytes chirp/drivers/__pycache__/tk8180.cpython-38.pyc | Bin 0 -> 33575 bytes chirp/drivers/__pycache__/tmv71.cpython-37.pyc | Bin 0 -> 2532 bytes chirp/drivers/__pycache__/tmv71.cpython-38.pyc | Bin 0 -> 2572 bytes chirp/drivers/__pycache__/tmv71_ll.cpython-37.pyc | Bin 0 -> 8948 bytes chirp/drivers/__pycache__/tmv71_ll.cpython-38.pyc | Bin 0 -> 8938 bytes chirp/drivers/__pycache__/ts850.cpython-37.pyc | Bin 0 -> 6108 bytes chirp/drivers/__pycache__/ts850.cpython-38.pyc | Bin 0 -> 6090 bytes chirp/drivers/__pycache__/uv5r.cpython-37.pyc | Bin 0 -> 44174 bytes chirp/drivers/__pycache__/uv5r.cpython-38.pyc | Bin 0 -> 44153 bytes chirp/drivers/__pycache__/uv5x3.cpython-37.pyc | Bin 0 -> 27600 bytes chirp/drivers/__pycache__/uv5x3.cpython-38.pyc | Bin 0 -> 27600 bytes chirp/drivers/__pycache__/uv6r.cpython-37.pyc | Bin 0 -> 18632 bytes chirp/drivers/__pycache__/uv6r.cpython-38.pyc | Bin 0 -> 18911 bytes chirp/drivers/__pycache__/uvb5.cpython-37.pyc | Bin 0 -> 19161 bytes chirp/drivers/__pycache__/uvb5.cpython-38.pyc | Bin 0 -> 19312 bytes chirp/drivers/__pycache__/vx170.cpython-37.pyc | Bin 0 -> 3241 bytes chirp/drivers/__pycache__/vx170.cpython-38.pyc | Bin 0 -> 3265 bytes chirp/drivers/__pycache__/vx2.cpython-37.pyc | Bin 0 -> 16458 bytes chirp/drivers/__pycache__/vx2.cpython-38.pyc | Bin 0 -> 16736 bytes chirp/drivers/__pycache__/vx3.cpython-37.pyc | Bin 0 -> 24811 bytes chirp/drivers/__pycache__/vx3.cpython-38.pyc | Bin 0 -> 25275 bytes chirp/drivers/__pycache__/vx5.cpython-37.pyc | Bin 0 -> 9218 bytes chirp/drivers/__pycache__/vx5.cpython-38.pyc | Bin 0 -> 9336 bytes chirp/drivers/__pycache__/vx510.cpython-37.pyc | Bin 0 -> 4956 bytes chirp/drivers/__pycache__/vx510.cpython-38.pyc | Bin 0 -> 4974 bytes chirp/drivers/__pycache__/vx6.cpython-37.pyc | Bin 0 -> 21134 bytes chirp/drivers/__pycache__/vx6.cpython-38.pyc | Bin 0 -> 21336 bytes chirp/drivers/__pycache__/vx7.cpython-37.pyc | Bin 0 -> 10247 bytes chirp/drivers/__pycache__/vx7.cpython-38.pyc | Bin 0 -> 10332 bytes chirp/drivers/__pycache__/vx8.cpython-37.pyc | Bin 0 -> 43485 bytes chirp/drivers/__pycache__/vx8.cpython-38.pyc | Bin 0 -> 43488 bytes .../__pycache__/wouxun_common.cpython-37.pyc | Bin 0 -> 1835 bytes .../__pycache__/wouxun_common.cpython-38.pyc | Bin 0 -> 1848 bytes .../drivers/__pycache__/yaesu_clone.cpython-37.pyc | Bin 0 -> 8097 bytes .../drivers/__pycache__/yaesu_clone.cpython-38.pyc | Bin 0 -> 8042 bytes chirp/drivers/alinco.py | 868 +++ chirp/drivers/anytone.py | 569 ++ chirp/drivers/anytone_ht.py | 946 +++ chirp/drivers/ap510.py | 807 +++ chirp/drivers/baofeng_common.py | 641 ++ chirp/drivers/baofeng_uv3r.py | 653 ++ chirp/drivers/baofeng_wp970i.py | 912 +++ chirp/drivers/bf-t1.py | 917 +++ chirp/drivers/bj9900.py | 407 ++ chirp/drivers/bjuv55.py | 652 ++ chirp/drivers/boblov_x3plus.py | 572 ++ chirp/drivers/btech.py | 4195 +++++++++++++ chirp/drivers/fd268.py | 914 +++ chirp/drivers/ft1500m.py | 235 + chirp/drivers/ft1802.py | 253 + chirp/drivers/ft1d.py | 1988 ++++++ chirp/drivers/ft2800.py | 281 + chirp/drivers/ft2900.py | 1256 ++++ chirp/drivers/ft2d.py | 166 + chirp/drivers/ft4.py | 1300 ++++ chirp/drivers/ft450d.py | 1494 +++++ chirp/drivers/ft50.py | 641 ++ chirp/drivers/ft60.py | 828 +++ chirp/drivers/ft70.py | 1191 ++++ chirp/drivers/ft7100.py | 1231 ++++ chirp/drivers/ft7800.py | 1028 +++ chirp/drivers/ft8100.py | 314 + chirp/drivers/ft817.py | 1214 ++++ chirp/drivers/ft818.py | 342 + chirp/drivers/ft857.py | 1202 ++++ chirp/drivers/ft90.py | 675 ++ chirp/drivers/ftm3200d.py | 201 + chirp/drivers/ftm350.py | 443 ++ chirp/drivers/generic_csv.py | 471 ++ chirp/drivers/generic_tpe.py | 60 + chirp/drivers/gmrsuv1.py | 1049 ++++ chirp/drivers/h777.py | 642 ++ chirp/drivers/hobbypcb.py | 327 + chirp/drivers/ic208.py | 263 + chirp/drivers/ic2100.py | 279 + chirp/drivers/ic2200.py | 295 + chirp/drivers/ic2300.py | 391 ++ chirp/drivers/ic2720.py | 191 + chirp/drivers/ic2730.py | 1291 ++++ chirp/drivers/ic2820.py | 356 ++ chirp/drivers/ic9x.py | 426 ++ chirp/drivers/ic9x_icf.py | 81 + chirp/drivers/ic9x_icf_ll.py | 152 + chirp/drivers/ic9x_ll.py | 580 ++ chirp/drivers/icf.py | 815 +++ chirp/drivers/icomciv.py | 692 ++ chirp/drivers/icp7.py | 243 + chirp/drivers/icq7.py | 349 ++ chirp/drivers/ict70.py | 223 + chirp/drivers/ict7h.py | 122 + chirp/drivers/ict8.py | 147 + chirp/drivers/icw32.py | 251 + chirp/drivers/icx8x.py | 209 + chirp/drivers/icx8x_ll.py | 539 ++ chirp/drivers/id31.py | 337 + chirp/drivers/id51.py | 136 + chirp/drivers/id51plus.py | 173 + chirp/drivers/id800.py | 383 ++ chirp/drivers/id880.py | 394 ++ chirp/drivers/idrp.py | 173 + chirp/drivers/kenwood_hmk.py | 134 + chirp/drivers/kenwood_itm.py | 137 + chirp/drivers/kenwood_live.py | 1764 ++++++ chirp/drivers/kguv8d.py | 1044 ++++ chirp/drivers/kguv8dplus.py | 1111 ++++ chirp/drivers/kguv8e.py | 1146 ++++ chirp/drivers/kguv9dplus.py | 1893 ++++++ chirp/drivers/kyd.py | 522 ++ chirp/drivers/kyd_IP620.py | 629 ++ chirp/drivers/leixen.py | 1038 +++ chirp/drivers/lt725uv.py | 1446 +++++ chirp/drivers/mursv1.py | 874 +++ chirp/drivers/puxing.py | 524 ++ chirp/drivers/puxing_px888k.py | 1878 ++++++ chirp/drivers/radioddity_r2.py | 622 ++ chirp/drivers/radtel_t18.py | 501 ++ chirp/drivers/repeaterbook.py | 36 + chirp/drivers/retevis_rt1.py | 748 +++ chirp/drivers/retevis_rt21.py | 584 ++ chirp/drivers/retevis_rt22.py | 650 ++ chirp/drivers/retevis_rt23.py | 868 +++ chirp/drivers/retevis_rt26.py | 919 +++ chirp/drivers/rfinder.py | 335 + chirp/drivers/rh5r_v2.py | 290 + chirp/drivers/tdxone_tdq8a.py | 1155 ++++ chirp/drivers/template.py | 130 + chirp/drivers/th350.py | 398 ++ chirp/drivers/th7800.py | 739 +++ chirp/drivers/th9000.py | 852 +++ chirp/drivers/th9800.py | 798 +++ chirp/drivers/th_uv3r.py | 270 + chirp/drivers/th_uv3r25.py | 209 + chirp/drivers/th_uv8000.py | 1491 +++++ chirp/drivers/th_uvf8d.py | 639 ++ chirp/drivers/thd72.py | 796 +++ chirp/drivers/thuv1f.py | 482 ++ chirp/drivers/tk270.py | 944 +++ chirp/drivers/tk760.py | 881 +++ chirp/drivers/tk760g.py | 1748 ++++++ chirp/drivers/tk8102.py | 456 ++ chirp/drivers/tk8180.py | 1215 ++++ chirp/drivers/tmv71.py | 79 + chirp/drivers/tmv71_ll.py | 391 ++ chirp/drivers/ts2000.py | 296 + chirp/drivers/ts480.py | 1144 ++++ chirp/drivers/ts590.py | 1684 +++++ chirp/drivers/ts850.py | 254 + chirp/drivers/uv5r.py | 1939 ++++++ chirp/drivers/uv5x3.py | 1232 ++++ chirp/drivers/uv6r.py | 872 +++ chirp/drivers/uvb5.py | 799 +++ chirp/drivers/vgc.py | 1450 +++++ chirp/drivers/vx170.py | 132 + chirp/drivers/vx2.py | 751 +++ chirp/drivers/vx3.py | 978 +++ chirp/drivers/vx5.py | 339 + chirp/drivers/vx510.py | 203 + chirp/drivers/vx6.py | 875 +++ chirp/drivers/vx7.py | 362 ++ chirp/drivers/vx8.py | 1675 +++++ chirp/drivers/vxa700.py | 317 + chirp/drivers/wouxun.py | 1567 +++++ chirp/drivers/wouxun_common.py | 80 + chirp/drivers/yaesu_clone.py | 286 + chirp/elib_intl.py | 520 ++ chirp/errors.py | 49 + chirp/import_logic.py | 268 + chirp/logger.py | 186 + chirp/memmap.py | 153 + chirp/platform.py | 493 ++ chirp/pyPEG.py | 401 ++ chirp/radioreference.py | 192 + chirp/settings.py | 477 ++ chirp/ui/__init__.py | 14 + chirp/ui/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 141 bytes chirp/ui/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 149 bytes chirp/ui/__pycache__/bandplans.cpython-37.pyc | Bin 0 -> 3220 bytes chirp/ui/__pycache__/bandplans.cpython-38.pyc | Bin 0 -> 3245 bytes chirp/ui/__pycache__/bankedit.cpython-37.pyc | Bin 0 -> 13309 bytes chirp/ui/__pycache__/bankedit.cpython-38.pyc | Bin 0 -> 13356 bytes chirp/ui/__pycache__/clone.cpython-37.pyc | Bin 0 -> 8030 bytes chirp/ui/__pycache__/clone.cpython-38.pyc | Bin 0 -> 8148 bytes chirp/ui/__pycache__/cloneprog.cpython-37.pyc | Bin 0 -> 1533 bytes chirp/ui/__pycache__/cloneprog.cpython-38.pyc | Bin 0 -> 1557 bytes chirp/ui/__pycache__/common.cpython-37.pyc | Bin 0 -> 12446 bytes chirp/ui/__pycache__/common.cpython-38.pyc | Bin 0 -> 12597 bytes chirp/ui/__pycache__/compat.cpython-37.pyc | Bin 0 -> 2727 bytes chirp/ui/__pycache__/compat.cpython-38.pyc | Bin 0 -> 2795 bytes chirp/ui/__pycache__/config.cpython-37.pyc | Bin 0 -> 4241 bytes chirp/ui/__pycache__/config.cpython-38.pyc | Bin 0 -> 4187 bytes chirp/ui/__pycache__/dstaredit.cpython-37.pyc | Bin 0 -> 5119 bytes chirp/ui/__pycache__/dstaredit.cpython-38.pyc | Bin 0 -> 5169 bytes chirp/ui/__pycache__/editorset.cpython-37.pyc | Bin 0 -> 11939 bytes chirp/ui/__pycache__/editorset.cpython-38.pyc | Bin 0 -> 12064 bytes chirp/ui/__pycache__/fips.cpython-37.pyc | Bin 0 -> 161701 bytes chirp/ui/__pycache__/fips.cpython-38.pyc | Bin 0 -> 113251 bytes chirp/ui/__pycache__/importdialog.cpython-37.pyc | Bin 0 -> 15844 bytes chirp/ui/__pycache__/importdialog.cpython-38.pyc | Bin 0 -> 16037 bytes chirp/ui/__pycache__/inputdialog.cpython-37.pyc | Bin 0 -> 4638 bytes chirp/ui/__pycache__/inputdialog.cpython-38.pyc | Bin 0 -> 4692 bytes chirp/ui/__pycache__/mainapp.cpython-37.pyc | Bin 0 -> 60902 bytes chirp/ui/__pycache__/mainapp.cpython-38.pyc | Bin 0 -> 60942 bytes chirp/ui/__pycache__/memdetail.cpython-37.pyc | Bin 0 -> 13286 bytes chirp/ui/__pycache__/memdetail.cpython-38.pyc | Bin 0 -> 13560 bytes chirp/ui/__pycache__/memedit.cpython-37.pyc | Bin 0 -> 49274 bytes chirp/ui/__pycache__/memedit.cpython-38.pyc | Bin 0 -> 49002 bytes chirp/ui/__pycache__/miscwidgets.cpython-37.pyc | Bin 0 -> 24463 bytes chirp/ui/__pycache__/miscwidgets.cpython-38.pyc | Bin 0 -> 24599 bytes chirp/ui/__pycache__/radiobrowser.cpython-37.pyc | Bin 0 -> 11223 bytes chirp/ui/__pycache__/radiobrowser.cpython-38.pyc | Bin 0 -> 11336 bytes chirp/ui/__pycache__/reporting.cpython-37.pyc | Bin 0 -> 4045 bytes chirp/ui/__pycache__/reporting.cpython-38.pyc | Bin 0 -> 4137 bytes chirp/ui/__pycache__/settingsedit.cpython-37.pyc | Bin 0 -> 6595 bytes chirp/ui/__pycache__/settingsedit.cpython-38.pyc | Bin 0 -> 6653 bytes chirp/ui/__pycache__/shiftdialog.cpython-37.pyc | Bin 0 -> 4246 bytes chirp/ui/__pycache__/shiftdialog.cpython-38.pyc | Bin 0 -> 4292 bytes chirp/ui/bandplans.py | 112 + chirp/ui/bankedit.py | 419 ++ chirp/ui/clone.py | 278 + chirp/ui/cloneprog.py | 66 + chirp/ui/common.py | 451 ++ chirp/ui/compat.py | 82 + chirp/ui/config.py | 131 + chirp/ui/dstaredit.py | 202 + chirp/ui/editorset.py | 428 ++ chirp/ui/fips.py | 6605 ++++++++++++++++++++ chirp/ui/importdialog.py | 652 ++ chirp/ui/inputdialog.py | 157 + chirp/ui/mainapp.py | 2177 +++++++ chirp/ui/memdetail.py | 421 ++ chirp/ui/memedit.py | 1744 ++++++ chirp/ui/miscwidgets.py | 879 +++ chirp/ui/radiobrowser.py | 350 ++ chirp/ui/reporting.py | 195 + chirp/ui/settingsedit.py | 232 + chirp/ui/shiftdialog.py | 161 + chirp/util.py | 146 + chirp/wxui/__init__.py | 0 chirp/wxui/clone.py | 276 + chirp/wxui/common.py | 272 + chirp/wxui/developer.py | 364 ++ chirp/wxui/main.py | 522 ++ chirp/wxui/memedit.py | 601 ++ chirp/wxui/settingsedit.py | 161 + chirpc | 403 ++ chirpw | 167 + chirpwx.py | 57 + locale/Makefile | 28 + locale/check_parameters.py | 31 + locale/de.po | 1081 ++++ locale/en_US.po | 955 +++ locale/es_ES.po | 946 +++ locale/fr.po | 944 +++ locale/hu.po | 1216 ++++ locale/it.po | 944 +++ locale/nl.po | 945 +++ locale/pl.po | 987 +++ locale/pt_BR.po | 897 +++ locale/ru.po | 907 +++ locale/uk_UA.po | 917 +++ py3syntax.patch | 31 + pylintrc | 249 + requirements.txt | 4 + rpttool | 148 + run_all_tests.bat | 3 + setup.cfg | 5 + setup.py | 163 + share/chirp.desktop | 13 + share/chirp.icns | Bin 0 -> 80528 bytes share/chirp.ico | Bin 0 -> 119911 bytes share/chirp.png | Bin 0 -> 9687 bytes share/chirp.svg | 224 + share/chirpw.1 | 46 + share/contrib/chirp.rnc | 28 + share/contrib/chirp.rng | 58 + share/contrib/chirp_banks.rnc | 3 + share/contrib/chirp_banks.rng | 11 + share/contrib/chirp_memory.rnc | 64 + share/contrib/chirp_memory.rng | 193 + share/make_supported.py | 178 + stock_configs/DE Freenet Frequencies.csv | 7 + stock_configs/EU LPD and PMR Channels.csv | 86 + stock_configs/FR Marine VHF Channels.csv | 58 + stock_configs/KDR444.csv | 9 + stock_configs/NOAA Weather Alert.csv | 11 + .../UK Business Radio Simple Light Frequencies.csv | 16 + stock_configs/US 60 meter channels (Center).csv | 6 + stock_configs/US 60 meter channels (Dial).csv | 6 + stock_configs/US CA Railroad Channels.csv | 187 + stock_configs/US Calling Frequencies.csv | 5 + stock_configs/US FRS and GMRS Channels.csv | 53 + stock_configs/US MURS Channels.csv | 6 + stock_configs/US Marine VHF Channels.csv | 61 + test-requirements.txt | 4 + tests/__init__.py | 151 + tests/icom_clone_simulator.py | 195 + tests/images/Alinco_DJ-G7EG.img | Bin 0 -> 108480 bytes tests/images/Alinco_DJ175.img | Bin 0 -> 6896 bytes tests/images/Alinco_DJ596.img | Bin 0 -> 4096 bytes tests/images/Alinco_DR235T.img | Bin 0 -> 4096 bytes tests/images/AnyTone_OBLTR-8R.img | Bin 0 -> 32768 bytes tests/images/AnyTone_TERMN-8R.img | Bin 0 -> 32768 bytes tests/images/BTECH_GMRS-50X1.img | Bin 0 -> 16384 bytes tests/images/BTECH_GMRS-V1.img | Bin 0 -> 8200 bytes tests/images/BTECH_MURS-V1.img | Bin 0 -> 8200 bytes tests/images/BTECH_UV-2501+220.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-25X2.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-25X4.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-5001.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-50X2.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-50X3.img | Bin 0 -> 32768 bytes tests/images/BTECH_UV-5X3.img | Bin 0 -> 8206 bytes tests/images/Baofeng_BF-888.img | Bin 0 -> 992 bytes tests/images/Baofeng_BF-A58S.img | Bin 0 -> 8361 bytes tests/images/Baofeng_BF-T1.img | Bin 0 -> 2048 bytes tests/images/Baofeng_F-11.img | Bin 0 -> 6472 bytes tests/images/Baofeng_UV-3R.img | Bin 0 -> 3648 bytes tests/images/Baofeng_UV-5R.img | Bin 0 -> 6472 bytes tests/images/Baofeng_UV-6R.img | Bin 0 -> 8200 bytes tests/images/Baofeng_UV-B5.img | Bin 0 -> 4144 bytes tests/images/Baojie_BJ-9900.img | Bin 0 -> 6385 bytes tests/images/Boblov_X3Plus.img | Bin 0 -> 1008 bytes tests/images/Feidaxin_FD-268A.img | Bin 0 -> 2048 bytes tests/images/Feidaxin_FD-268B.img | Bin 0 -> 2048 bytes tests/images/Feidaxin_FD-288B.img | Bin 0 -> 2048 bytes tests/images/Generic_CSV.csv | 104 + tests/images/Icom_IC-208H.img | Bin 0 -> 9728 bytes tests/images/Icom_IC-2100H.img | Bin 0 -> 2016 bytes tests/images/Icom_IC-2200H.img | Bin 0 -> 6848 bytes tests/images/Icom_IC-2300H.img | Bin 0 -> 6304 bytes tests/images/Icom_IC-2720H.img | Bin 0 -> 5152 bytes tests/images/Icom_IC-2730A.img | Bin 0 -> 21312 bytes tests/images/Icom_IC-2820H.img | Bin 0 -> 44224 bytes tests/images/Icom_IC-P7.img | Bin 0 -> 29952 bytes tests/images/Icom_IC-Q7A.img | Bin 0 -> 1984 bytes tests/images/Icom_IC-T70.img | Bin 0 -> 6624 bytes tests/images/Icom_IC-T7H.img | Bin 0 -> 944 bytes tests/images/Icom_IC-T8A.img | Bin 0 -> 1968 bytes tests/images/Icom_IC-V82_U82.img | Bin 0 -> 6464 bytes tests/images/Icom_IC-W32A.img | Bin 0 -> 4064 bytes tests/images/Icom_IC-W32E.img | Bin 0 -> 4065 bytes tests/images/Icom_ID-31A.img | Bin 0 -> 87296 bytes tests/images/Icom_ID-51.img | Bin 0 -> 129856 bytes tests/images/Icom_ID-51_Plus.img | Bin 0 -> 129856 bytes tests/images/Icom_ID-800H_v2.img | Bin 0 -> 14528 bytes tests/images/Icom_ID-880H.img | Bin 0 -> 62976 bytes tests/images/Jetstream_JT220M.img | Bin 0 -> 8192 bytes tests/images/Jetstream_JT270M.img | Bin 0 -> 8192 bytes tests/images/Jetstream_JT270MH.img | Bin 0 -> 8192 bytes tests/images/KYD_IP-620.img | Bin 0 -> 8192 bytes tests/images/KYD_NC-630A.img | Bin 0 -> 824 bytes tests/images/Kenwood_HMK.hmk | 71 + tests/images/Kenwood_TH-D72_clone_mode.img | Bin 0 -> 65536 bytes tests/images/Kenwood_TK-272G.img | Bin 0 -> 32768 bytes tests/images/Kenwood_TK-3180K2.img | Bin 0 -> 53681 bytes tests/images/Kenwood_TK-760G.img | Bin 0 -> 32768 bytes tests/images/Kenwood_TK-8102.img | Bin 0 -> 1040 bytes tests/images/Kenwood_TK-8180.img | Bin 0 -> 53673 bytes tests/images/Kenwood_TS-480_CloneMode.img | Bin 0 -> 3018 bytes tests/images/LUITON_LT-725UV.img | Bin 0 -> 7176 bytes tests/images/Leixen_VV-898.img | Bin 0 -> 8192 bytes tests/images/Leixen_VV-898S.img | Bin 0 -> 8192 bytes tests/images/Polmar_DB-50M.img | Bin 0 -> 32768 bytes tests/images/Puxing_PX-2R.img | Bin 0 -> 4064 bytes tests/images/Puxing_PX-777.img | Bin 0 -> 3168 bytes tests/images/Puxing_PX-888K.img | Bin 0 -> 4096 bytes tests/images/QYT_KT7900D.img | Bin 0 -> 16384 bytes tests/images/QYT_KT8900D.img | Bin 0 -> 16384 bytes tests/images/Radioddity_R2.img | Bin 0 -> 1181 bytes tests/images/Radtel_T18.img | Bin 0 -> 1008 bytes tests/images/Retevis_RT21.img | Bin 0 -> 1024 bytes tests/images/Retevis_RT22.img | Bin 0 -> 1032 bytes tests/images/Retevis_RT23.img | Bin 0 -> 4096 bytes tests/images/Retevis_RT26.img | Bin 0 -> 1024 bytes tests/images/TDXone_TD-Q8A.img | Bin 0 -> 8200 bytes tests/images/TYT_TH-350.img | Bin 0 -> 4297 bytes tests/images/TYT_TH-7800.img | Bin 0 -> 65296 bytes tests/images/TYT_TH-9800.img | Bin 0 -> 65296 bytes tests/images/TYT_TH-UV3R-25.img | Bin 0 -> 2864 bytes tests/images/TYT_TH-UV3R.img | Bin 0 -> 2320 bytes tests/images/TYT_TH-UV8000.img | Bin 0 -> 5040 bytes tests/images/TYT_TH-UVF1.img | Bin 0 -> 4112 bytes tests/images/TYT_TH9000_144.img | Bin 0 -> 16384 bytes tests/images/Vertex_Standard_VXA-700.img | Bin 0 -> 4096 bytes tests/images/WACCOM_MINI-8900.img | Bin 0 -> 16384 bytes tests/images/Wouxun_KG-816.img | Bin 0 -> 8192 bytes tests/images/Wouxun_KG-818.img | Bin 0 -> 8192 bytes tests/images/Wouxun_KG-UV6.img | Bin 0 -> 8192 bytes tests/images/Wouxun_KG-UV8D.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UV8D_Plus.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UV8E.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UV9D_Plus.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UVD1P.img | Bin 0 -> 8192 bytes tests/images/Yaesu_FT-1500M.img | Bin 0 -> 4140 bytes tests/images/Yaesu_FT-1802M.img | Bin 0 -> 8011 bytes tests/images/Yaesu_FT-1D_R.img | Bin 0 -> 130507 bytes tests/images/Yaesu_FT-25R.img | Bin 0 -> 8689 bytes tests/images/Yaesu_FT-2800M.img | Bin 0 -> 7680 bytes tests/images/Yaesu_FT-2900R_1900R.img | Bin 0 -> 8000 bytes tests/images/Yaesu_FT-450D.img | Bin 0 -> 15024 bytes tests/images/Yaesu_FT-4VR.img | Bin 0 -> 8697 bytes tests/images/Yaesu_FT-4XE.img | Bin 0 -> 8697 bytes tests/images/Yaesu_FT-4XR.img | Bin 0 -> 8689 bytes tests/images/Yaesu_FT-50.img | Bin 0 -> 3723 bytes tests/images/Yaesu_FT-60.img | Bin 0 -> 28617 bytes tests/images/Yaesu_FT-65E.img | Bin 0 -> 8697 bytes tests/images/Yaesu_FT-65R.img | Bin 0 -> 8693 bytes tests/images/Yaesu_FT-70D.img | Bin 0 -> 65227 bytes tests/images/Yaesu_FT-7100M.img | Bin 0 -> 7936 bytes tests/images/Yaesu_FT-7800_7900.img | Bin 0 -> 31561 bytes tests/images/Yaesu_FT-817.img | Bin 0 -> 6509 bytes tests/images/Yaesu_FT-817ND.img | Bin 0 -> 6521 bytes tests/images/Yaesu_FT-817ND_US.img | Bin 0 -> 6651 bytes tests/images/Yaesu_FT-818.img | Bin 0 -> 6730 bytes tests/images/Yaesu_FT-857_897.img | Bin 0 -> 7341 bytes tests/images/Yaesu_FT-857_897_US.img | Bin 0 -> 7481 bytes tests/images/Yaesu_FT-8800.img | Bin 0 -> 22217 bytes tests/images/Yaesu_FT-8900.img | Bin 0 -> 14793 bytes tests/images/Yaesu_FT2D_R.img | Bin 0 -> 130507 bytes tests/images/Yaesu_FT3D_R.img | Bin 0 -> 130652 bytes tests/images/Yaesu_FTM-3200D_R.img | Bin 0 -> 65227 bytes tests/images/Yaesu_FTM-350.img | Bin 0 -> 65664 bytes tests/images/Yaesu_VX-2.img | Bin 0 -> 32595 bytes tests/images/Yaesu_VX-3.img | Bin 0 -> 32587 bytes tests/images/Yaesu_VX-5.img | Bin 0 -> 8123 bytes tests/images/Yaesu_VX-6.img | Bin 0 -> 32587 bytes tests/images/Yaesu_VX-7.img | Bin 0 -> 16211 bytes tests/images/Yaesu_VX-8DR.img | Bin 0 -> 65227 bytes tests/images/Yaesu_VX-8GE.img | Bin 0 -> 65227 bytes tests/images/Yaesu_VX-8R.img | Bin 0 -> 65227 bytes tests/run_tests | 3 + tests/run_tests.py | 1372 ++++ tests/test_drivers.py | 24 + tests/unit/__init__.py | 0 tests/unit/base.py | 50 + tests/unit/test_bitwise.py | 341 + tests/unit/test_chirp_common.py | 415 ++ tests/unit/test_directory.py | 69 + tests/unit/test_icom_clone.py | 117 + tests/unit/test_import_logic.py | 373 ++ tests/unit/test_mappingmodel.py | 283 + tests/unit/test_memedit_edits.py | 82 + tests/unit/test_platform.py | 62 + tests/unit/test_repeaterbook.py | 28 + tests/unit/test_settings.py | 140 + tests/unit/test_shiftdialog.py | 112 + tests/unit/test_utils.py | 21 + tests/unit/test_yaesu_clone.py | 41 + tools/Makefile | 35 + tools/bitdiff.py | 166 + tools/check_for_bug.sh | 7 + tools/checkpatch.sh | 72 + tools/cpep8.blacklist | 3 + tools/cpep8.exceptions | 10 + tools/cpep8.manifest | 152 + tools/cpep8.py | 137 + tools/icomsio.sh | 95 + tools/img2thd72.py | 127 + tools/serialsniff.c | 440 ++ tox.ini | 61 + 651 files changed, 136859 insertions(+) create mode 100644 COPYING create mode 100644 INSTALL create mode 100644 MANIFEST.in create mode 100644 README.chirpc create mode 100644 README.developers create mode 100644 README.md create mode 100644 README.rpttool create mode 100644 build/chirp.app/Contents/Info.plist create mode 100755 build/chirp.app/Contents/MacOS/chirp create mode 100644 build/chirp.app/Contents/PkgInfo create mode 100644 build/chirp.app/Contents/Resources/.placeholder create mode 100644 build/macos/make_pango.sh create mode 100755 build/make_source_release.sh create mode 100644 build/version create mode 100644 chirp/__init__.py create mode 100644 chirp/__pycache__/__init__.cpython-37.pyc create mode 100644 chirp/__pycache__/__init__.cpython-38.pyc create mode 100644 chirp/__pycache__/bandplan.cpython-37.pyc create mode 100644 chirp/__pycache__/bandplan.cpython-38.pyc create mode 100644 chirp/__pycache__/bandplan_au.cpython-37.pyc create mode 100644 chirp/__pycache__/bandplan_au.cpython-38.pyc create mode 100644 chirp/__pycache__/bandplan_iaru_r1.cpython-37.pyc create mode 100644 chirp/__pycache__/bandplan_iaru_r1.cpython-38.pyc create mode 100644 chirp/__pycache__/bandplan_iaru_r2.cpython-37.pyc create mode 100644 chirp/__pycache__/bandplan_iaru_r2.cpython-38.pyc create mode 100644 chirp/__pycache__/bandplan_iaru_r3.cpython-37.pyc create mode 100644 chirp/__pycache__/bandplan_iaru_r3.cpython-38.pyc create mode 100644 chirp/__pycache__/bandplan_na.cpython-37.pyc create mode 100644 chirp/__pycache__/bandplan_na.cpython-38.pyc create mode 100644 chirp/__pycache__/bitwise.cpython-37.pyc create mode 100644 chirp/__pycache__/bitwise.cpython-38.pyc create mode 100644 chirp/__pycache__/bitwise_grammar.cpython-37.pyc create mode 100644 chirp/__pycache__/bitwise_grammar.cpython-38.pyc create mode 100644 chirp/__pycache__/chirp_common.cpython-37.pyc create mode 100644 chirp/__pycache__/chirp_common.cpython-38.pyc create mode 100644 chirp/__pycache__/detect.cpython-37.pyc create mode 100644 chirp/__pycache__/detect.cpython-38.pyc create mode 100644 chirp/__pycache__/directory.cpython-37.pyc create mode 100644 chirp/__pycache__/directory.cpython-38.pyc create mode 100644 chirp/__pycache__/elib_intl.cpython-37.pyc create mode 100644 chirp/__pycache__/elib_intl.cpython-38.pyc create mode 100644 chirp/__pycache__/errors.cpython-37.pyc create mode 100644 chirp/__pycache__/errors.cpython-38.pyc create mode 100644 chirp/__pycache__/import_logic.cpython-37.pyc create mode 100644 chirp/__pycache__/import_logic.cpython-38.pyc create mode 100644 chirp/__pycache__/logger.cpython-37.pyc create mode 100644 chirp/__pycache__/logger.cpython-38.pyc create mode 100644 chirp/__pycache__/memmap.cpython-37.pyc create mode 100644 chirp/__pycache__/memmap.cpython-38.pyc create mode 100644 chirp/__pycache__/platform.cpython-37.pyc create mode 100644 chirp/__pycache__/platform.cpython-38.pyc create mode 100644 chirp/__pycache__/pyPEG.cpython-37.pyc create mode 100644 chirp/__pycache__/pyPEG.cpython-38.pyc create mode 100644 chirp/__pycache__/radioreference.cpython-37.pyc create mode 100644 chirp/__pycache__/radioreference.cpython-38.pyc create mode 100644 chirp/__pycache__/settings.cpython-37.pyc create mode 100644 chirp/__pycache__/settings.cpython-38.pyc create mode 100644 chirp/__pycache__/util.cpython-37.pyc create mode 100644 chirp/__pycache__/util.cpython-38.pyc create mode 100644 chirp/bandplan.py create mode 100644 chirp/bandplan_au.py create mode 100644 chirp/bandplan_iaru_r1.py create mode 100644 chirp/bandplan_iaru_r2.py create mode 100644 chirp/bandplan_iaru_r3.py create mode 100644 chirp/bandplan_na.py create mode 100644 chirp/bitwise.py create mode 100644 chirp/bitwise_grammar.py create mode 100644 chirp/chirp_common.py create mode 100644 chirp/detect.py create mode 100644 chirp/directory.py create mode 100644 chirp/dmrmarc.py create mode 100644 chirp/drivers/__init__.py create mode 100644 chirp/drivers/__pycache__/__init__.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/__init__.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/baofeng_wp970i.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/baofeng_wp970i.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/boblov_x3plus.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/boblov_x3plus.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/btech.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/btech.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft1500m.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft1500m.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft1802.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft1802.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft2d.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft2d.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft4.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft4.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft7800.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft7800.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft817.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft817.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft818.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft818.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ft857.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ft857.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ftm3200d.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ftm3200d.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/generic_csv.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/generic_csv.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/generic_tpe.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/generic_tpe.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/gmrsuv1.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/gmrsuv1.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/h777.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/h777.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/hobbypcb.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/hobbypcb.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic208.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic208.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic2100.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic2100.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic2200.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic2200.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic2300.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic2300.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic2720.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic2720.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic2730.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic2730.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic2820.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic2820.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic9x.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic9x.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic9x_icf.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic9x_icf.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic9x_icf_ll.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic9x_icf_ll.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ic9x_ll.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ic9x_ll.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icf.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icf.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icomciv.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icomciv.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icp7.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icp7.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icq7.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icq7.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ict70.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ict70.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ict7h.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ict7h.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ict8.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ict8.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icw32.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icw32.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icx8x.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icx8x.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/icx8x_ll.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/icx8x_ll.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/id31.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/id31.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/id51.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/id51.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/id51plus.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/id51plus.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/id800.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/id800.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/id880.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/id880.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/idrp.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/idrp.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/kenwood_hmk.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/kenwood_hmk.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/kenwood_itm.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/kenwood_itm.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/kenwood_live.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/kenwood_live.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/mursv1.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/mursv1.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/puxing_px888k.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/puxing_px888k.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/repeaterbook.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/repeaterbook.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/template.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/template.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/th350.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/th350.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/th_uv3r.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/th_uv3r.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/th_uv3r25.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/th_uv3r25.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/th_uvf8d.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/th_uvf8d.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/tk270.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/tk270.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/tk760.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/tk760.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/tk8102.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/tk8102.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/tk8180.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/tk8180.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/tmv71.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/tmv71.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/tmv71_ll.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/tmv71_ll.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/ts850.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/ts850.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/uv5r.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/uv5r.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/uv5x3.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/uv5x3.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/uv6r.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/uv6r.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/uvb5.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/uvb5.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx170.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx170.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx2.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx2.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx3.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx3.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx5.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx5.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx510.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx510.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx6.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx6.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx7.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx7.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/vx8.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/vx8.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/wouxun_common.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/wouxun_common.cpython-38.pyc create mode 100644 chirp/drivers/__pycache__/yaesu_clone.cpython-37.pyc create mode 100644 chirp/drivers/__pycache__/yaesu_clone.cpython-38.pyc create mode 100644 chirp/drivers/alinco.py create mode 100644 chirp/drivers/anytone.py create mode 100644 chirp/drivers/anytone_ht.py create mode 100644 chirp/drivers/ap510.py create mode 100644 chirp/drivers/baofeng_common.py create mode 100644 chirp/drivers/baofeng_uv3r.py create mode 100644 chirp/drivers/baofeng_wp970i.py create mode 100644 chirp/drivers/bf-t1.py create mode 100644 chirp/drivers/bj9900.py create mode 100644 chirp/drivers/bjuv55.py create mode 100644 chirp/drivers/boblov_x3plus.py create mode 100644 chirp/drivers/btech.py create mode 100644 chirp/drivers/fd268.py create mode 100644 chirp/drivers/ft1500m.py create mode 100644 chirp/drivers/ft1802.py create mode 100644 chirp/drivers/ft1d.py create mode 100644 chirp/drivers/ft2800.py create mode 100644 chirp/drivers/ft2900.py create mode 100644 chirp/drivers/ft2d.py create mode 100644 chirp/drivers/ft4.py create mode 100644 chirp/drivers/ft450d.py create mode 100644 chirp/drivers/ft50.py create mode 100644 chirp/drivers/ft60.py create mode 100644 chirp/drivers/ft70.py create mode 100644 chirp/drivers/ft7100.py create mode 100644 chirp/drivers/ft7800.py create mode 100644 chirp/drivers/ft8100.py create mode 100644 chirp/drivers/ft817.py create mode 100755 chirp/drivers/ft818.py create mode 100644 chirp/drivers/ft857.py create mode 100644 chirp/drivers/ft90.py create mode 100644 chirp/drivers/ftm3200d.py create mode 100644 chirp/drivers/ftm350.py create mode 100644 chirp/drivers/generic_csv.py create mode 100644 chirp/drivers/generic_tpe.py create mode 100644 chirp/drivers/gmrsuv1.py create mode 100644 chirp/drivers/h777.py create mode 100644 chirp/drivers/hobbypcb.py create mode 100644 chirp/drivers/ic208.py create mode 100644 chirp/drivers/ic2100.py create mode 100644 chirp/drivers/ic2200.py create mode 100644 chirp/drivers/ic2300.py create mode 100644 chirp/drivers/ic2720.py create mode 100644 chirp/drivers/ic2730.py create mode 100644 chirp/drivers/ic2820.py create mode 100644 chirp/drivers/ic9x.py create mode 100644 chirp/drivers/ic9x_icf.py create mode 100644 chirp/drivers/ic9x_icf_ll.py create mode 100644 chirp/drivers/ic9x_ll.py create mode 100644 chirp/drivers/icf.py create mode 100644 chirp/drivers/icomciv.py create mode 100644 chirp/drivers/icp7.py create mode 100644 chirp/drivers/icq7.py create mode 100644 chirp/drivers/ict70.py create mode 100644 chirp/drivers/ict7h.py create mode 100644 chirp/drivers/ict8.py create mode 100644 chirp/drivers/icw32.py create mode 100644 chirp/drivers/icx8x.py create mode 100644 chirp/drivers/icx8x_ll.py create mode 100644 chirp/drivers/id31.py create mode 100644 chirp/drivers/id51.py create mode 100644 chirp/drivers/id51plus.py create mode 100644 chirp/drivers/id800.py create mode 100644 chirp/drivers/id880.py create mode 100644 chirp/drivers/idrp.py create mode 100644 chirp/drivers/kenwood_hmk.py create mode 100644 chirp/drivers/kenwood_itm.py create mode 100644 chirp/drivers/kenwood_live.py create mode 100644 chirp/drivers/kguv8d.py create mode 100644 chirp/drivers/kguv8dplus.py create mode 100644 chirp/drivers/kguv8e.py create mode 100644 chirp/drivers/kguv9dplus.py create mode 100644 chirp/drivers/kyd.py create mode 100644 chirp/drivers/kyd_IP620.py create mode 100644 chirp/drivers/leixen.py create mode 100644 chirp/drivers/lt725uv.py create mode 100644 chirp/drivers/mursv1.py create mode 100644 chirp/drivers/puxing.py create mode 100644 chirp/drivers/puxing_px888k.py create mode 100644 chirp/drivers/radioddity_r2.py create mode 100644 chirp/drivers/radtel_t18.py create mode 100644 chirp/drivers/repeaterbook.py create mode 100644 chirp/drivers/retevis_rt1.py create mode 100644 chirp/drivers/retevis_rt21.py create mode 100644 chirp/drivers/retevis_rt22.py create mode 100644 chirp/drivers/retevis_rt23.py create mode 100644 chirp/drivers/retevis_rt26.py create mode 100644 chirp/drivers/rfinder.py create mode 100644 chirp/drivers/rh5r_v2.py create mode 100644 chirp/drivers/tdxone_tdq8a.py create mode 100644 chirp/drivers/template.py create mode 100644 chirp/drivers/th350.py create mode 100644 chirp/drivers/th7800.py create mode 100644 chirp/drivers/th9000.py create mode 100644 chirp/drivers/th9800.py create mode 100644 chirp/drivers/th_uv3r.py create mode 100644 chirp/drivers/th_uv3r25.py create mode 100644 chirp/drivers/th_uv8000.py create mode 100644 chirp/drivers/th_uvf8d.py create mode 100644 chirp/drivers/thd72.py create mode 100644 chirp/drivers/thuv1f.py create mode 100644 chirp/drivers/tk270.py create mode 100644 chirp/drivers/tk760.py create mode 100644 chirp/drivers/tk760g.py create mode 100644 chirp/drivers/tk8102.py create mode 100644 chirp/drivers/tk8180.py create mode 100644 chirp/drivers/tmv71.py create mode 100644 chirp/drivers/tmv71_ll.py create mode 100644 chirp/drivers/ts2000.py create mode 100644 chirp/drivers/ts480.py create mode 100644 chirp/drivers/ts590.py create mode 100644 chirp/drivers/ts850.py create mode 100644 chirp/drivers/uv5r.py create mode 100644 chirp/drivers/uv5x3.py create mode 100644 chirp/drivers/uv6r.py create mode 100644 chirp/drivers/uvb5.py create mode 100644 chirp/drivers/vgc.py create mode 100644 chirp/drivers/vx170.py create mode 100644 chirp/drivers/vx2.py create mode 100644 chirp/drivers/vx3.py create mode 100644 chirp/drivers/vx5.py create mode 100644 chirp/drivers/vx510.py create mode 100644 chirp/drivers/vx6.py create mode 100644 chirp/drivers/vx7.py create mode 100644 chirp/drivers/vx8.py create mode 100644 chirp/drivers/vxa700.py create mode 100644 chirp/drivers/wouxun.py create mode 100644 chirp/drivers/wouxun_common.py create mode 100644 chirp/drivers/yaesu_clone.py create mode 100644 chirp/elib_intl.py create mode 100644 chirp/errors.py create mode 100644 chirp/import_logic.py create mode 100644 chirp/logger.py create mode 100644 chirp/memmap.py create mode 100644 chirp/platform.py create mode 100644 chirp/pyPEG.py create mode 100644 chirp/radioreference.py create mode 100644 chirp/settings.py create mode 100644 chirp/ui/__init__.py create mode 100644 chirp/ui/__pycache__/__init__.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/__init__.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/bandplans.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/bandplans.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/bankedit.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/bankedit.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/clone.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/clone.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/cloneprog.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/cloneprog.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/common.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/common.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/compat.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/compat.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/config.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/config.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/dstaredit.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/dstaredit.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/editorset.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/editorset.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/fips.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/fips.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/importdialog.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/importdialog.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/inputdialog.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/inputdialog.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/mainapp.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/mainapp.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/memdetail.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/memdetail.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/memedit.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/memedit.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/miscwidgets.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/miscwidgets.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/radiobrowser.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/radiobrowser.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/reporting.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/reporting.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/settingsedit.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/settingsedit.cpython-38.pyc create mode 100644 chirp/ui/__pycache__/shiftdialog.cpython-37.pyc create mode 100644 chirp/ui/__pycache__/shiftdialog.cpython-38.pyc create mode 100644 chirp/ui/bandplans.py create mode 100644 chirp/ui/bankedit.py create mode 100644 chirp/ui/clone.py create mode 100644 chirp/ui/cloneprog.py create mode 100644 chirp/ui/common.py create mode 100644 chirp/ui/compat.py create mode 100644 chirp/ui/config.py create mode 100644 chirp/ui/dstaredit.py create mode 100644 chirp/ui/editorset.py create mode 100644 chirp/ui/fips.py create mode 100644 chirp/ui/importdialog.py create mode 100644 chirp/ui/inputdialog.py create mode 100644 chirp/ui/mainapp.py create mode 100644 chirp/ui/memdetail.py create mode 100644 chirp/ui/memedit.py create mode 100644 chirp/ui/miscwidgets.py create mode 100644 chirp/ui/radiobrowser.py create mode 100644 chirp/ui/reporting.py create mode 100644 chirp/ui/settingsedit.py create mode 100644 chirp/ui/shiftdialog.py create mode 100644 chirp/util.py create mode 100644 chirp/wxui/__init__.py create mode 100644 chirp/wxui/clone.py create mode 100644 chirp/wxui/common.py create mode 100644 chirp/wxui/developer.py create mode 100644 chirp/wxui/main.py create mode 100644 chirp/wxui/memedit.py create mode 100644 chirp/wxui/settingsedit.py create mode 100755 chirpc create mode 100755 chirpw create mode 100755 chirpwx.py create mode 100644 locale/Makefile create mode 100755 locale/check_parameters.py create mode 100644 locale/de.po create mode 100644 locale/en_US.po create mode 100644 locale/es_ES.po create mode 100644 locale/fr.po create mode 100644 locale/hu.po create mode 100644 locale/it.po create mode 100644 locale/nl.po create mode 100644 locale/pl.po create mode 100644 locale/pt_BR.po create mode 100644 locale/ru.po create mode 100644 locale/uk_UA.po create mode 100644 py3syntax.patch create mode 100644 pylintrc create mode 100644 requirements.txt create mode 100755 rpttool create mode 100644 run_all_tests.bat create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 share/chirp.desktop create mode 100644 share/chirp.icns create mode 100755 share/chirp.ico create mode 100644 share/chirp.png create mode 100644 share/chirp.svg create mode 100644 share/chirpw.1 create mode 100644 share/contrib/chirp.rnc create mode 100644 share/contrib/chirp.rng create mode 100644 share/contrib/chirp_banks.rnc create mode 100644 share/contrib/chirp_banks.rng create mode 100644 share/contrib/chirp_memory.rnc create mode 100644 share/contrib/chirp_memory.rng create mode 100755 share/make_supported.py create mode 100644 stock_configs/DE Freenet Frequencies.csv create mode 100644 stock_configs/EU LPD and PMR Channels.csv create mode 100644 stock_configs/FR Marine VHF Channels.csv create mode 100644 stock_configs/KDR444.csv create mode 100644 stock_configs/NOAA Weather Alert.csv create mode 100644 stock_configs/UK Business Radio Simple Light Frequencies.csv create mode 100644 stock_configs/US 60 meter channels (Center).csv create mode 100644 stock_configs/US 60 meter channels (Dial).csv create mode 100644 stock_configs/US CA Railroad Channels.csv create mode 100644 stock_configs/US Calling Frequencies.csv create mode 100644 stock_configs/US FRS and GMRS Channels.csv create mode 100644 stock_configs/US MURS Channels.csv create mode 100644 stock_configs/US Marine VHF Channels.csv create mode 100644 test-requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/icom_clone_simulator.py create mode 100644 tests/images/Alinco_DJ-G7EG.img create mode 100644 tests/images/Alinco_DJ175.img create mode 100644 tests/images/Alinco_DJ596.img create mode 100644 tests/images/Alinco_DR235T.img create mode 100644 tests/images/AnyTone_OBLTR-8R.img create mode 100644 tests/images/AnyTone_TERMN-8R.img create mode 100644 tests/images/BTECH_GMRS-50X1.img create mode 100644 tests/images/BTECH_GMRS-V1.img create mode 100644 tests/images/BTECH_MURS-V1.img create mode 100755 tests/images/BTECH_UV-2501+220.img create mode 100755 tests/images/BTECH_UV-25X2.img create mode 100755 tests/images/BTECH_UV-25X4.img create mode 100755 tests/images/BTECH_UV-5001.img create mode 100755 tests/images/BTECH_UV-50X2.img create mode 100644 tests/images/BTECH_UV-50X3.img create mode 100755 tests/images/BTECH_UV-5X3.img create mode 100644 tests/images/Baofeng_BF-888.img create mode 100644 tests/images/Baofeng_BF-A58S.img create mode 100644 tests/images/Baofeng_BF-T1.img create mode 100644 tests/images/Baofeng_F-11.img create mode 100644 tests/images/Baofeng_UV-3R.img create mode 100644 tests/images/Baofeng_UV-5R.img create mode 100755 tests/images/Baofeng_UV-6R.img create mode 100644 tests/images/Baofeng_UV-B5.img create mode 100644 tests/images/Baojie_BJ-9900.img create mode 100644 tests/images/Boblov_X3Plus.img create mode 100755 tests/images/Feidaxin_FD-268A.img create mode 100755 tests/images/Feidaxin_FD-268B.img create mode 100755 tests/images/Feidaxin_FD-288B.img create mode 100644 tests/images/Generic_CSV.csv create mode 100644 tests/images/Icom_IC-208H.img create mode 100644 tests/images/Icom_IC-2100H.img create mode 100644 tests/images/Icom_IC-2200H.img create mode 100644 tests/images/Icom_IC-2300H.img create mode 100644 tests/images/Icom_IC-2720H.img create mode 100644 tests/images/Icom_IC-2730A.img create mode 100644 tests/images/Icom_IC-2820H.img create mode 100755 tests/images/Icom_IC-P7.img create mode 100644 tests/images/Icom_IC-Q7A.img create mode 100644 tests/images/Icom_IC-T70.img create mode 100644 tests/images/Icom_IC-T7H.img create mode 100644 tests/images/Icom_IC-T8A.img create mode 100644 tests/images/Icom_IC-V82_U82.img create mode 100644 tests/images/Icom_IC-W32A.img create mode 100644 tests/images/Icom_IC-W32E.img create mode 100644 tests/images/Icom_ID-31A.img create mode 100644 tests/images/Icom_ID-51.img create mode 100755 tests/images/Icom_ID-51_Plus.img create mode 100644 tests/images/Icom_ID-800H_v2.img create mode 100644 tests/images/Icom_ID-880H.img create mode 100644 tests/images/Jetstream_JT220M.img create mode 100644 tests/images/Jetstream_JT270M.img create mode 100644 tests/images/Jetstream_JT270MH.img create mode 100755 tests/images/KYD_IP-620.img create mode 100755 tests/images/KYD_NC-630A.img create mode 100644 tests/images/Kenwood_HMK.hmk create mode 100644 tests/images/Kenwood_TH-D72_clone_mode.img create mode 100755 tests/images/Kenwood_TK-272G.img create mode 100644 tests/images/Kenwood_TK-3180K2.img create mode 100755 tests/images/Kenwood_TK-760G.img create mode 100644 tests/images/Kenwood_TK-8102.img create mode 100644 tests/images/Kenwood_TK-8180.img create mode 100644 tests/images/Kenwood_TS-480_CloneMode.img create mode 100755 tests/images/LUITON_LT-725UV.img create mode 100644 tests/images/Leixen_VV-898.img create mode 100755 tests/images/Leixen_VV-898S.img create mode 100644 tests/images/Polmar_DB-50M.img create mode 100644 tests/images/Puxing_PX-2R.img create mode 100644 tests/images/Puxing_PX-777.img create mode 100644 tests/images/Puxing_PX-888K.img create mode 100755 tests/images/QYT_KT7900D.img create mode 100755 tests/images/QYT_KT8900D.img create mode 100644 tests/images/Radioddity_R2.img create mode 100755 tests/images/Radtel_T18.img create mode 100644 tests/images/Retevis_RT21.img create mode 100644 tests/images/Retevis_RT22.img create mode 100755 tests/images/Retevis_RT23.img create mode 100644 tests/images/Retevis_RT26.img create mode 100755 tests/images/TDXone_TD-Q8A.img create mode 100644 tests/images/TYT_TH-350.img create mode 100644 tests/images/TYT_TH-7800.img create mode 100644 tests/images/TYT_TH-9800.img create mode 100755 tests/images/TYT_TH-UV3R-25.img create mode 100644 tests/images/TYT_TH-UV3R.img create mode 100644 tests/images/TYT_TH-UV8000.img create mode 100644 tests/images/TYT_TH-UVF1.img create mode 100644 tests/images/TYT_TH9000_144.img create mode 100644 tests/images/Vertex_Standard_VXA-700.img create mode 100755 tests/images/WACCOM_MINI-8900.img create mode 100644 tests/images/Wouxun_KG-816.img create mode 100644 tests/images/Wouxun_KG-818.img create mode 100644 tests/images/Wouxun_KG-UV6.img create mode 100644 tests/images/Wouxun_KG-UV8D.img create mode 100644 tests/images/Wouxun_KG-UV8D_Plus.img create mode 100644 tests/images/Wouxun_KG-UV8E.img create mode 100644 tests/images/Wouxun_KG-UV9D_Plus.img create mode 100644 tests/images/Wouxun_KG-UVD1P.img create mode 100644 tests/images/Yaesu_FT-1500M.img create mode 100644 tests/images/Yaesu_FT-1802M.img create mode 100644 tests/images/Yaesu_FT-1D_R.img create mode 100644 tests/images/Yaesu_FT-25R.img create mode 100644 tests/images/Yaesu_FT-2800M.img create mode 100755 tests/images/Yaesu_FT-2900R_1900R.img create mode 100644 tests/images/Yaesu_FT-450D.img create mode 100644 tests/images/Yaesu_FT-4VR.img create mode 100644 tests/images/Yaesu_FT-4XE.img create mode 100644 tests/images/Yaesu_FT-4XR.img create mode 100755 tests/images/Yaesu_FT-50.img create mode 100644 tests/images/Yaesu_FT-60.img create mode 100644 tests/images/Yaesu_FT-65E.img create mode 100644 tests/images/Yaesu_FT-65R.img create mode 100644 tests/images/Yaesu_FT-70D.img create mode 100644 tests/images/Yaesu_FT-7100M.img create mode 100644 tests/images/Yaesu_FT-7800_7900.img create mode 100644 tests/images/Yaesu_FT-817.img create mode 100644 tests/images/Yaesu_FT-817ND.img create mode 100644 tests/images/Yaesu_FT-817ND_US.img create mode 100644 tests/images/Yaesu_FT-818.img create mode 100644 tests/images/Yaesu_FT-857_897.img create mode 100644 tests/images/Yaesu_FT-857_897_US.img create mode 100644 tests/images/Yaesu_FT-8800.img create mode 100644 tests/images/Yaesu_FT-8900.img create mode 100644 tests/images/Yaesu_FT2D_R.img create mode 100644 tests/images/Yaesu_FT3D_R.img create mode 100644 tests/images/Yaesu_FTM-3200D_R.img create mode 100644 tests/images/Yaesu_FTM-350.img create mode 100644 tests/images/Yaesu_VX-2.img create mode 100644 tests/images/Yaesu_VX-3.img create mode 100644 tests/images/Yaesu_VX-5.img create mode 100644 tests/images/Yaesu_VX-6.img create mode 100644 tests/images/Yaesu_VX-7.img create mode 100644 tests/images/Yaesu_VX-8DR.img create mode 100644 tests/images/Yaesu_VX-8GE.img create mode 100644 tests/images/Yaesu_VX-8R.img create mode 100755 tests/run_tests create mode 100755 tests/run_tests.py create mode 100644 tests/test_drivers.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/base.py create mode 100644 tests/unit/test_bitwise.py create mode 100644 tests/unit/test_chirp_common.py create mode 100644 tests/unit/test_directory.py create mode 100644 tests/unit/test_icom_clone.py create mode 100644 tests/unit/test_import_logic.py create mode 100644 tests/unit/test_mappingmodel.py create mode 100644 tests/unit/test_memedit_edits.py create mode 100644 tests/unit/test_platform.py create mode 100644 tests/unit/test_repeaterbook.py create mode 100644 tests/unit/test_settings.py create mode 100644 tests/unit/test_shiftdialog.py create mode 100644 tests/unit/test_utils.py create mode 100644 tests/unit/test_yaesu_clone.py create mode 100644 tools/Makefile create mode 100644 tools/bitdiff.py create mode 100755 tools/check_for_bug.sh create mode 100755 tools/checkpatch.sh create mode 100644 tools/cpep8.blacklist create mode 100644 tools/cpep8.exceptions create mode 100644 tools/cpep8.manifest create mode 100755 tools/cpep8.py create mode 100755 tools/icomsio.sh create mode 100644 tools/img2thd72.py create mode 100644 tools/serialsniff.c create mode 100644 tox.ini diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..a55c4d4 --- /dev/null +++ b/INSTALL @@ -0,0 +1,52 @@ +This file describes the installation of Chirp without package management +on Linux and other Unix-like operating systems. This sort of thing may +be your only choice because 1) a package has not yet been made for your +OS or distribution, 2) the packaged version is obsolete, or 3) you want +to try a daily build. + + +For Debian, Ubuntu, and related systems, the following packages are required: +python +python-gtk2 +python-libxml2 +python-libxslt1 +python-serial +python-suds (optional) +python-support + +For Redhat, Fedora, CentOS and related systems, the following packages +are required: (This list is incomplete. Please submit corrections.) +python +pygtk2 +libxml2-python +python-libxslt + +For openSUSE, the following packages are required: +python +python-gtk +python-pyserial +python-libxml2 +libxslt-python +python-suds-jurko + +Once these packages are installed, you can run Chirp directly from the +distribution directory by typing "./chirpw". If you want to install it +properly, type this: + + sudo python setup.py install --record files.txt + +This will install the package and create a list of files that were +added to your system. If you want to deinstall Chirp, type this: + + sudo xargs -0 rm -rf < files.txt + +This will cause rm(1) to take its list of arguments from the file named +"files.txt" and remove those files from the system. If you forgot to +create "files.txt", you can simply reinstall the way it is shown here +and continue on your way. + +Note: This will not uninstall directories created by the installation of +Chirp. Presence of these empty directories shouldn't be a problem, but +if they are, it's easy to go through the files.txt file, identify them, +and remove them. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..77a5f3a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include *.xsd +include share/*.desktop +include share/chirp.png +include share/chirp.ico +include share/*.1 +include stock_configs/* +include COPYING +include *requirements.txt diff --git a/README.chirpc b/README.chirpc new file mode 100644 index 0000000..a367fac --- /dev/null +++ b/README.chirpc @@ -0,0 +1,104 @@ +chirpc: CHIRP Command-line interface +==================================== + +CHIRP provides a CLI tool (chirpc) to interact with your radio and +memory image files. It has been designed to be used from programs or +scripts written in other languages, providing facilities for automating +queries and transformations. + + +WARNING: All modifications are made in-place, overwriting the original +file with new contents. Be sure to make a backup copy of any files +that you want unchanged. + + +======== +Cookbook +======== + +This section provides copy-and-paste recipies for accomplishing some +tasks using the CLI. + + +List Radios +----------- + +To see the list of supported names that can be passed to the +-r/--radio option: + + chirpc --list-radios + + +Download from Radio +------------------- + +To download a new image from your radio: + + chirpc -r --serial= --mmap= --download-mmap + +This will connect to the specified on , saving the image +obtained from the radio into the specified . + + +Upload to Radio +--------------- + +To upload an existing image to your radio: + + chirpc -r --serial= --mmap= --upload-mmap + +This will connect to the specified on , loading the image +in the specified onto the radio. + + +List Settings +------------- + +For radios that support settings, you can list the current settings +in a saved image: + + chirpc --mmap= --list-settings + + +Show Memory Channels +-------------------- + +You can list all current memory channels in a saved image: + + chirpc --mmap= --list-mem + +That command only lists the currently programmed channels. To see the +complete list (including empty channels), add '--verbose'. + +To view only a single channel, use the --get-mem option: + + chirpc --mmap= --get-mem + + +Set a Memory Channel +-------------------- + + chirpc --mmap= --set-mem-name= ... + +See the --help text for a complete list of options that can be used +to configure the channel. Any settings that are not configured using +a command option will be left unchanged. + + +Clearing a Memory Channel +------------------------- + +You can clear a memory channel, discarding all settings: + + chirpc --mmap= --clear-mem + + +Copying a Memory Channel +------------------------ + +You can copy a memory channel: + + chirpc --mmap= --copy-mem + +Note: The contents of will be overwritten with +the contents from diff --git a/README.developers b/README.developers new file mode 100644 index 0000000..e784b2a --- /dev/null +++ b/README.developers @@ -0,0 +1,64 @@ +This file describes some features in the Chirp Developer's Functions +that allow some customization of their behavior, but are only +accessible through editing the chirp.config file before starting +Chirp, and are not accessible through the GUI. + +The Developer Functions are enabled in the GUI by checking the box +in the "Help" tab. They enable the Image Browser tab in the left +sidebar, several tools under the View -> Developer tab, and some others. + +These directives are similar to other chirp.config entries in syntax. +They reside in the [developer] section in chirp.config. +This section, and all the directives, are not required to be present; +all the options have defaults. + +Here is an example [developer] section listing all the directives, +followed by an explanation of each directive: + +=================================== +[developer] +diff_fontsize = 16 +browser_fontsize = 13 +hexdump_addrfmt = %(addr)04i x%(addr)04X + +=================================== +browser_fontsize = +This specifies the fontsize used in the file browser, invoked by selecting +the "Browser" tab in the left sidebar, which is visible when the Developer +tools are enabled. + +The default size is 10. Values less than 4, greater than 144, or not +recognized as an integer will result in a log message and the default +size will be used. + +======== +diff_fontize = +This specifies the fontsize used in the hex dump/diff display which is +invoked by selecting View -> Developer -> Diff tabs. + +The default size is 11. Values less than 4, greater than 144, or not +recognized as an integer will result in a log message and the default +size will be used. + +======== +hexdump_addrfmt = +This specifies the format of the address printed during some hexdump +operations. The default is %(addr)03i which prints the byte offset +of the first byte of each line in decimal, producing lines such as + 064: 00 00 00 ... + +Any of the variables in local scope of chirp/util.py::hexdump() are +valid for substitution, including block and block_size. Any may +be used more than once. The example above, %(addr)04i x%(addr)04X +produces lines such as + 0064 x0040: 00 00 00 ... + +0x%(addr)04x specifies lower case hex, and wwould produce lines such as + 0x0040: 00 00 00 ... + +Exceptions that have been observed in testing formats which are +invalid in this context are caught, and the default format is used. + +======== + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a93ffd --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# chirp-gentoo +A custom repository for CHIRP, a tool for configuring HAM radios. + +## What's Different? +The official CHIRP project does not support Python 3 and depends on pygtk which hasn't seen a stable release since April 2011. + +chirp-gentoo is a fork of the py3 branch of the official CHIRP project with some syntax fixes leftover from Python2. + +## The Story + +When I first started using Gentoo again CHIRP was still a part of the main Gentoo Portage Repository. + +Eventually Gentoo began migrating away from Python2 and due to a lack of movement in the official project it was removed from the official Gentoo repository. + +I still needed to configure my radios and maintain an up to date Gentoo desktop so I cloned the py3 branch from the official CHIRP project and fixed a few syntax errors leftover from Python2 and everything worked great. + +I was able to download an image from my BF-F8HP, fully reconfigure it and upload the new image back to my radio. + + +### Repo Source (Forked From) + + - Mercurial Repository: http://d-rats.com/hg/chirp.hg + - Revision: py3 + - Number: 3351:68534f20c141 + +### Bugs and Patches Submitted to Official Project + + - https://chirp.danplanet.com/issues/8475 - Fixed old Python2 Syntax + +### Related Bugs in Gentoo + + - https://bugs.gentoo.org/708304 - Removed CHIRP from Portage + diff --git a/README.rpttool b/README.rpttool new file mode 100644 index 0000000..9968348 --- /dev/null +++ b/README.rpttool @@ -0,0 +1,23 @@ + *** KK7DS ID-RP* Frequency Tool *** + +This is a simple tool that allows setting of the frequency parameters +on ICOM D-STAR Repeaters. It has been tested on the following +modules: + + - ID-RP2000V and ID-RP4000V + - ID-RP2D (the ID-RP2V is expected to work as well) + +To run the tool, connect one of the repeater's SERVICE ports to a USB +port on this machine and then run the tool as root: + + # ./rpttool + +WARNING: Do not connect or try to program the ID-RP2C controller +module. This tool should be safe to use, but no guarantees are made. +The author believes that the ICOM devices are smart enough to prevent +bricking, but you're on your own if anything happens. + +If you have any other devices connected that are using an FTDI +USB-to-Serial device, they must be disconnected prior to running this +tool. The FTDI driver module will be re-loaded with a special +parameter to allow it to recognize the ICOM device. diff --git a/build/chirp.app/Contents/Info.plist b/build/chirp.app/Contents/Info.plist new file mode 100644 index 0000000..673eeeb --- /dev/null +++ b/build/chirp.app/Contents/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + chirp + CFBundleIconFile + Chirp + CFBundleIdentifier + com.danplanet.chirp + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + chirp + CFBundleShortVersionString + 0.1 + CFBundleVersion + 0.1 + LSHasLocalizedDisplayName + + LSMinimumSystemVersion + 10.5 + + diff --git a/build/chirp.app/Contents/MacOS/chirp b/build/chirp.app/Contents/MacOS/chirp new file mode 100755 index 0000000..d0ff86e --- /dev/null +++ b/build/chirp.app/Contents/MacOS/chirp @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +LOCATION=$(dirname "${BASH_SOURCE}") + +PYTHON=/opt/kk7ds/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python + +not_translocated () { + security translocate-status-check "${LOCATION}" 2>&1 | grep -q -e NOT -e unknown -e "not found" +} + +if [ ! -x $PYTHON ]; then + PYTHON=/opt/kk7ds/bin/python2.7 +elif not_translocated; then + ln -s $PYTHON "${LOCATION}/../CHIRP" + PYTHON=${LOCATION}/../CHIRP + export PYTHONPATH=/opt/kk7ds/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages +fi + +exec "$PYTHON" "${LOCATION}/../Resources/chirp/chirpw" diff --git a/build/chirp.app/Contents/PkgInfo b/build/chirp.app/Contents/PkgInfo new file mode 100644 index 0000000..bd04210 --- /dev/null +++ b/build/chirp.app/Contents/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/build/chirp.app/Contents/Resources/.placeholder b/build/chirp.app/Contents/Resources/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/build/macos/make_pango.sh b/build/macos/make_pango.sh new file mode 100644 index 0000000..57d3429 --- /dev/null +++ b/build/macos/make_pango.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +make_pango_modules() { + local src=$1 + local dst=$2 + local sf=${src}/etc/pango/pango.modules + local df=${dst}/etc/pango/pango.modules + + cat $sf | sed 's/\/opt\/.*\/lib/..\/Resources/' > $df +} + +make_pango_rc() { + local src=$1 + local dst=$2 + local sf=${src}/etc/pango/pangorc + local df=${dst}/etc/pango/pangorc + + cat $sf | sed 's/\/opt\/.*\/etc/.\/etc/' > $df +} + +make_pangox_aliases() { + local src=$1 + local dst=$2 + + cp ${src}/etc/pango/pangox.aliases ${dst}/etc/pango +} + +usage() { + echo 'Usage: make_pango.sh [PATH_TO_MACPORTS] [PATH_TO_APP]' + echo 'Example:' + echo ' make_pango.sh /opt/local dist/d-rats.app' +} + +if [ -z "$1" ]; then + usage + exit 1 +fi + +if [ -z "$2" ]; then + usage + exit 1 +fi + +base=$1 +app="$2/Contents/Resources" + +mkdir -p ${app}/etc/pango + +make_pango_modules $base $app +make_pango_rc $base $app +make_pangox_aliases $base $app diff --git a/build/make_source_release.sh b/build/make_source_release.sh new file mode 100755 index 0000000..2a5af86 --- /dev/null +++ b/build/make_source_release.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +VERSION=$(cat build/version) +INCLUDE="COPYING" +TMP=$(mktemp -d) +EXCLUDE="" + +sed -i 's/^CHIRP_VERSION.*$/CHIRP_VERSION=\"'$VERSION'\"/' chirp/__init__.py + +RELDIR=chirp-${VERSION} + +DST="${TMP}/${RELDIR}" + +mkdir -p $DST + +cp -rav --parents chirp/*.py chirp/drivers/*.py csvdump/*.py chirp/ui/* $DST +cp -av *.py ${DST} + +cp -rav $INCLUDE ${DST} + +(cd $TMP && tar czf - $RELDIR) > ${RELDIR}.tar.gz + +rm -Rf ${TMP}/${RELDIR} diff --git a/build/version b/build/version new file mode 100644 index 0000000..8508cef --- /dev/null +++ b/build/version @@ -0,0 +1 @@ +0.3.0dev diff --git a/chirp/__init__.py b/chirp/__init__.py new file mode 100644 index 0000000..7b48821 --- /dev/null +++ b/chirp/__init__.py @@ -0,0 +1,27 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +from glob import glob + +CHIRP_VERSION = "0.3.0dev" + +module_dir = os.path.dirname(sys.modules["chirp"].__file__) +__all__ = [] +for i in glob(os.path.join(module_dir, "*.py")): + name = os.path.basename(i)[:-3] + if not name.startswith("__"): + __all__.append(name) diff --git a/chirp/__pycache__/__init__.cpython-37.pyc b/chirp/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..ee9bb2e Binary files /dev/null and b/chirp/__pycache__/__init__.cpython-37.pyc differ diff --git a/chirp/__pycache__/__init__.cpython-38.pyc b/chirp/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..a86f803 Binary files /dev/null and b/chirp/__pycache__/__init__.cpython-38.pyc differ diff --git a/chirp/__pycache__/bandplan.cpython-37.pyc b/chirp/__pycache__/bandplan.cpython-37.pyc new file mode 100644 index 0000000..481029f Binary files /dev/null and b/chirp/__pycache__/bandplan.cpython-37.pyc differ diff --git a/chirp/__pycache__/bandplan.cpython-38.pyc b/chirp/__pycache__/bandplan.cpython-38.pyc new file mode 100644 index 0000000..5e3b4e2 Binary files /dev/null and b/chirp/__pycache__/bandplan.cpython-38.pyc differ diff --git a/chirp/__pycache__/bandplan_au.cpython-37.pyc b/chirp/__pycache__/bandplan_au.cpython-37.pyc new file mode 100644 index 0000000..f504061 Binary files /dev/null and b/chirp/__pycache__/bandplan_au.cpython-37.pyc differ diff --git a/chirp/__pycache__/bandplan_au.cpython-38.pyc b/chirp/__pycache__/bandplan_au.cpython-38.pyc new file mode 100644 index 0000000..d1b5220 Binary files /dev/null and b/chirp/__pycache__/bandplan_au.cpython-38.pyc differ diff --git a/chirp/__pycache__/bandplan_iaru_r1.cpython-37.pyc b/chirp/__pycache__/bandplan_iaru_r1.cpython-37.pyc new file mode 100644 index 0000000..a61ac66 Binary files /dev/null and b/chirp/__pycache__/bandplan_iaru_r1.cpython-37.pyc differ diff --git a/chirp/__pycache__/bandplan_iaru_r1.cpython-38.pyc b/chirp/__pycache__/bandplan_iaru_r1.cpython-38.pyc new file mode 100644 index 0000000..369de54 Binary files /dev/null and b/chirp/__pycache__/bandplan_iaru_r1.cpython-38.pyc differ diff --git a/chirp/__pycache__/bandplan_iaru_r2.cpython-37.pyc b/chirp/__pycache__/bandplan_iaru_r2.cpython-37.pyc new file mode 100644 index 0000000..d5f7b1c Binary files /dev/null and b/chirp/__pycache__/bandplan_iaru_r2.cpython-37.pyc differ diff --git a/chirp/__pycache__/bandplan_iaru_r2.cpython-38.pyc b/chirp/__pycache__/bandplan_iaru_r2.cpython-38.pyc new file mode 100644 index 0000000..bed9f08 Binary files /dev/null and b/chirp/__pycache__/bandplan_iaru_r2.cpython-38.pyc differ diff --git a/chirp/__pycache__/bandplan_iaru_r3.cpython-37.pyc b/chirp/__pycache__/bandplan_iaru_r3.cpython-37.pyc new file mode 100644 index 0000000..3c329f7 Binary files /dev/null and b/chirp/__pycache__/bandplan_iaru_r3.cpython-37.pyc differ diff --git a/chirp/__pycache__/bandplan_iaru_r3.cpython-38.pyc b/chirp/__pycache__/bandplan_iaru_r3.cpython-38.pyc new file mode 100644 index 0000000..3a1eb28 Binary files /dev/null and b/chirp/__pycache__/bandplan_iaru_r3.cpython-38.pyc differ diff --git a/chirp/__pycache__/bandplan_na.cpython-37.pyc b/chirp/__pycache__/bandplan_na.cpython-37.pyc new file mode 100644 index 0000000..46ab73d Binary files /dev/null and b/chirp/__pycache__/bandplan_na.cpython-37.pyc differ diff --git a/chirp/__pycache__/bandplan_na.cpython-38.pyc b/chirp/__pycache__/bandplan_na.cpython-38.pyc new file mode 100644 index 0000000..ece445e Binary files /dev/null and b/chirp/__pycache__/bandplan_na.cpython-38.pyc differ diff --git a/chirp/__pycache__/bitwise.cpython-37.pyc b/chirp/__pycache__/bitwise.cpython-37.pyc new file mode 100644 index 0000000..1c27fed Binary files /dev/null and b/chirp/__pycache__/bitwise.cpython-37.pyc differ diff --git a/chirp/__pycache__/bitwise.cpython-38.pyc b/chirp/__pycache__/bitwise.cpython-38.pyc new file mode 100644 index 0000000..72b3eee Binary files /dev/null and b/chirp/__pycache__/bitwise.cpython-38.pyc differ diff --git a/chirp/__pycache__/bitwise_grammar.cpython-37.pyc b/chirp/__pycache__/bitwise_grammar.cpython-37.pyc new file mode 100644 index 0000000..eed5163 Binary files /dev/null and b/chirp/__pycache__/bitwise_grammar.cpython-37.pyc differ diff --git a/chirp/__pycache__/bitwise_grammar.cpython-38.pyc b/chirp/__pycache__/bitwise_grammar.cpython-38.pyc new file mode 100644 index 0000000..69a92b6 Binary files /dev/null and b/chirp/__pycache__/bitwise_grammar.cpython-38.pyc differ diff --git a/chirp/__pycache__/chirp_common.cpython-37.pyc b/chirp/__pycache__/chirp_common.cpython-37.pyc new file mode 100644 index 0000000..027b77a Binary files /dev/null and b/chirp/__pycache__/chirp_common.cpython-37.pyc differ diff --git a/chirp/__pycache__/chirp_common.cpython-38.pyc b/chirp/__pycache__/chirp_common.cpython-38.pyc new file mode 100644 index 0000000..7454eb8 Binary files /dev/null and b/chirp/__pycache__/chirp_common.cpython-38.pyc differ diff --git a/chirp/__pycache__/detect.cpython-37.pyc b/chirp/__pycache__/detect.cpython-37.pyc new file mode 100644 index 0000000..06ab670 Binary files /dev/null and b/chirp/__pycache__/detect.cpython-37.pyc differ diff --git a/chirp/__pycache__/detect.cpython-38.pyc b/chirp/__pycache__/detect.cpython-38.pyc new file mode 100644 index 0000000..a139940 Binary files /dev/null and b/chirp/__pycache__/detect.cpython-38.pyc differ diff --git a/chirp/__pycache__/directory.cpython-37.pyc b/chirp/__pycache__/directory.cpython-37.pyc new file mode 100644 index 0000000..7c1a849 Binary files /dev/null and b/chirp/__pycache__/directory.cpython-37.pyc differ diff --git a/chirp/__pycache__/directory.cpython-38.pyc b/chirp/__pycache__/directory.cpython-38.pyc new file mode 100644 index 0000000..4f6f2e3 Binary files /dev/null and b/chirp/__pycache__/directory.cpython-38.pyc differ diff --git a/chirp/__pycache__/elib_intl.cpython-37.pyc b/chirp/__pycache__/elib_intl.cpython-37.pyc new file mode 100644 index 0000000..14a2cd4 Binary files /dev/null and b/chirp/__pycache__/elib_intl.cpython-37.pyc differ diff --git a/chirp/__pycache__/elib_intl.cpython-38.pyc b/chirp/__pycache__/elib_intl.cpython-38.pyc new file mode 100644 index 0000000..f36bd18 Binary files /dev/null and b/chirp/__pycache__/elib_intl.cpython-38.pyc differ diff --git a/chirp/__pycache__/errors.cpython-37.pyc b/chirp/__pycache__/errors.cpython-37.pyc new file mode 100644 index 0000000..a3fb3cd Binary files /dev/null and b/chirp/__pycache__/errors.cpython-37.pyc differ diff --git a/chirp/__pycache__/errors.cpython-38.pyc b/chirp/__pycache__/errors.cpython-38.pyc new file mode 100644 index 0000000..39a32d8 Binary files /dev/null and b/chirp/__pycache__/errors.cpython-38.pyc differ diff --git a/chirp/__pycache__/import_logic.cpython-37.pyc b/chirp/__pycache__/import_logic.cpython-37.pyc new file mode 100644 index 0000000..57baad6 Binary files /dev/null and b/chirp/__pycache__/import_logic.cpython-37.pyc differ diff --git a/chirp/__pycache__/import_logic.cpython-38.pyc b/chirp/__pycache__/import_logic.cpython-38.pyc new file mode 100644 index 0000000..d20c296 Binary files /dev/null and b/chirp/__pycache__/import_logic.cpython-38.pyc differ diff --git a/chirp/__pycache__/logger.cpython-37.pyc b/chirp/__pycache__/logger.cpython-37.pyc new file mode 100644 index 0000000..6fd4439 Binary files /dev/null and b/chirp/__pycache__/logger.cpython-37.pyc differ diff --git a/chirp/__pycache__/logger.cpython-38.pyc b/chirp/__pycache__/logger.cpython-38.pyc new file mode 100644 index 0000000..a21b493 Binary files /dev/null and b/chirp/__pycache__/logger.cpython-38.pyc differ diff --git a/chirp/__pycache__/memmap.cpython-37.pyc b/chirp/__pycache__/memmap.cpython-37.pyc new file mode 100644 index 0000000..ac0bda3 Binary files /dev/null and b/chirp/__pycache__/memmap.cpython-37.pyc differ diff --git a/chirp/__pycache__/memmap.cpython-38.pyc b/chirp/__pycache__/memmap.cpython-38.pyc new file mode 100644 index 0000000..ec6ff2f Binary files /dev/null and b/chirp/__pycache__/memmap.cpython-38.pyc differ diff --git a/chirp/__pycache__/platform.cpython-37.pyc b/chirp/__pycache__/platform.cpython-37.pyc new file mode 100644 index 0000000..1dd9d68 Binary files /dev/null and b/chirp/__pycache__/platform.cpython-37.pyc differ diff --git a/chirp/__pycache__/platform.cpython-38.pyc b/chirp/__pycache__/platform.cpython-38.pyc new file mode 100644 index 0000000..f23a02e Binary files /dev/null and b/chirp/__pycache__/platform.cpython-38.pyc differ diff --git a/chirp/__pycache__/pyPEG.cpython-37.pyc b/chirp/__pycache__/pyPEG.cpython-37.pyc new file mode 100644 index 0000000..604c66b Binary files /dev/null and b/chirp/__pycache__/pyPEG.cpython-37.pyc differ diff --git a/chirp/__pycache__/pyPEG.cpython-38.pyc b/chirp/__pycache__/pyPEG.cpython-38.pyc new file mode 100644 index 0000000..8762852 Binary files /dev/null and b/chirp/__pycache__/pyPEG.cpython-38.pyc differ diff --git a/chirp/__pycache__/radioreference.cpython-37.pyc b/chirp/__pycache__/radioreference.cpython-37.pyc new file mode 100644 index 0000000..a51f627 Binary files /dev/null and b/chirp/__pycache__/radioreference.cpython-37.pyc differ diff --git a/chirp/__pycache__/radioreference.cpython-38.pyc b/chirp/__pycache__/radioreference.cpython-38.pyc new file mode 100644 index 0000000..1a37703 Binary files /dev/null and b/chirp/__pycache__/radioreference.cpython-38.pyc differ diff --git a/chirp/__pycache__/settings.cpython-37.pyc b/chirp/__pycache__/settings.cpython-37.pyc new file mode 100644 index 0000000..8632c31 Binary files /dev/null and b/chirp/__pycache__/settings.cpython-37.pyc differ diff --git a/chirp/__pycache__/settings.cpython-38.pyc b/chirp/__pycache__/settings.cpython-38.pyc new file mode 100644 index 0000000..22f5418 Binary files /dev/null and b/chirp/__pycache__/settings.cpython-38.pyc differ diff --git a/chirp/__pycache__/util.cpython-37.pyc b/chirp/__pycache__/util.cpython-37.pyc new file mode 100644 index 0000000..df08f15 Binary files /dev/null and b/chirp/__pycache__/util.cpython-37.pyc differ diff --git a/chirp/__pycache__/util.cpython-38.pyc b/chirp/__pycache__/util.cpython-38.pyc new file mode 100644 index 0000000..af10711 Binary files /dev/null and b/chirp/__pycache__/util.cpython-38.pyc differ diff --git a/chirp/bandplan.py b/chirp/bandplan.py new file mode 100644 index 0000000..19d70a3 --- /dev/null +++ b/chirp/bandplan.py @@ -0,0 +1,85 @@ +# Copyright 2013 Sean Burford +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common + + +class Band(object): + def __init__(self, limits, name, mode=None, step_khz=None, + input_offset=None, output_offset=None, tones=None): + # Apply semantic and chirp limitations to settings. + # memedit applies radio limitations when settings are applied. + try: + assert limits[0] <= limits[1], "Lower freq > upper freq" + if mode is not None: + assert mode in chirp_common.MODES, "Mode %s not one of %s" % ( + mode, chirp_common.MODES) + if step_khz is not None: + assert step_khz in chirp_common.TUNING_STEPS, ( + "step_khz %s not one of %s" % + (step_khz, chirp_common.TUNING_STEPS)) + if tones: + for tone in tones: + assert tone in chirp_common.TONES, ( + "tone %s not one of %s" % (tone, chirp_common.TONES)) + except AssertionError as e: + raise ValueError("%s %s: %s" % (name, limits, e)) + + self.name = name + self.mode = mode + self.step_khz = step_khz + self.tones = tones + self.limits = limits + self.offset = None + self.duplex = "simplex" + if input_offset is not None: + self.offset = input_offset + self.duplex = "rpt TX" + elif output_offset is not None: + self.offset = output_offset + self.duplex = "rpt RX" + + def __eq__(self, other): + return (other.limits[0] == self.limits[0] and + other.limits[1] == self.limits[1]) + + def contains(self, other): + return (other.limits[0] >= self.limits[0] and + other.limits[1] <= self.limits[1]) + + def width(self): + return self.limits[1] - self.limits[0] + + def inverse(self): + """Create an RX/TX shadow of this band using the offset.""" + if not self.offset: + return self + limits = (self.limits[0] + self.offset, self.limits[1] + self.offset) + offset = -1 * self.offset + if self.duplex == "rpt RX": + return Band(limits, self.name, self.mode, self.step_khz, + input_offset=offset, tones=self.tones) + return Band(limits, self.name, self.mode, self.step_khz, + output_offset=offset, tones=self.tones) + + def __repr__(self): + desc = '%s%s%s%s' % ( + self.mode and 'mode: %s ' % (self.mode,) or '', + self.step_khz and 'step_khz: %s ' % (self.step_khz,) or '', + self.offset and 'offset: %s ' % (self.offset,) or '', + self.tones and 'tones: %s ' % (self.tones,) or '') + + return "%s-%s %s %s %s" % ( + self.limits[0], self.limits[1], self.name, self.duplex, desc) diff --git a/chirp/bandplan_au.py b/chirp/bandplan_au.py new file mode 100644 index 0000000..9f4ba50 --- /dev/null +++ b/chirp/bandplan_au.py @@ -0,0 +1,115 @@ +# Copyright 2013 Sean Burford +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from chirp import bandplan, bandplan_iaru_r3 + + +SHORTNAME = "australia" + +DESC = { + "name": "Australian Amateur Band Plan", + "updated": "April 2010", + "url": "http://www.wia.org.au/members/bandplans/data" + "/documents/Australian%20Band%20Plans%20100404.pdf", +} + +BANDS_10M = ( + # bandplan.Band((28000000, 29700000), "10 Meter Band"), + bandplan.Band((29520000, 29680000), "FM Simplex and Repeaters", + mode="FM", step_khz=20), + bandplan.Band((29620000, 29680000), "FM Repeaters", input_offset=-100000), +) + +BANDS_6M = ( + # bandplan.Band((50000000, 54000000), "6 Meter Band"), + bandplan.Band((52525000, 53975000), "FM Simplex and Repeaters", + mode="FM", step_khz=25), + bandplan.Band((53550000, 53975000), "FM Repeaters", input_offset=-1000000), +) + +BANDS_2M = ( + bandplan.Band((144000000, 148000000), "2 Meter Band", + tones=(91.5, 123.0, 141.3, 146.2, 85.4)), + bandplan.Band((144400000, 144600000), "Beacons", step_khz=1), + bandplan.Band((146025000, 147975000), "FM Simplex and Repeaters", + mode="FM", step_khz=25), + bandplan.Band((146625000, 147000000), "FM Repeaters Group A", + input_offset=-600000), + bandplan.Band((147025000, 147375000), "FM Repeaters Group B", + input_offset=600000), +) + +BANDS_70CM = ( + bandplan.Band((420000000, 450000000), "70cm Band", + tones=(91.5, 123.0, 141.3, 146.2, 85.4)), + bandplan.Band((432400000, 432600000), "Beacons", step_khz=1), + bandplan.Band((438025000, 439975000), "FM Simplex and Repeaters", + mode="FM", step_khz=25), + bandplan.Band((438025000, 438725000), "FM Repeaters Group A", + input_offset=-5000000), + bandplan.Band((439275000, 439975000), "FM Repeaters Group B", + input_offset=-5000000), +) + +BANDS_23CM = ( + # bandplan.Band((1240000000, 1300000000), "23cm Band"), + bandplan.Band((1273025000, 1273975000), "FM Repeaters", + mode="FM", step_khz=25, input_offset=20000000), + bandplan.Band((1296400000, 1296600000), "Beacons", step_khz=1), + bandplan.Band((1297025000, 1300400000), "General FM Simplex Data", + mode="FM", step_khz=25), +) + +BANDS_13CM = ( + bandplan.Band((2300000000, 2450000000), "13cm Band"), + bandplan.Band((2403400000, 2403600000), "Beacons", step_khz=1), + bandplan.Band((2425000000, 2428000000), "FM Simplex", + mode="FM", step_khz=25), + bandplan.Band((2428025000, 2429000000), "FM Duplex (Voice)", + mode="FM", step_khz=25, input_offset=20000000), + bandplan.Band((2429000000, 2429975000), "FM Duplex (Data)", + mode="FM", step_khz=100, input_offset=20000000), +) + +BANDS_9CM = ( + bandplan.Band((3300000000, 3600000000), "9cm Band"), + bandplan.Band((3320000000, 3340000000), "WB Channel 2: Voice/Data", + step_khz=100), + bandplan.Band((3400400000, 3400600000), "Beacons", step_khz=1), + bandplan.Band((3402000000, 3403000000), "FM Simplex (Voice)", + mode="FM", step_khz=100), + bandplan.Band((3403000000, 3405000000), "FM Simplex (Data)", + mode="FM", step_khz=100), +) + +BANDS_6CM = ( + bandplan.Band((5650000000, 5850000000), "6cm Band"), + bandplan.Band((5760400000, 5760600000), "Beacons", step_khz=1), + bandplan.Band((5700000000, 5720000000), "WB Channel 2: Data", + step_khz=100, input_offset=70000000), + bandplan.Band((5720000000, 5740000000), "WB Channel 3: Voice", + step_khz=100, input_offset=70000000), + bandplan.Band((5762000000, 5763000000), "FM Simplex (Voice)", + mode="FM", step_khz=100), + bandplan.Band((5763000000, 5765000000), "FM Simplex (Data)", + mode="FM", step_khz=100), +) + +BANDS = bandplan_iaru_r3.BANDS_20M + bandplan_iaru_r3.BANDS_17M +BANDS += bandplan_iaru_r3.BANDS_15M + bandplan_iaru_r3.BANDS_12M +BANDS += bandplan_iaru_r3.BANDS_10M + bandplan_iaru_r3.BANDS_6M +BANDS += BANDS_10M + BANDS_6M + BANDS_2M + BANDS_70CM + BANDS_23CM +BANDS += BANDS_13CM + BANDS_9CM + BANDS_6CM diff --git a/chirp/bandplan_iaru_r1.py b/chirp/bandplan_iaru_r1.py new file mode 100644 index 0000000..e41b0ee --- /dev/null +++ b/chirp/bandplan_iaru_r1.py @@ -0,0 +1,147 @@ +# Copyright 2013 Sean Burford +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import bandplan + +SHORTNAME = "iaru_r1" + +DESC = { + "name": "IARU Region 1 (Europe, Africa, Middle East and Northern Asia)", + "url": "http://iaru-r1.org/index.php?option=com_content" + "&view=article&id=175&Itemid=127", + "updated": "General Conference Sun City 2011", +} + +# Bands are broken up like this so that other plans can import bits. + +BANDS_2100M = ( + bandplan.Band((135700, 137800), "137khz Band", mode="CW"), +) + +BANDS_160M = ( + bandplan.Band((1810000, 2000000), "160 Meter Band"), + bandplan.Band((1810000, 1838000), "CW", mode="CW"), + bandplan.Band((1838000, 1840000), "All narrow band modes"), + bandplan.Band((1840000, 1843000), "All modes, digimodes", mode="RTTY"), +) + +BANDS_80M = ( + bandplan.Band((3500000, 3800000), "80 Meter Band"), + bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"), + bandplan.Band((3510000, 3560000), "CW, contest preferred", mode="CW"), + bandplan.Band((3560000, 3580000), "CW", mode="CW"), + bandplan.Band((3580000, 3600000), "All narrow band modes, digimodes"), + bandplan.Band((3590000, 3600000), "All narrow band, digimodes, unattended"), + bandplan.Band((3600000, 3650000), "All modes, SSB contest preferred", + mode="LSB"), + bandplan.Band((3600000, 3700000), "All modes, SSB QRP", mode="LSB"), + bandplan.Band((3700000, 3800000), "All modes, SSB contest preferred", + mode="LSB"), + bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"), +) + +BANDS_40M = ( + bandplan.Band((7000000, 7200000), "40 Meter Band"), + bandplan.Band((7000000, 7040000), "CW", mode="CW"), + bandplan.Band((7040000, 7047000), "All narrow band modes, digimodes"), + bandplan.Band((7047000, 7050000), "All narrow band, digimodes, unattended"), + bandplan.Band((7050000, 7053000), "All modes, digimodes, unattended"), + bandplan.Band((7053000, 7060000), "All modes, digimodes"), + bandplan.Band((7060000, 7100000), "All modes, SSB contest preferred", + mode="LSB"), + bandplan.Band((7100000, 7130000), + "All modes, R1 Emergency Center Of Activity", mode="LSB"), + bandplan.Band((7130000, 7200000), "All modes, SSB contest preferred", + mode="LSB"), + bandplan.Band((7175000, 7200000), "All modes, SSB DX preferred", mode="LSB"), +) + +BANDS_30M = ( + bandplan.Band((10100000, 10150000), "30 Meter Band"), + bandplan.Band((10100000, 10140000), "CW", mode="CW"), + bandplan.Band((10140000, 10150000), "All narrow band digimodes"), +) + +BANDS_20M = ( + bandplan.Band((14000000, 14350000), "20 Meter Band"), + bandplan.Band((14000000, 14070000), "CW", mode="CW"), + bandplan.Band((14070000, 14099000), "All narrow band modes, digimodes"), + bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((14101000, 14112000), "All narrow band modes, digimodes"), + bandplan.Band((14125000, 14350000), "All modes, SSB contest preferred", + mode="USB"), + bandplan.Band((14300000, 14350000), + "All modes, Global Emergency center of activity", mode="USB"), +) + +BANDS_17M = ( + bandplan.Band((18068000, 18168000), "17 Meter Band"), + bandplan.Band((18068000, 18095000), "CW", mode="CW"), + bandplan.Band((18095000, 18109000), "All narrow band modes, digimodes"), + bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((18111000, 18168000), "All modes, digimodes"), +) + +BANDS_15M = ( + bandplan.Band((21000000, 21450000), "15 Meter Band"), + bandplan.Band((21000000, 21070000), "CW", mode="CW"), + bandplan.Band((21070000, 21090000), "All narrow band modes, digimodes"), + bandplan.Band((21090000, 21110000), + "All narrow band, digimodes, unattended"), + bandplan.Band((21110000, 21120000), "All modes, digimodes, unattended"), + bandplan.Band((21120000, 21149000), "All narrow band modes"), + bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((21151000, 21450000), "All modes", mode="USB"), +) + +BANDS_12M = ( + bandplan.Band((24890000, 24990000), "12 Meter Band"), + bandplan.Band((24890000, 24915000), "CW", mode="CW"), + bandplan.Band((24915000, 24929000), "All narrow band modes, digimodes"), + bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((24931000, 24990000), "All modes, digimodes", mode="USB"), +) + +BANDS_10M = ( + bandplan.Band((28000000, 29700000), "10 Meter Band"), + bandplan.Band((28000000, 28070000), "CW", mode="CW"), + bandplan.Band((28070000, 28120000), "All narrow band modes, digimodes"), + bandplan.Band((28120000, 28150000), + "All narrow band, digimodes, unattended"), + bandplan.Band((28150000, 28190000), "All narrow band modes"), + bandplan.Band((28190000, 28199000), "Beacons", mode="CW"), + bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((28201000, 28300000), "Beacons", mode="CW"), + bandplan.Band((28300000, 28320000), "All modes, digimodes, unattended"), + bandplan.Band((28320000, 29100000), "All modes"), + bandplan.Band((29100000, 29200000), "FM simplex", mode="NFM", step_khz=10), + bandplan.Band((29200000, 29300000), "All modes, digimodes, unattended"), + bandplan.Band((29300000, 29510000), "Satellite downlink"), + bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"), + bandplan.Band((29520000, 29590000), "All modes, FM repeater inputs", + step_khz=10, mode="NFM"), + bandplan.Band((29600000, 29610000), "FM simplex", step_khz=10, mode="NFM"), + bandplan.Band((29620000, 29700000), "All modes, FM repeater outputs", + step_khz=10, mode="NFM", input_offset=-100000), + bandplan.Band((29520000, 29700000), "Wide band", step_khz=10, mode="NFM"), +) + +BANDS = BANDS_2100M + BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M +BANDS = BANDS + BANDS_20M + BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M diff --git a/chirp/bandplan_iaru_r2.py b/chirp/bandplan_iaru_r2.py new file mode 100644 index 0000000..5886187 --- /dev/null +++ b/chirp/bandplan_iaru_r2.py @@ -0,0 +1,145 @@ +# Copyright 2013 Sean Burford +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import bandplan + +SHORTNAME = "iaru_r2" + +DESC = { + "name": "IARU Region 2 (The Americas)", + "updated": "October 8, 2010", + "url": "http://www.iaru.org/uploads/1/3/0/7/13073366/r2_band_plan.pdf" +} + +# Bands are broken up like this so that other plans can import bits. + +BANDS_160M = ( + bandplan.Band((1800000, 2000000), "160 Meter Band"), + bandplan.Band((1800000, 1810000), "Digimodes"), + bandplan.Band((1810000, 1830000), "CW", mode="CW"), + bandplan.Band((1830000, 1840000), "CW, priority for DX", mode="CW"), + bandplan.Band((1840000, 1850000), "SSB, priority for DX", mode="LSB"), + bandplan.Band((1850000, 1999000), "All modes", mode="LSB"), + bandplan.Band((1999000, 2000000), "Beacons", mode="CW"), +) + +BANDS_80M = ( + bandplan.Band((3500000, 4000000), "80 Meter Band"), + bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"), + bandplan.Band((3510000, 3560000), "CW, contest preferred", mode="CW"), + bandplan.Band((3560000, 3580000), "CW", mode="CW"), + bandplan.Band((3580000, 3590000), "All narrow band modes, digimodes"), + bandplan.Band((3590000, 3600000), "All modes"), + bandplan.Band((3600000, 3650000), "All modes, SSB contest preferred", + mode="LSB"), + bandplan.Band((3650000, 3700000), "All modes", mode="LSB"), + bandplan.Band((3700000, 3775000), "All modes, SSB contest preferred", + mode="LSB"), + bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"), + bandplan.Band((3800000, 4000000), "All modes"), +) + +BANDS_40M = ( + bandplan.Band((7000000, 7300000), "40 Meter Band"), + bandplan.Band((7000000, 7025000), "CW, priority for DX", mode="CW"), + bandplan.Band((7025000, 7035000), "CW", mode="CW"), + bandplan.Band((7035000, 7038000), "All narrow band modes, digimodes"), + bandplan.Band((7038000, 7040000), "All narrow band modes, digimodes"), + bandplan.Band((7040000, 7043000), "All modes, digimodes"), + bandplan.Band((7043000, 7300000), "All modes"), +) + +BANDS_30M = ( + bandplan.Band((10100000, 10150000), "30 Meter Band"), + bandplan.Band((10100000, 10130000), "CW", mode="CW"), + bandplan.Band((10130000, 10140000), "All narrow band digimodes"), + bandplan.Band((10140000, 10150000), "All modes, digimodes, no phone"), +) + +BANDS_20M = ( + bandplan.Band((14000000, 14350000), "20 Meter Band"), + bandplan.Band((14000000, 14025000), "CW, priority for DX", mode="CW"), + bandplan.Band((14025000, 14060000), "CW, contest preferred", mode="CW"), + bandplan.Band((14060000, 14070000), "CW", mode="CW"), + bandplan.Band((14070000, 14089000), "All narrow band modes, digimodes"), + bandplan.Band((14089000, 14099000), "All modes, digimodes"), + bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((14101000, 14112000), "All modes, digimodes"), + bandplan.Band((14112000, 14285000), "All modes, SSB contest preferred", + mode="USB"), + bandplan.Band((14285000, 14300000), "All modes", mode="AM"), + bandplan.Band((14300000, 14350000), "All modes"), +) + +BANDS_17M = ( + bandplan.Band((18068000, 18168000), "17 Meter Band"), + bandplan.Band((18068000, 18095000), "CW", mode="CW"), + bandplan.Band((18095000, 18105000), "All narrow band modes, digimodes"), + bandplan.Band((18105000, 18109000), "All narrow band modes, digimodes"), + bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((18111000, 18120000), "All modes, digimodes"), + bandplan.Band((18120000, 18168000), "All modes"), +) + +BANDS_15M = ( + bandplan.Band((21000000, 21450000), "15 Meter Band"), + bandplan.Band((21000000, 21070000), "CW", mode="CW"), + bandplan.Band((21070000, 21090000), "All narrow band modes, digimodes"), + bandplan.Band((21090000, 21110000), "All narrow band modes, digimodes"), + bandplan.Band((21110000, 21120000), "All modes (exc SSB), digimodes"), + bandplan.Band((21120000, 21149000), "All narrow band modes"), + bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((21151000, 21450000), "All modes", mode="USB"), +) + +BANDS_12M = ( + bandplan.Band((24890000, 24990000), "12 Meter Band"), + bandplan.Band((24890000, 24915000), "CW", mode="CW"), + bandplan.Band((24915000, 24925000), "All narrow band modes, digimodes"), + bandplan.Band((24925000, 24929000), "All narrow band modes, digimodes"), + bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((24931000, 24940000), "All modes, digimodes"), + bandplan.Band((24940000, 24990000), "All modes", mode="USB"), +) + +BANDS_10M = ( + bandplan.Band((28000000, 29520000), "10 Meter Band"), + bandplan.Band((28000000, 28070000), "CW", mode="CW"), + bandplan.Band((28070000, 28120000), "All narrow band modes, digimodes"), + bandplan.Band((28120000, 28150000), "All narrow band modes, digimodes"), + bandplan.Band((28150000, 28190000), "All narrow band modes, digimodes"), + bandplan.Band((28190000, 28199000), "Regional time shared beacons", + mode="CW"), + bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((28201000, 28225000), "Continuous duty beacons", + mode="CW"), + bandplan.Band((28225000, 28300000), "All modes, beacons"), + bandplan.Band((28300000, 28320000), "All modes, digimodes"), + bandplan.Band((28320000, 29000000), "All modes"), + bandplan.Band((29000000, 29200000), "All modes, AM preferred", mode="AM"), + bandplan.Band((29200000, 29300000), "All modes including FM, digimodes"), + bandplan.Band((29300000, 29510000), "Satellite downlink"), + bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"), + bandplan.Band((29520000, 29700000), "FM", step_khz=10, mode="NFM"), + bandplan.Band((29620000, 29690000), "FM Repeaters", input_offset=-100000), +) + +BANDS = BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + BANDS_20M +BANDS = BANDS + BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M diff --git a/chirp/bandplan_iaru_r3.py b/chirp/bandplan_iaru_r3.py new file mode 100644 index 0000000..08bb56e --- /dev/null +++ b/chirp/bandplan_iaru_r3.py @@ -0,0 +1,139 @@ +# Copyright 2013 Sean Burford +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import bandplan + +SHORTNAME = "iaru_r3" + +DESC = { + "name": "IARU Region 3 (Asia Pacific)", + "updated": "16 October 2009", + "url": "http://www.iaru.org/uploads/1/3/0/7/13073366/r3_band_plan.pdf" +} + +# Bands are broken up like this so that other plans can import bits. + +BANDS_2100M = ( + bandplan.Band((135700, 137800), "137khz Band", mode="CW"), +) + +BANDS_160M = ( + bandplan.Band((1800000, 2000000), "160 Meter Band"), + bandplan.Band((1830000, 1840000), "Digimodes", mode="RTTY"), + bandplan.Band((1840000, 2000000), "Phone"), +) + +BANDS_80M = ( + bandplan.Band((3500000, 3900000), "80 Meter Band"), + bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"), + bandplan.Band((3535000, 3900000), "Phone"), + bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"), +) + +BANDS_40M = ( + bandplan.Band((7000000, 7300000), "40 Meter Band"), + bandplan.Band((7000000, 7025000), "CW, priority for DX", mode="CW"), + bandplan.Band((7025000, 7035000), "All narrow band modes, cw", mode="CW"), + bandplan.Band((7035000, 7040000), "All narrow band modes, phone"), + bandplan.Band((7040000, 7300000), "All modes, digimodes"), +) + +BANDS_30M = ( + bandplan.Band((10100000, 10150000), "30 Meter Band"), + bandplan.Band((10100000, 10130000), "CW", mode="CW"), + bandplan.Band((10130000, 10150000), "All narrow band digimodes"), +) + +BANDS_20M = ( + bandplan.Band((14000000, 14350000), "20 Meter Band"), + bandplan.Band((14000000, 14070000), "CW", mode="CW"), + bandplan.Band((14070000, 14099000), "All narrow band modes, digimodes"), + bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((14101000, 14112000), "All narrow band modes, digimodes"), + bandplan.Band((14101000, 14350000), "All modes, digimodes"), +) + +BANDS_17M = ( + bandplan.Band((18068000, 18168000), "17 Meter Band"), + bandplan.Band((18068000, 18100000), "CW", mode="CW"), + bandplan.Band((18100000, 18109000), "All narrow band modes, digimodes"), + bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((18111000, 18168000), "All modes, digimodes"), +) + +BANDS_15M = ( + bandplan.Band((21000000, 21450000), "15 Meter Band"), + bandplan.Band((21000000, 21070000), "CW", mode="CW"), + bandplan.Band((21070000, 21125000), "All narrow band modes, digimodes"), + bandplan.Band((21125000, 21149000), "All narrow band modes, digimodes"), + bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((21151000, 21450000), "All modes", mode="USB"), +) + +BANDS_12M = ( + bandplan.Band((24890000, 24990000), "12 Meter Band"), + bandplan.Band((24890000, 24920000), "CW", mode="CW"), + bandplan.Band((24920000, 24929000), "All narrow band modes, digimodes"), + bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((24931000, 24990000), "All modes, digimodes", mode="USB"), +) + +BANDS_10M = ( + bandplan.Band((28000000, 29700000), "10 Meter Band"), + bandplan.Band((28000000, 28050000), "CW", mode="CW"), + bandplan.Band((28050000, 28150000), "All narrow band modes, digimodes"), + bandplan.Band((28150000, 28190000), "All narrow band modes, digimodes"), + bandplan.Band((28190000, 28199000), "Beacons", mode="CW"), + bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons", + mode="CW"), + bandplan.Band((28201000, 28300000), "Beacons", mode="CW"), + bandplan.Band((28300000, 29300000), "Phone"), + bandplan.Band((29300000, 29510000), "Satellite downlink"), + bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"), + bandplan.Band((29520000, 29700000), "Wide band", step_khz=10, mode="NFM"), +) + +BANDS_6M = ( + bandplan.Band((50000000, 54000000), "6 Meter Band"), + bandplan.Band((50000000, 50100000), "Beacons", mode="CW"), + bandplan.Band((50100000, 50500000), "Phone and narrow band"), + bandplan.Band((50500000, 54000000), "Wide band"), +) + +BANDS_2M = ( + bandplan.Band((144000000, 148000000), "2 Meter Band"), + bandplan.Band((144000000, 144035000), "Earth Moon Earth"), + bandplan.Band((145800000, 146000000), "Satellite"), +) + +BANDS_70CM = ( + bandplan.Band((430000000, 450000000), "70cm Band"), + bandplan.Band((431900000, 432240000), "Earth Moon Earth"), + bandplan.Band((435000000, 438000000), "Satellite"), +) + +BANDS_23CM = ( + bandplan.Band((1240000000, 1300000000), "23cm Band"), + bandplan.Band((1260000000, 1270000000), "Satellite"), + bandplan.Band((1296000000, 1297000000), "Earth Moon Earth"), +) + +BANDS = BANDS_2100M + BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M +BANDS = BANDS + BANDS_20M + BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M +BANDS = BANDS + BANDS_6M + BANDS_2M + BANDS_70CM + BANDS_23CM diff --git a/chirp/bandplan_na.py b/chirp/bandplan_na.py new file mode 100644 index 0000000..0ddc781 --- /dev/null +++ b/chirp/bandplan_na.py @@ -0,0 +1,270 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import bandplan, bandplan_iaru_r2 + + +SHORTNAME = "north_america" + +DESC = { + "name": "North American Band Plan", + "url": "http://www.arrl.org/band-plan" +} + +BANDS_160M = ( + bandplan.Band((1800000, 2000000), "160 Meter Band", mode="CW"), + bandplan.Band((1800000, 1810000), "Digital Modes"), + bandplan.Band((1843000, 2000000), "SSB, SSTV and other wideband modes"), + bandplan.Band((1995000, 2000000), "Experimental"), + bandplan.Band((1999000, 2000000), "Beacons"), +) + +BANDS_80M = ( + bandplan.Band((3500000, 4000000), "80 Meter Band"), + bandplan.Band((3570000, 3600000), "RTTY/Data", mode="RTTY"), + bandplan.Band((3790000, 3800000), "DX window"), +) + +BANDS_40M = ( + bandplan.Band((7000000, 7300000), "40 Meter Band"), + bandplan.Band((7080000, 7125000), "RTTY/Data", mode="RTTY"), +) + +BANDS_30M = ( + bandplan.Band((10100000, 10150000), "30 Meter Band"), + bandplan.Band((10130000, 10140000), "RTTY", mode="RTTY"), + bandplan.Band((10140000, 10150000), "Packet"), +) + +BANDS_20M = ( + bandplan.Band((14000000, 14350000), "20 Meter Band"), + bandplan.Band((14070000, 14095000), "RTTY", mode="RTTY"), + bandplan.Band((14095000, 14099500), "Packet"), + bandplan.Band((14100500, 14112000), "Packet"), +) + +BANDS_17M = ( + bandplan.Band((18068000, 18168000), "17 Meter Band"), + bandplan.Band((18100000, 18105000), "RTTY", mode="RTTY"), + bandplan.Band((18105000, 18110000), "Packet"), +) + +BANDS_15M = ( + bandplan.Band((21000000, 21450000), "15 Meter Band"), + bandplan.Band((21070000, 21110000), "RTTY/Data", mode="RTTY"), +) + +BANDS_12M = ( + bandplan.Band((24890000, 24990000), "12 Meter Band"), + bandplan.Band((24920000, 24925000), "RTTY", mode="RTTY"), + bandplan.Band((24925000, 24930000), "Packet"), +) + +BANDS_10M = ( + bandplan.Band((28000000, 29700000), "10 Meter Band"), + bandplan.Band((28000000, 28070000), "CW", mode="CW"), + bandplan.Band((28070000, 28150000), "RTTY", mode="RTTY"), + bandplan.Band((28150000, 28190000), "CW", mode="CW"), + bandplan.Band((28201000, 28300000), "Beacons", mode="CW"), + bandplan.Band((28300000, 29300000), "Phone"), + bandplan.Band((29000000, 29200000), "AM", mode="AM"), + bandplan.Band((29300000, 29510000), "Satellite Downlinks"), + bandplan.Band((29520000, 29590000), "Repeater Inputs", + step_khz=10, mode="FM"), + bandplan.Band((29610000, 29700000), "Repeater Outputs", + step_khz=10, mode="FM", input_offset=-890000), +) + +BANDS_6M = ( + bandplan.Band((50000000, 54000000), "6 Meter Band"), + bandplan.Band((50000000, 50100000), "CW, beacons", mode="CW"), + bandplan.Band((50060000, 50080000), "beacon subband"), + bandplan.Band((50100000, 50300000), "SSB, CW", mode="USB"), + bandplan.Band((50100000, 50125000), "DX window", mode="USB"), + bandplan.Band((50300000, 50600000), "All modes"), + bandplan.Band((50600000, 50800000), "Nonvoice communications"), + bandplan.Band((50800000, 51000000), "Radio remote control", step_khz=20), + bandplan.Band((51000000, 51100000), "Pacific DX window"), + bandplan.Band((51120000, 51180000), "Digital repeater inputs", step_khz=10), + bandplan.Band((51500000, 51600000), "Simplex"), + bandplan.Band((51620000, 51980000), "Repeater outputs A", + input_offset=-500000), + bandplan.Band((51620000, 51680000), "Digital repeater outputs", + input_offset=-500000), + bandplan.Band((52020000, 52040000), "FM simplex", mode="FM"), + bandplan.Band((52500000, 52980000), "Repeater outputs B", + input_offset=-500000, step_khz=20, mode="FM"), + bandplan.Band((53000000, 53100000), "FM simplex", mode="FM"), + bandplan.Band((53100000, 53400000), "Radio remote control", step_khz=100), + bandplan.Band((53500000, 53980000), "Repeater outputs C", + input_offset=-500000), + bandplan.Band((53500000, 53800000), "Radio remote control", step_khz=100), + bandplan.Band((53520000, 53900000), "Simplex"), +) + +BANDS_2M = ( + bandplan.Band((144000000, 148000000), "2 Meter Band"), + bandplan.Band((144000000, 144050000), "EME (CW)", mode="CW"), + bandplan.Band((144050000, 144100000), "General CW and weak signals", + mode="CW"), + bandplan.Band((144100000, 144200000), "EME and weak-signal SSB", + mode="USB"), + bandplan.Band((144200000, 144275000), "General SSB operation", + mode="USB"), + bandplan.Band((144275000, 144300000), "Propagation beacons", mode="CW"), + bandplan.Band((144300000, 144500000), "OSCAR subband"), + bandplan.Band((144600000, 144900000), "FM repeater inputs", mode="FM"), + bandplan.Band((144900000, 145100000), "Weak signal and FM simplex", + mode="FM", step_khz=10), + bandplan.Band((145100000, 145200000), "Linear translator outputs", + input_offset=-600000), + bandplan.Band((145200000, 145500000), "FM repeater outputs", + input_offset=-600000, mode="FM",), + bandplan.Band((145500000, 145800000), "Misc and experimental modes"), + bandplan.Band((145800000, 146000000), "OSCAR subband"), + bandplan.Band((146400000, 146580000), "Simplex"), + bandplan.Band((146610000, 146970000), "Repeater outputs", + input_offset=-600000), + bandplan.Band((147000000, 147390000), "Repeater outputs", + input_offset=600000), + bandplan.Band((147420000, 147570000), "Simplex"), +) + +BANDS_1_25M = ( + bandplan.Band((222000000, 225000000), "1.25 Meters"), + bandplan.Band((222000000, 222150000), "Weak-signal modes"), + bandplan.Band((222000000, 222025000), "EME"), + bandplan.Band((222050000, 222060000), "Propagation beacons"), + bandplan.Band((222100000, 222150000), "Weak-signal CW & SSB"), + bandplan.Band((222150000, 222250000), "Local coordinator's option"), + bandplan.Band((223400000, 223520000), "FM simplex", mode="FM"), + bandplan.Band((223520000, 223640000), "Digital, packet"), + bandplan.Band((223640000, 223700000), "Links, control"), + bandplan.Band((223710000, 223850000), "Local coordinator's option"), + bandplan.Band((223850000, 224980000), "Repeater outputs only", + mode="FM", input_offset=-1600000), +) + +BANDS_70CM = ( + bandplan.Band((420000000, 450000000), "70cm Band"), + bandplan.Band((420000000, 426000000), "ATV repeater or simplex"), + bandplan.Band((426000000, 432000000), "ATV simplex"), + bandplan.Band((432000000, 432070000), "EME (Earth-Moon-Earth)"), + bandplan.Band((432070000, 432100000), "Weak-signal CW", mode="CW"), + bandplan.Band((432100000, 432300000), "Mixed-mode and weak-signal work"), + bandplan.Band((432300000, 432400000), "Propagation beacons"), + bandplan.Band((432400000, 433000000), "Mixed-mode and weak-signal work"), + bandplan.Band((433000000, 435000000), "Auxiliary/repeater links"), + bandplan.Band((435000000, 438000000), "Satellite only (internationally)"), + bandplan.Band((438000000, 444000000), "ATV repeater input/repeater links", + input_offset=5000000), + bandplan.Band((442000000, 445000000), "Repeater input/output (local option)", + input_offset=5000000), + bandplan.Band((445000000, 447000000), "Shared by aux and control links, " + "repeaters, simplex (local option)"), + bandplan.Band((447000000, 450000000), "Repeater inputs and outputs " + "(local option)", input_offset=-5000000), +) + +BANDS_33CM = ( + bandplan.Band((902000000, 928000000), "33 Centimeter Band"), + bandplan.Band((902075000, 902100000), "CW/SSB, Weak signal"), + bandplan.Band((902100000, 902125000), "CW/SSB, Weak signal"), + bandplan.Band((903000000, 903100000), "CW/SSB, Beacons and weak signal"), + bandplan.Band((903100000, 903400000), "CW/SSB, Weak signal"), + bandplan.Band((903400000, 909000000), "Mixed modes, Mixed operations " + "including control links"), + bandplan.Band((909000000, 915000000), "Analog/digital Broadband multimedia " + "including ATV, DATV and SS"), + bandplan.Band((915000000, 921000000), "Analog/digital Broadband multimedia " + "including ATV, DATV and SS"), + bandplan.Band((921000000, 927000000), "Analog/digital Broadband multimedia " + "including ATV, DATV and SS"), + bandplan.Band((927000000, 927075000), "FM / other including DV or CW/SSB", + input_offset=-25000000, step_khz=12.5), + bandplan.Band((927075000, 927125000), "FM / other including DV. Simplex"), + bandplan.Band((927125000, 928000000), "FM / other including DV", + input_offset=-25000000, step_khz=12.5), +) + +BANDS_23CM = ( + bandplan.Band((1240000000, 1300000000), "23 Centimeter Band"), + bandplan.Band((1240000000, 1246000000), "ATV Channel #1"), + bandplan.Band((1246000000, 1248000000), "Point-to-point links paired " + "with 1258.000-1260.000", mode="FM"), + bandplan.Band((1248000000, 1252000000), "Digital"), + bandplan.Band((1252000000, 1258000000), "ATV Channel #2"), + bandplan.Band((1258000000, 1260000000), + "Point-to-point links paired with 1246.000-1248.000", + mode="FM"), + bandplan.Band((1240000000, 1260000000), "Regional option, FM ATV"), + bandplan.Band((1260000000, 1270000000), "Satellite uplinks, Experimental, " + "Simplex ATV"), + bandplan.Band((1270000000, 1276000000), "FM, digital Repeater inputs " + "(Regional option)", step_khz=25), + bandplan.Band((1276000000, 1282000000), "ATV Channel #3"), + bandplan.Band((1282000000, 1288000000), "FM, digital repeater outputs", + step_khz=25, input_offset=-12000000), + bandplan.Band((1288000000, 1294000000), "Various Broadband Experimental, " + "Simplex ATV"), + bandplan.Band((1290000000, 1294000000), "FM, digital Repeater outputs " + "(Regional option)", step_khz=25, input_offset=-20000000), + bandplan.Band((1294000000, 1295000000), "FM simplex", mode="FM"), + bandplan.Band((1295000000, 1297000000), "Narrow Band Segment"), + bandplan.Band((1295000000, 1295800000), "Narrow Band Image, Experimental"), + bandplan.Band((1295800000, 1296080000), "CW, SSB, digital EME"), + bandplan.Band((1296080000, 1296200000), "CW, SSB Weak Signal"), + bandplan.Band((1296200000, 1296400000), "CW, digital Beacons"), + bandplan.Band((1296400000, 1297000000), "General Narrow Band"), + bandplan.Band((1297000000, 1300000000), "Digital"), +) + +BANDS_13CM = ( + bandplan.Band((2300000000, 2450000000), "13 Centimeter Band"), + bandplan.Band((2300000000, 2303000000), "Analog & Digital 0.05-1.0 MHz, " + "including full duplex; paired with 2390-2393"), + bandplan.Band((2303000000, 2303750000), "Analog & Digital <50kHz; " + "paired with 2393 - 2393.750"), + bandplan.Band((2303750000, 2304000000), "SSB, CW, digital weak-signal"), + bandplan.Band((2304000000, 2304100000), "Weak Signal EME Band, <3kHz"), + bandplan.Band((2304100000, 2304300000), + "SSB, CW, digital weak-signal, <3kHz"), + bandplan.Band((2304300000, 2304400000), "Beacons, <3kHz"), + bandplan.Band((2304400000, 2304750000), "SSB, CW, digital weak-signal and " + "NBFM, <6kHz"), + bandplan.Band((2304750000, 2305000000), "Analog & Digital; paired with " + "2394.750-2395, <50kHz"), + bandplan.Band((2305000000, 2310000000), "Analog & Digital, paired with " + "2395-2400, 0.05 - 1.0 MHz"), + bandplan.Band((2310000000, 2390000000), "NON-AMATEUR"), + bandplan.Band((2390000000, 2393000000), "Analog & Digital, including full " + "duplex; paired with 2300-2303, 0.05 - 1.0 MHz"), + bandplan.Band((2393000000, 2393750000), "Analog & Digital; paired with " + "2303-2303.750, < 50 kHz"), + bandplan.Band((2393750000, 2394750000), "Experimental"), + bandplan.Band((2394750000, 2395000000), "Analog & Digital; paired with " + "2304.750-2305, < 50 kHz"), + bandplan.Band((2395000000, 2400000000), "Analog & Digital, including full " + "duplex; paired with 2305-2310, 0.05-1.0 MHz"), + bandplan.Band((2400000000, 2410000000), "Amateur Satellite Communications, " + "<6kHz"), + bandplan.Band((2410000000, 2450000000), "Broadband Modes, 22MHz max."), +) + +BANDS = bandplan_iaru_r2.BANDS +BANDS += BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + BANDS_20M +BANDS += BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M + BANDS_6M +BANDS += BANDS_2M + BANDS_1_25M + BANDS_70CM + BANDS_33CM + BANDS_23CM +BANDS += BANDS_13CM diff --git a/chirp/bitwise.py b/chirp/bitwise.py new file mode 100644 index 0000000..1559162 --- /dev/null +++ b/chirp/bitwise.py @@ -0,0 +1,1067 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Language: +# +# Example definitions: +# +# bit foo[8]; /* Eight single bit values */ +# u8 foo; /* Unsigned 8-bit value */ +# u16 foo; /* Unsigned 16-bit value */ +# ul16 foo; /* Unsigned 16-bit value (LE) */ +# u24 foo; /* Unsigned 24-bit value */ +# ul24 foo; /* Unsigned 24-bit value (LE) */ +# u32 foo; /* Unsigned 32-bit value */ +# ul32 foo; /* Unsigned 32-bit value (LE) */ +# i8 foo; /* Signed 8-bit value */ +# i16 foo; /* Signed 16-bit value */ +# il16 foo; /* Signed 16-bit value (LE) */ +# i24 foo; /* Signed 24-bit value */ +# il24 foo; /* Signed 24-bit value (LE) */ +# i32 foo; /* Signed 32-bit value */ +# il32 foo; /* Signed 32-bit value (LE) */ +# char foo; /* Character (single-byte */ +# lbcd foo; /* BCD-encoded byte (LE) */ +# bbcd foo; /* BCD-encoded byte (BE) */ +# char foo[8]; /* 8-char array */ +# struct { +# u8 foo; +# u16 bar; +# } baz; /* Structure with u8 and u16 */ +# +# Example directives: +# +# #seekto 0x1AB; /* Set the data offset to 0x1AB */ +# #seek 4; /* Set the data offset += 4 */ +# #printoffset "foobar" /* Echo the live data offset, +# prefixed by string while parsing */ +# +# Usage: +# +# Create a data definition in a string, and pass it and the data +# to parse to the parse() function. The result is a structure with +# dict-like objects for structures, indexed by name, and lists of +# objects for arrays. The actual data elements can be interpreted +# as integers directly (for int types). Strings and BCD arrays +# behave as expected. + +import struct +import os +import logging + +import six +from builtins import bytes + +from chirp import bitwise_grammar +from chirp.memmap import MemoryMap + +LOG = logging.getLogger(__name__) + + +class ParseError(Exception): + """Indicates an error parsing a definition""" + pass + + +def byte_to_int(b): + if six.PY3 or isinstance(b, int): + return b + else: + return ord(b) + + +def int_to_byte(i): + if six.PY3: + return bytes([i]) + else: + return chr(i) + + +def string_straight_encode(string): + """str -> bytes""" + # So, there are a lot of py2-thinking chirp drivers that + # will do something like this: + # + # memobj.name = 'foo\xff' + # + # because they need the string to be stored in radio memory + # with a 0xFF byte terminating the string. If they do that in + # py3 we will get the unicode equivalent, which is a non-ASCII + # character. Conventional unicode wisdom would tell us we need + # to encode() the string to get UTF-8 bytes, but that's not + # what we want here (the radio doesn't support UTF-8 and we need + # specific binary values in memory). Ideally we would have + # written all of chirp with bytes() for these values, but alas. + # We can get the intended string here by doing bytes([ord(char)]). + return bytes(b''.join(int_to_byte(ord(b)) for b in string)) + + +def string_straight_decode(string): + """bytes -> str""" + # Normally, we would want to decode bytes() to str() for py3. + # However...chirp drivers are currently using strings with + # hex escapes for setting binary byte values in memories. + # Technically, this is char, which is a little more like bytes() + # in py3, but until every driver has converted its strings to + # bytes(), this is massively simpler. Since set_value() is + # doing the inverse, we can do this here. If something sets + # a char to '\xFF' it will arrive below as the unicode character, + # and be converted to the integer/bytes value. When it is read out + # here, we will return that same unicode character and the driver + # will detect '\xFF' properly. + # FIXMEPY3: Remove this and the hack below when drivers convert to + # bytestrings. + return ''.join(chr(byte_to_int(b)) for b in string) + + +def format_binary(nbits, value, pad=8): + s = "" + for i in range(0, nbits): + s = "%i%s" % (value & 0x01, s) + value >>= 1 + return "%s%s" % ((pad - len(s)) * ".", s) + + +def bits_between(start, end): + bits = (1 << (int(end) - int(start))) - 1 + return bits << int(start) + + +def pp(structure, level=0): + for i in structure: + if isinstance(i, list): + pp(i, level+2) + elif isinstance(i, tuple): + if isinstance(i[1], str): + print("%s%s: %s" % (" " * level, i[0], i[1])) + else: + print("%s%s:" % (" " * level, i[0])) + pp(i, level+2) + elif isinstance(i, str): + print("%s%s" % (" " * level, i)) + + +def array_copy(dst, src): + """Copy an array src into DataElement array dst""" + if len(dst) != len(src): + raise Exception("Arrays differ in size") + + for i in range(0, len(dst)): + dst[i].set_value(src[i]) + + +def bcd_to_int(bcd_array): + """Convert an array of bcdDataElement like \x12\x34 + into an int like 1234""" + value = 0 + for bcd in bcd_array: + a, b = bcd.get_value() + value = (value * 100) + (a * 10) + b + return value + + +def int_to_bcd(bcd_array, value): + """Convert an int like 1234 into bcdDataElements like "\x12\x34" """ + for i in reversed(list(range(0, len(bcd_array)))): + bcd_array[i].set_value(value % 100) + value /= 100 + + +def get_string(char_array): + """Convert an array of charDataElements into a string""" + return "".join([x.get_value() for x in char_array]) + + +def set_string(char_array, string): + """Set an array of charDataElements from a string""" + array_copy(char_array, list(string)) + + +class DataElement: + _size = 1 + + def __init__(self, data, offset, count=1): + self._data = data + self._offset = int(offset) + self._count = count + + def size(self): + return self._size * 8 + + def get_offset(self): + return self._offset + + def _get_value(self, data): + raise Exception("Not implemented") + + def get_value(self): + value = self._data[self._offset:self._offset + self._size] + return self._get_value(value) + + def set_value(self, value): + raise Exception("Not implemented for %s" % self.__class__) + + def get_raw(self, asbytes=False): + raw = self._data[self._offset:self._offset+self._size] + if asbytes: + return bytes(raw) + else: + return string_straight_decode(raw) + + def set_raw(self, data): + if isinstance(data, str): + data = string_straight_encode(data) + self._data[self._offset] = data[:self._size] + + def __getattr__(self, name): + raise AttributeError("Unknown attribute %s in %s" % (name, + self.__class__)) + + def __repr__(self): + return "(%s:%i bytes @ %04x)" % (self.__class__.__name__, + self._size, + self._offset) + + +class arrayDataElement(DataElement): + def __repr__(self): + if isinstance(self.__items[0], bcdDataElement): + return "%i:[(%i)]" % (len(self.__items), int(self)) + + if isinstance(self.__items[0], charDataElement): + return "%i:[(%s)]" % (len(self.__items), repr(str(self))[1:-1]) + + s = "%i:[" % len(self.__items) + s += ",".join([repr(item) for item in self.__items]) + s += "]" + return s + + def __init__(self, offset): + self.__items = [] + self._offset = offset + + def append(self, item): + self.__items.append(item) + + def get_value(self): + return list(self.__items) + + def get_raw(self, asbytes=False): + raw = [item.get_raw(asbytes=asbytes) for item in self.__items] + if asbytes: + return bytes(b''.join(raw)) + else: + return "".join(raw) + + def __setitem__(self, index, val): + self.__items[index].set_value(val) + + def __getitem__(self, index): + if isinstance(index, slice): + return self.__items[int(index.start):int(index.stop)] + else: + return self.__items[int(index)] + + def __len__(self): + return len(self.__items) + + def __str__(self): + if isinstance(self.__items[0], charDataElement): + # NOTE: All the values should be strings coming out of py3. + # and on py2 they could be a mix of unicode and str, the latter + # for non-ASCII values. On py2 we can just coerce all of these + # types to a string for compatbility. + return "".join([str(x.get_value()) for x in self.__items]) + else: + return str(self.__items) + + def __int__(self): + if isinstance(self.__items[0], bcdDataElement): + val = 0 + if isinstance(self.__items[0], bbcdDataElement): + items = self.__items + else: + items = reversed(self.__items) + for i in items: + tens, ones = i.get_value() + val = (val * 100) + (tens * 10) + ones + return val + else: + raise ValueError("Cannot coerce this to int") + + def __set_value_bbcd(self, value): + for i in reversed(self.__items): + twodigits = value % 100 + value /= 100 + i.set_value(twodigits) + + def __set_value_lbcd(self, value): + for i in self.__items: + twodigits = value % 100 + value /= 100 + i.set_value(twodigits) + + def __set_value_char(self, value): + if len(value) != len(self.__items): + raise ValueError("String expects exactly %i characters, not %i" % ( + len(self.__items), len(value))) + for i in range(0, len(self.__items)): + self.__items[i].set_value(value[i]) + + def set_value(self, value): + if isinstance(self.__items[0], bbcdDataElement): + self.__set_value_bbcd(int(value)) + elif isinstance(self.__items[0], lbcdDataElement): + self.__set_value_lbcd(int(value)) + elif isinstance(self.__items[0], charDataElement): + self.__set_value_char(value) + elif len(value) != len(self.__items): + raise ValueError("Array cardinality mismatch") + else: + for i in range(0, len(value)): + self.__items[i].set_value(value[i]) + + def index(self, value): + index = 0 + for i in self.__items: + if i.get_value() == value: + return index + index += 1 + raise IndexError() + + def __iter__(self): + return iter(self.__items) + + def items(self): + index = 0 + for item in self.__items: + yield (str(index), item) + index += 1 + + def size(self): + size = 0 + for i in self.__items: + size += i.size() + return size + + +class intDataElement(DataElement): + def __repr__(self): + fmt = "0x%%0%iX" % (self._size * 2) + return fmt % int(self) + + def __int__(self): + return self.get_value() + + def __nonzero__(self): + return int(self) != 0 + + def __invert__(self): + return ~self.get_value() + + def __trunc__(self): + return self.get_value() + + def __abs__(self): + return abs(self.get_value()) + + def __mod__(self, val): + return self.get_value() % val + + def __mul__(self, val): + return self.get_value() * val + + def __truediv__(self, val): + return self.get_value() / val + + def __div__(self, val): + return self.get_value() / val + + def __floordiv__(self, val): + return self.get_value() // val + + def __add__(self, val): + return self.get_value() + val + + def __sub__(self, val): + return self.get_value() - val + + def __or__(self, val): + return self.get_value() | val + + def __xor__(self, val): + return self.get_value() ^ val + + def __and__(self, val): + return self.get_value() & val + + def __radd__(self, val): + return val + self.get_value() + + def __rsub__(self, val): + return val - self.get_value() + + def __rmul__(self, val): + return val * self.get_value() + + def __rdiv__(self, val): + return val / self.get_value() + + def __rand__(self, val): + return val & self.get_value() + + def __ror__(self, val): + return val | self.get_value() + + def __rxor__(self, val): + return val ^ self.get_value() + + def __rmod__(self, val): + return val % self.get_value() + + def __lshift__(self, val): + return self.get_value() << val + + def __rshift__(self, val): + return self.get_value() >> val + + def __iadd__(self, val): + self.set_value(self.get_value() + val) + return self + + def __isub__(self, val): + self.set_value(self.get_value() - val) + return self + + def __imul__(self, val): + self.set_value(self.get_value() * val) + return self + + def __idiv__(self, val): + self.set_value(self.get_value() / val) + return self + + def __imod__(self, val): + self.set_value(self.get_value() % val) + return self + + def __iand__(self, val): + self.set_value(self.get_value() & val) + return self + + def __ior__(self, val): + self.set_value(self.get_value() | val) + return self + + def __ixor__(self, val): + self.set_value(self.get_value() ^ val) + return self + + def __index__(self): + return abs(self) + + def __eq__(self, val): + return self.get_value() == val + + def __ne__(self, val): + return self.get_value() != val + + def __lt__(self, val): + return self.get_value() < val + + def __le__(self, val): + return self.get_value() <= val + + def __gt__(self, val): + return self.get_value() > val + + def __ge__(self, val): + return self.get_value() >= val + + def __bool__(self): + return self.get_value() != 0 + + +class u8DataElement(intDataElement): + _size = 1 + + def _get_value(self, data): + return ord(data) + + def set_value(self, value): + self._data[self._offset] = (int(value) & 0xFF) + + +class u16DataElement(intDataElement): + _size = 2 + _endianess = ">" + + def _get_value(self, data): + return struct.unpack(self._endianess + "H", data)[0] + + def set_value(self, value): + self._data[self._offset] = struct.pack(self._endianess + "H", + int(value) & 0xFFFF) + + +class ul16DataElement(u16DataElement): + _endianess = "<" + + +class u24DataElement(intDataElement): + _size = 3 + _endianess = ">" + + def _get_value(self, data): + pre = self._endianess == ">" and b"\x00" or b"" + post = self._endianess == "<" and b"\x00" or b"" + return struct.unpack(self._endianess + "I", pre+data+post)[0] + + def set_value(self, value): + if self._endianess == "<": + start = 0 + end = 3 + else: + start = 1 + end = 4 + packed = struct.pack(self._endianess + "I", int(value) & 0xFFFFFFFF) + self._data[self._offset] = packed[start:end] + + +class ul24DataElement(u24DataElement): + _endianess = "<" + + +class u32DataElement(intDataElement): + _size = 4 + _endianess = ">" + + def _get_value(self, data): + return struct.unpack(self._endianess + "I", data)[0] + + def set_value(self, value): + self._data[self._offset] = struct.pack(self._endianess + "I", + int(value) & 0xFFFFFFFF) + + +class ul32DataElement(u32DataElement): + _endianess = "<" + + +class i8DataElement(u8DataElement): + _size = 1 + + def _get_value(self, data): + return struct.unpack("b", data)[0] + + def set_value(self, value): + self._data[self._offset] = struct.pack("b", int(value)) + + +class i16DataElement(intDataElement): + _size = 2 + _endianess = ">" + + def _get_value(self, data): + return struct.unpack(self._endianess + "h", data)[0] + + def set_value(self, value): + self._data[self._offset] = struct.pack(self._endianess + "h", + int(value)) + + +class il16DataElement(i16DataElement): + _endianess = "<" + + +class i24DataElement(intDataElement): + _size = 3 + _endianess = ">" + + def _get_value(self, data): + pre = self._endianess == ">" and "\x00" or "" + post = self._endianess == "<" and "\x00" or "" + return struct.unpack(self._endianess + "i", pre+data+post)[0] + + def set_value(self, value): + if self._endianess == "<": + start = 0 + end = 3 + else: + start = 1 + end = 4 + self._data[self._offset] = struct.pack(self._endianess + "i", + int(value))[start:end] + + +class il24DataElement(i24DataElement): + _endianess = "<" + + +class i32DataElement(intDataElement): + _size = 4 + _endianess = ">" + + def _get_value(self, data): + return struct.unpack(self._endianess + "i", data)[0] + + def set_value(self, value): + self._data[self._offset] = struct.pack(self._endianess + "i", + int(value)) + + +class il32DataElement(i32DataElement): + _endianess = "<" + + +class charDataElement(DataElement): + _size = 1 + + def __str__(self): + return str(self.get_value()) + + def __int__(self): + return ord(self.get_value()) + + def _get_value(self, data): + return string_straight_decode(data) + + def set_value(self, value): + if isinstance(value, int): + # This is the case if bytes() are passed in to arrayDataElement + # to set a string + self._data[self._offset] = value + else: + self._data[self._offset] = string_straight_encode(value) + + +class bcdDataElement(DataElement): + def __int__(self): + tens, ones = self.get_value() + return (tens * 10) + ones + + def set_bits(self, mask): + self._data[self._offset] = ord(self._data[self._offset]) | int(mask) + + def clr_bits(self, mask): + self._data[self._offset] = ord(self._data[self._offset]) & ~int(mask) + + def get_bits(self, mask): + return ord(self._data[self._offset]) & int(mask) + + def set_raw(self, data): + if isinstance(data, int): + self._data[self._offset] = data & 0xFF + elif isinstance(data, str): + self._data[self._offset] = string_straight_encode(data[0]) + else: + raise TypeError("Unable to set bcdDataElement from type %s" % + type(data)) + + def set_value(self, value): + self._data[self._offset] = int("%02i" % value, 16) + + def _get_value(self, data): + a = (ord(data) & 0xF0) >> 4 + b = ord(data) & 0x0F + return (a, b) + + +class lbcdDataElement(bcdDataElement): + _size = 1 + + +class bbcdDataElement(bcdDataElement): + _size = 1 + + +class bitDataElement(intDataElement): + _nbits = 0 + _shift = 0 + _subgen = u8DataElement # Default to a byte + + def __repr__(self): + fmt = "0x%%0%iX (%%sb)" % (self._size * 2) + return fmt % (int(self), format_binary(self._nbits, self.get_value())) + + def get_value(self): + data = self._subgen(self._data, self._offset).get_value() + mask = bits_between(self._shift-self._nbits, self._shift) + val = (data & mask) >> int(self._shift - self._nbits) + return val + + def set_value(self, value): + mask = bits_between(self._shift-self._nbits, self._shift) + + data = self._subgen(self._data, self._offset).get_value() + data &= ~mask + + value = ((int(value) << int(self._shift-self._nbits)) & mask) | data + + self._subgen(self._data, self._offset).set_value(value) + + def size(self): + return self._nbits + + +class structDataElement(DataElement): + def __repr__(self): + s = "struct {" + os.linesep + for prop in self._keys: + s += " %15s: %s%s" % (prop, repr(self._generators[prop]), + os.linesep) + s += "} %s (%i bytes at 0x%04X)%s" % (self._name, + self.size() / 8, + self._offset, + os.linesep) + return s + + def __init__(self, *args, **kwargs): + self._generators = {} + self._keys = [] + self._count = 1 + if "name" in list(kwargs.keys()): + self._name = kwargs["name"] + del kwargs["name"] + else: + self._name = "(anonymous)" + DataElement.__init__(self, *args, **kwargs) + self.__init = True + + def _value(self, data, generators): + result = {} + for name, gen in list(generators.items()): + result[name] = gen.get_value(data) + return result + + def get_value(self): + result = [] + for i in range(0, self._count): + result.append(self._value(self._data, self._generators[i])) + + if self._count == 1: + return result[0] + else: + return result + + def __getitem__(self, key): + return self._generators[key] + + def __setitem__(self, key, value): + if key in self._generators: + self._generators[key].set_value(value) + else: + self._generators[key] = value + self._keys.append(key) + + def __getattr__(self, name): + try: + return self._generators[name] + except KeyError: + raise AttributeError("No attribute %s in struct" % name) + + def __setattr__(self, name, value): + if "_structDataElement__init" not in self.__dict__: + self.__dict__[name] = value + else: + self.__dict__["_generators"][name].set_value(value) + + def size(self): + size = 0 + for name, gen in list(self._generators.items()): + if not isinstance(gen, list): + gen = [gen] + + i = 0 + for el in gen: + i += 1 + size += el.size() + return size + + def get_raw(self, asbytes=False): + size = self.size() // 8 + raw = self._data[self._offset:self._offset+size] + if asbytes: + return bytes(raw) + else: + return string_straight_decode(raw) + + def set_raw(self, buffer): + if len(buffer) != (self.size() / 8): + raise ValueError("Struct size mismatch during set_raw()") + if isinstance(buffer, str): + buffer = string_straight_encode(buffer) + self._data[self._offset] = buffer + + def __iter__(self): + for item in list(self._generators.values()): + yield item + + def items(self): + for key in self._keys: + yield key, self._generators[key] + + +class Processor: + + _types = { + "u8": u8DataElement, + "u16": u16DataElement, + "ul16": ul16DataElement, + "u24": u24DataElement, + "ul24": ul24DataElement, + "u32": u32DataElement, + "ul32": ul32DataElement, + "i8": i8DataElement, + "i16": i16DataElement, + "il16": il16DataElement, + "i24": i24DataElement, + "il24": il24DataElement, + "i32": i32DataElement, + "char": charDataElement, + "lbcd": lbcdDataElement, + "bbcd": bbcdDataElement, + } + + def __init__(self, data, offset): + if hasattr(data, 'get_byte_compatible'): + # bitwise uses the byte-compatible interface of MemoryMap, + # if that is what was passed in + data = data.get_byte_compatible() + self._data = data + self._offset = offset + self._obj = None + self._user_types = {} + + def do_symbol(self, symdef, gen): + name = symdef[1] + self._generators[name] = gen + + def do_bitfield(self, dtype, bitfield): + bytes = self._types[dtype](self._data, 0).size() / 8 + bitsleft = bytes * 8 + + for _bitdef, defn in bitfield: + name = defn[0][1] + bits = int(defn[1][1]) + if bitsleft < 0: + raise ParseError("Invalid bitfield spec") + + class bitDE(bitDataElement): + _nbits = bits + _shift = bitsleft + _subgen = self._types[dtype] + + self._generators[name] = bitDE(self._data, self._offset) + bitsleft -= bits + + if bitsleft: + LOG.warn("WARNING: %i trailing bits unaccounted for in %s" % + (bitsleft, bitfield)) + + return bytes + + def do_bitarray(self, i, count): + if count % 8 != 0: + raise ValueError("bit array must be divisible by 8.") + + class bitDE(bitDataElement): + _nbits = 1 + _shift = 8 - i % 8 + + return bitDE(self._data, self._offset) + + def parse_defn(self, defn): + dtype = defn[0] + + if defn[1][0] == "bitfield": + size = self.do_bitfield(dtype, defn[1][1]) + count = 1 + self._offset += size + else: + if defn[1][0] == "array": + sym = defn[1][1][0] + count = int(defn[1][1][1][1]) + else: + count = 1 + sym = defn[1] + + name = sym[1] + res = arrayDataElement(self._offset) + size = 0 + for i in range(0, count): + if dtype == "bit": + gen = self.do_bitarray(i, count) + self._offset += int((i+1) % 8 == 0) + else: + gen = self._types[dtype](self._data, self._offset) + self._offset += (gen.size() / 8) + res.append(gen) + + if count == 1: + self._generators[name] = res[0] + else: + self._generators[name] = res + + def parse_struct_decl(self, struct): + block = struct[:-1] + if block[0][0] == "symbol": + # This is a pre-defined struct + block = self._user_types[block[0][1]] + deftype = struct[-1] + if deftype[0] == "array": + name = deftype[1][0][1] + count = int(deftype[1][1][1]) + elif deftype[0] == "symbol": + name = deftype[1] + count = 1 + + result = arrayDataElement(self._offset) + for i in range(0, count): + element = structDataElement(self._data, self._offset, count, + name=name) + result.append(element) + tmp = self._generators + self._generators = element + self.parse_block(block) + self._generators = tmp + + if count == 1: + self._generators[name] = result[0] + else: + self._generators[name] = result + + def parse_struct_defn(self, struct): + name = struct[0][1] + block = struct[1:] + self._user_types[name] = block + + def parse_struct(self, struct): + if struct[0][0] == "struct_defn": + return self.parse_struct_defn(struct[0][1]) + elif struct[0][0] == "struct_decl": + return self.parse_struct_decl(struct[0][1]) + else: + raise Exception("Internal error: What is `%s'?" % struct[0][0]) + + def parse_directive(self, directive): + name = directive[0][0] + value = directive[0][1][0][1] + if name == "seekto": + self._offset = int(value, 0) + elif name == "seek": + self._offset += int(value, 0) + elif name == "printoffset": + LOG.debug("%s: %i (0x%08X)" % + (value[1:-1], self._offset, self._offset)) + + def parse_block(self, lang): + for t, d in lang: + if t == "struct": + self.parse_struct(d) + elif t == "definition": + self.parse_defn(d) + elif t == "directive": + self.parse_directive(d) + + def parse(self, lang): + self._generators = structDataElement(self._data, self._offset) + self.parse_block(lang) + return self._generators + + +def parse(spec, data, offset=0): + ast = bitwise_grammar.parse(spec) + p = Processor(data, offset) + return p.parse(ast) + +if __name__ == "__main__": + defn = """ +struct mytype { u8 foo; }; +struct mytype bar; +struct { + u8 foo; + u8 highbit:1, + sixzeros:6, + lowbit:1; + char string[3]; + bbcd fourdigits[2]; +} mystruct; +""" + data = "\xab\x7F\x81abc\x12\x34" + tree = parse(defn, data) + + print(repr(tree)) + + print("Foo %i" % tree.mystruct.foo) + print("Highbit: %i SixZeros: %i: Lowbit: %i" % (tree.mystruct.highbit, + tree.mystruct.sixzeros, + tree.mystruct.lowbit)) + print("String: %s" % tree.mystruct.string) + print("Fourdigits: %i" % tree.mystruct.fourdigits) + + import sys + sys.exit(0) + + test = """ + struct { + u16 bar; + u16 baz; + u8 one; + u8 upper:2, + twobit:1, + onebit:1, + lower:4; + u8 array[3]; + char str[3]; + bbcd bcdL[2]; + } foo[2]; + u8 tail; + """ + data = "\xfe\x10\x00\x08\xFF\x23\x01\x02\x03abc\x34\x89" + data = (data * 2) + "\x12" + data = MemoryMap(data) + + ast = bitwise_grammar.parse(test) + + # Just for testing, pretty-print the tree + pp(ast) + + # Mess with it a little + p = Processor(data, 0) + obj = p.parse(ast) + print("Object: %s" % obj) + print(obj["foo"][0]["bcdL"]) + print(obj["tail"]) + print(obj["foo"][0]["bar"]) + obj["foo"][0]["bar"].set_value(255 << 8) + obj["foo"][0]["twobit"].set_value(0) + obj["foo"][0]["onebit"].set_value(1) + print("%i" % int(obj["foo"][0]["bar"])) + + for i in obj["foo"][0]["array"]: + print(int(i)) + obj["foo"][0]["array"][1].set_value(255) + + for i in obj["foo"][0]["bcdL"]: + print(i.get_value()) + + int_to_bcd(obj["foo"][0]["bcdL"], 1234) + print(bcd_to_int(obj["foo"][0]["bcdL"])) + + set_string(obj["foo"][0]["str"], "xyz") + print(get_string(obj["foo"][0]["str"])) + + print(repr(data.get_packed())) diff --git a/chirp/bitwise_grammar.py b/chirp/bitwise_grammar.py new file mode 100644 index 0000000..c61eda2 --- /dev/null +++ b/chirp/bitwise_grammar.py @@ -0,0 +1,134 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re +from chirp.pyPEG import keyword, parse as pypeg_parse + +TYPES = ["bit", "u8", "u16", "ul16", "u24", "ul24", "u32", "ul32", + "i8", "i16", "il16", "i24", "il24", "i32", "il32", "char", + "lbcd", "bbcd"] +DIRECTIVES = ["seekto", "seek", "printoffset"] + + +def string(): + return re.compile(r"\"[^\"]*\"") + + +def symbol(): + return re.compile(r"\w+") + + +def count(): + return re.compile(r"([1-9][0-9]*|0x[0-9a-fA-F]+)") + + +def bitdef(): + return symbol, ":", count, -1 + + +def _bitdeflist(): + return bitdef, -1, (",", bitdef) + + +def bitfield(): + return -2, _bitdeflist + + +def array(): + return symbol, '[', count, ']' + + +def _typedef(): + return re.compile(r"(%s)" % "|".join(TYPES)) + + +def definition(): + return _typedef, [array, bitfield, symbol], ";" + + +def seekto(): + return keyword("seekto"), count + + +def seek(): + return keyword("seek"), count + + +def printoffset(): + return keyword("printoffset"), string + + +def directive(): + return "#", [seekto, seek, printoffset], ";" + + +def _block_inner(): + return -2, [definition, struct, directive] + + +def _block(): + return "{", _block_inner, "}" + + +def struct_defn(): + return symbol, _block + + +def struct_decl(): + return [symbol, _block], [array, symbol] + + +def struct(): + return keyword("struct"), [struct_defn, struct_decl], ";" + + +def _language(): + return _block_inner + + +def parse(data): + lines = data.split("\n") + for index, line in enumerate(lines): + if '//' in line: + lines[index] = line[:line.index('//')] + + class FakeFileInput(object): + """Simulate line-by-line file reading from @data""" + line = -1 + + def isfirstline(self): + return self.line == 0 + + def filename(self): + return "input" + + def lineno(self): + return self.line + + def __iter__(self): + return self + + def next(self): + return self.__next__() + + def __next__(self): + self.line += 1 + try: + # Note, FileInput objects keep the newlines + return lines[self.line] + "\n" + except IndexError: + raise StopIteration + + return pypeg_parse(_language, FakeFileInput()) diff --git a/chirp/chirp_common.py b/chirp/chirp_common.py new file mode 100644 index 0000000..dd3cf7a --- /dev/null +++ b/chirp/chirp_common.py @@ -0,0 +1,1631 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes +from future import standard_library + +import base64 +import json +import logging +import math +import sys + +from chirp import errors, memmap, CHIRP_VERSION + +LOG = logging.getLogger(__name__) + +SEPCHAR = "," + +# 50 Tones +TONES = [67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, + 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, + 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, + 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, + 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, + 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, + 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, + 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, + ] + +TONES_EXTRA = [56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, + 62.5, 63.0, 64.0] + +OLD_TONES = list(TONES) +[OLD_TONES.remove(x) for x in [159.8, 165.5, 171.3, 177.3, 183.5, 189.9, + 196.6, 199.5, 206.5, 229.1, 254.1]] + +# 104 DTCS Codes +DTCS_CODES = [ + 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, + 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, + 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, + 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, + 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, + 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, + 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, + 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, + 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, + 731, 732, 734, 743, 754, +] + +# 512 Possible DTCS Codes +ALL_DTCS_CODES = [] +for a in range(0, 8): + for b in range(0, 8): + for c in range(0, 8): + ALL_DTCS_CODES.append((a * 100) + (b * 10) + c) + +CROSS_MODES = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS", + "Tone->" +] + +MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY", + "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR", + "FSK", "FSKR", "DMR"] + +TONE_MODES = [ + "", + "Tone", + "TSQL", + "DTCS", + "DTCS-R", + "TSQL-R", + "Cross", +] + +TUNING_STEPS = [ + 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0, + 125.0, 200.0, + # Need to fix drivers using this list as an index! + 9.0, 1.0, 2.5, +] +# These are the default for RadioFeatures.valid_tuning_steps +COMMON_TUNING_STEPS = [5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + + +SKIP_VALUES = ["", "S", "P"] + +CHARSET_UPPER_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890" +CHARSET_ALPHANUMERIC = \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 1234567890" +CHARSET_ASCII = "".join([chr(x) for x in range(ord(" "), ord("~") + 1)]) + +# http://aprs.org/aprs11/SSIDs.txt +APRS_SSID = ( + "0 Your primary station usually fixed and message capable", + "1 generic additional station, digi, mobile, wx, etc", + "2 generic additional station, digi, mobile, wx, etc", + "3 generic additional station, digi, mobile, wx, etc", + "4 generic additional station, digi, mobile, wx, etc", + "5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)", + "6 Special activity, Satellite ops, camping or 6 meters, etc", + "7 walkie talkies, HT's or other human portable", + "8 boats, sailboats, RV's or second main mobile", + "9 Primary Mobile (usually message capable)", + "10 internet, Igates, echolink, winlink, AVRS, APRN, etc", + "11 balloons, aircraft, spacecraft, etc", + "12 APRStt, DTMF, RFID, devices, one-way trackers*, etc", + "13 Weather stations", + "14 Truckers or generally full time drivers", + "15 generic additional station, digi, mobile, wx, etc") +APRS_POSITION_COMMENT = ( + "off duty", "en route", "in service", "returning", "committed", + "special", "priority", "custom 0", "custom 1", "custom 2", "custom 3", + "custom 4", "custom 5", "custom 6", "EMERGENCY") +# http://aprs.org/symbols/symbolsX.txt +APRS_SYMBOLS = ( + "Police/Sheriff", "[reserved]", "Digi", "Phone", "DX Cluster", + "HF Gateway", "Small Aircraft", "Mobile Satellite Groundstation", + "Wheelchair", "Snowmobile", "Red Cross", "Boy Scouts", "House QTH (VHF)", + "X", "Red Dot", "0 in Circle", "1 in Circle", "2 in Circle", + "3 in Circle", "4 in Circle", "5 in Circle", "6 in Circle", "7 in Circle", + "8 in Circle", "9 in Circle", "Fire", "Campground", "Motorcycle", + "Railroad Engine", "Car", "File Server", "Hurricane Future Prediction", + "Aid Station", "BBS or PBBS", "Canoe", "[reserved]", "Eyeball", + "Tractor/Farm Vehicle", "Grid Square", "Hotel", "TCP/IP", "[reserved]", + "School", "PC User", "MacAPRS", "NTS Station", "Balloon", "Police", "TBD", + "Recreational Vehicle", "Space Shuttle", "SSTV", "Bus", "ATV", + "National WX Service Site", "Helicopter", "Yacht/Sail Boat", "WinAPRS", + "Human/Person", "Triangle", "Mail/Postoffice", "Large Aircraft", + "WX Station", "Dish Antenna", "Ambulance", "Bicycle", + "Incident Command Post", "Dual Garage/Fire Dept", "Horse/Equestrian", + "Fire Truck", "Glider", "Hospital", "IOTA", "Jeep", "Truck", "Laptop", + "Mic-Repeater", "Node", "Emergency Operations Center", "Rover (dog)", + "Grid Square above 128m", "Repeater", "Ship/Power Boat", "Truck Stop", + "Truck (18 wheeler)", "Van", "Water Station", "X-APRS", "Yagi at QTH", + "TDB", "[reserved]" +) + + +def watts_to_dBm(watts): + """Converts @watts in watts to dBm""" + return int(10 * math.log10(int(watts * 1000))) + + +def dBm_to_watts(dBm): + """Converts @dBm from dBm to watts""" + return int(math.pow(10, (dBm - 30) / 10)) + + +class PowerLevel: + """Represents a power level supported by a radio""" + + def __init__(self, label, watts=0, dBm=0): + if watts: + dBm = watts_to_dBm(watts) + self._power = int(dBm) + self._label = label + + def __str__(self): + return str(self._label) + + def __int__(self): + return self._power + + def __sub__(self, val): + return int(self) - int(val) + + def __add__(self, val): + return int(self) + int(val) + + def __eq__(self, val): + if val is not None: + return int(self) == int(val) + return False + + def __lt__(self, val): + return int(self) < int(val) + + def __gt__(self, val): + return int(self) > int(val) + + def __bool__(self): + return int(self) != 0 + + def __repr__(self): + return "%s (%i dBm)" % (self._label, self._power) + + +def parse_freq(freqstr): + """Parse a frequency string and return the value in integral Hz""" + freqstr = freqstr.strip() + if freqstr == "": + return 0 + elif freqstr.endswith(" MHz"): + return parse_freq(freqstr.split(" ")[0]) + elif freqstr.endswith(" kHz"): + return int(freqstr.split(" ")[0]) * 1000 + + if "." in freqstr: + mhz, khz = freqstr.split(".") + if mhz == "": + mhz = 0 + khz = khz.ljust(6, "0") + if len(khz) > 6: + raise ValueError("Invalid kHz value: %s", khz) + mhz = int(mhz) * 1000000 + khz = int(khz) + else: + mhz = int(freqstr) * 1000000 + khz = 0 + + return mhz + khz + + +def format_freq(freq): + """Format a frequency given in Hz as a string""" + + return "%i.%06i" % (freq / 1000000, freq % 1000000) + + +class ImmutableValueError(ValueError): + pass + + +class Memory: + """Base class for a single radio memory""" + freq = 0 + number = 0 + extd_number = "" + name = "" + vfo = 0 + rtone = 88.5 + ctone = 88.5 + dtcs = 23 + rx_dtcs = 23 + tmode = "" + cross_mode = "Tone->Tone" + dtcs_polarity = "NN" + skip = "" + power = None + duplex = "" + offset = 600000 + mode = "FM" + tuning_step = 5.0 + + comment = "" + + empty = False + + immutable = [] + + # A RadioSettingGroup of additional settings supported by the radio, + # or an empty list if none + extra = [] + + def __init__(self): + self.freq = 0 + self.number = 0 + self.extd_number = "" + self.name = "" + self.vfo = 0 + self.rtone = 88.5 + self.ctone = 88.5 + self.dtcs = 23 + self.rx_dtcs = 23 + self.tmode = "" + self.cross_mode = "Tone->Tone" + self.dtcs_polarity = "NN" + self.skip = "" + self.power = None + self.duplex = "" + self.offset = 600000 + self.mode = "FM" + self.tuning_step = 5.0 + + self.comment = "" + + self.empty = False + + self.immutable = [] + + _valid_map = { + "rtone": TONES + TONES_EXTRA, + "ctone": TONES + TONES_EXTRA, + "dtcs": ALL_DTCS_CODES, + "rx_dtcs": ALL_DTCS_CODES, + "tmode": TONE_MODES, + "dtcs_polarity": ["NN", "NR", "RN", "RR"], + "cross_mode": CROSS_MODES, + "mode": MODES, + "duplex": ["", "+", "-", "split", "off"], + "skip": SKIP_VALUES, + "empty": [True, False], + "dv_code": [x for x in range(0, 100)], + } + + def __repr__(self): + return "Memory[%i]" % self.number + + def dupe(self): + """Return a deep copy of @self""" + mem = self.__class__() + for k, v in list(self.__dict__.items()): + mem.__dict__[k] = v + + return mem + + def clone(self, source): + """Absorb all of the properties of @source""" + for k, v in list(source.__dict__.items()): + self.__dict__[k] = v + + CSV_FORMAT = ["Location", "Name", "Frequency", + "Duplex", "Offset", "Tone", + "rToneFreq", "cToneFreq", "DtcsCode", + "DtcsPolarity", "Mode", "TStep", + "Skip", "Comment", + "URCALL", "RPT1CALL", "RPT2CALL", "DVCODE"] + + def __setattr__(self, name, val): + if not hasattr(self, name): + raise ValueError("No such attribute `%s'" % name) + + if name in self.immutable: + raise ImmutableValueError("Field %s is not " % name + + "mutable on this memory") + + if name in self._valid_map and val not in self._valid_map[name]: + raise ValueError("`%s' is not in valid list: %s" % + (val, self._valid_map[name])) + + self.__dict__[name] = val + + def format_freq(self): + """Return a properly-formatted string of this memory's frequency""" + return format_freq(self.freq) + + def parse_freq(self, freqstr): + """Set the frequency from a string""" + self.freq = parse_freq(freqstr) + return self.freq + + def __str__(self): + if self.tmode == "Tone": + tenc = "*" + else: + tenc = " " + + if self.tmode == "TSQL": + tsql = "*" + else: + tsql = " " + + if self.tmode == "DTCS": + dtcs = "*" + else: + dtcs = " " + + if self.duplex == "": + dup = "/" + else: + dup = self.duplex + + return \ + "Memory %s: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]" % \ + (self.number if self.extd_number == "" else self.extd_number, + format_freq(self.freq), + dup, + format_freq(self.offset), + self.mode, + self.name, + self.rtone, + tenc, + self.ctone, + tsql, + self.dtcs, + dtcs, + self.dtcs_polarity, + self.tuning_step) + + def to_csv(self): + """Return a CSV representation of this memory""" + return [ + "%i" % self.number, + "%s" % self.name, + format_freq(self.freq), + "%s" % self.duplex, + format_freq(self.offset), + "%s" % self.tmode, + "%.1f" % self.rtone, + "%.1f" % self.ctone, + "%03i" % self.dtcs, + "%s" % self.dtcs_polarity, + "%s" % self.mode, + "%.2f" % self.tuning_step, + "%s" % self.skip, + "%s" % self.comment, + "", "", "", ""] + + @classmethod + def _from_csv(cls, _line): + line = _line.strip() + if line.startswith("Location"): + raise errors.InvalidMemoryLocation("Non-CSV line") + + vals = line.split(SEPCHAR) + if len(vals) < 11: + raise errors.InvalidDataError("CSV format error " + + "(14 columns expected)") + + if vals[10] == "DV": + mem = DVMemory() + else: + mem = Memory() + + mem.really_from_csv(vals) + return mem + + def really_from_csv(self, vals): + """Careful parsing of split-out @vals""" + try: + self.number = int(vals[0]) + except: + raise errors.InvalidDataError( + "Location '%s' is not a valid integer" % vals[0]) + + self.name = vals[1] + + try: + self.freq = float(vals[2]) + except: + raise errors.InvalidDataError("Frequency is not a valid number") + + if vals[3].strip() in ["+", "-", ""]: + self.duplex = vals[3].strip() + else: + raise errors.InvalidDataError("Duplex is not +,-, or empty") + + try: + self.offset = float(vals[4]) + except: + raise errors.InvalidDataError("Offset is not a valid number") + + self.tmode = vals[5] + if self.tmode not in TONE_MODES: + raise errors.InvalidDataError("Invalid tone mode `%s'" % + self.tmode) + + try: + self.rtone = float(vals[6]) + except: + raise errors.InvalidDataError("rTone is not a valid number") + if self.rtone not in TONES: + raise errors.InvalidDataError("rTone is not valid") + + try: + self.ctone = float(vals[7]) + except: + raise errors.InvalidDataError("cTone is not a valid number") + if self.ctone not in TONES: + raise errors.InvalidDataError("cTone is not valid") + + try: + self.dtcs = int(vals[8], 10) + except: + raise errors.InvalidDataError("DTCS code is not a valid number") + if self.dtcs not in DTCS_CODES: + raise errors.InvalidDataError("DTCS code is not valid") + + try: + self.rx_dtcs = int(vals[8], 10) + except: + raise errors.InvalidDataError("DTCS Rx code is not a valid number") + if self.rx_dtcs not in DTCS_CODES: + raise errors.InvalidDataError("DTCS Rx code is not valid") + + if vals[9] in ["NN", "NR", "RN", "RR"]: + self.dtcs_polarity = vals[9] + else: + raise errors.InvalidDataError("DtcsPolarity is not valid") + + if vals[10] in MODES: + self.mode = vals[10] + else: + raise errors.InvalidDataError("Mode is not valid") + + try: + self.tuning_step = float(vals[11]) + except: + raise errors.InvalidDataError("Tuning step is invalid") + + try: + self.skip = vals[12] + except: + raise errors.InvalidDataError("Skip value is not valid") + + return True + + +class DVMemory(Memory): + """A Memory with D-STAR attributes""" + dv_urcall = "CQCQCQ" + dv_rpt1call = "" + dv_rpt2call = "" + dv_code = 0 + + def __str__(self): + string = Memory.__str__(self) + + string += " <%s,%s,%s>" % (self.dv_urcall, + self.dv_rpt1call, + self.dv_rpt2call) + + return string + + def to_csv(self): + return [ + "%i" % self.number, + "%s" % self.name, + format_freq(self.freq), + "%s" % self.duplex, + format_freq(self.offset), + "%s" % self.tmode, + "%.1f" % self.rtone, + "%.1f" % self.ctone, + "%03i" % self.dtcs, + "%s" % self.dtcs_polarity, + "%s" % self.mode, + "%.2f" % self.tuning_step, + "%s" % self.skip, + "%s" % self.comment, + "%s" % self.dv_urcall, + "%s" % self.dv_rpt1call, + "%s" % self.dv_rpt2call, + "%i" % self.dv_code] + + def really_from_csv(self, vals): + Memory.really_from_csv(self, vals) + + self.dv_urcall = vals[15].rstrip()[:8] + self.dv_rpt1call = vals[16].rstrip()[:8] + self.dv_rpt2call = vals[17].rstrip()[:8] + try: + self.dv_code = int(vals[18].strip()) + except Exception: + self.dv_code = 0 + + +class MemoryMapping(object): + """Base class for a memory mapping""" + + def __init__(self, model, index, name): + self._model = model + self._index = index + self._name = name + + def __str__(self): + return self.get_name() + + def __repr__(self): + return "%s-%s" % (self.__class__.__name__, self._index) + + def get_name(self): + """Returns the mapping name""" + return self._name + + def get_index(self): + """Returns the immutable index (string or int)""" + return self._index + + def __eq__(self, other): + return self.get_index() == other.get_index() + + +class MappingModel(object): + """Base class for a memory mapping model""" + + def __init__(self, radio, name): + self._radio = radio + self._name = name + + def get_name(self): + return self._name + + def get_num_mappings(self): + """Returns the number of mappings in the model (should be + callable without consulting the radio""" + raise NotImplementedError() + + def get_mappings(self): + """Return a list of mappings""" + raise NotImplementedError() + + def add_memory_to_mapping(self, memory, mapping): + """Add @memory to @mapping.""" + raise NotImplementedError() + + def remove_memory_from_mapping(self, memory, mapping): + """Remove @memory from @mapping. + Shall raise exception if @memory is not in @bank""" + raise NotImplementedError() + + def get_mapping_memories(self, mapping): + """Return a list of memories in @mapping""" + raise NotImplementedError() + + def get_memory_mappings(self, memory): + """Return a list of mappings that @memory is in""" + raise NotImplementedError() + + +class Bank(MemoryMapping): + """Base class for a radio's Bank""" + + +class NamedBank(Bank): + """A bank that can have a name""" + + def set_name(self, name): + """Changes the user-adjustable bank name""" + self._name = name + + +class BankModel(MappingModel): + """A bank model where one memory is in zero or one banks at any point""" + + def __init__(self, radio, name='Banks'): + super(BankModel, self).__init__(radio, name) + + +class MappingModelIndexInterface: + """Interface for mappings with index capabilities""" + + def get_index_bounds(self): + """Returns a tuple (lo,hi) of the min and max mapping indices""" + raise NotImplementedError() + + def get_memory_index(self, memory, mapping): + """Returns the index of @memory in @mapping""" + raise NotImplementedError() + + def set_memory_index(self, memory, mapping, index): + """Sets the index of @memory in @mapping to @index""" + raise NotImplementedError() + + def get_next_mapping_index(self, mapping): + """Returns the next available mapping index in @mapping, or raises + Exception if full""" + raise NotImplementedError() + + +class MTOBankModel(BankModel): + """A bank model where one memory can be in multiple banks at once """ + pass + + +def console_status(status): + """Write a status object to the console""" + import logging + from chirp import logger + if not logger.is_visible(logging.WARN): + return + import sys + import os + sys.stdout.write("\r%s" % status) + if status.cur == status.max: + sys.stdout.write(os.linesep) + + +class RadioPrompts: + """Radio prompt strings""" + info = None + experimental = None + pre_download = None + pre_upload = None + display_pre_upload_prompt_before_opening_port = True + + +BOOLEAN = [True, False] + + +class RadioFeatures: + """Radio Feature Flags""" + _valid_map = { + # General + "has_bank_index": BOOLEAN, + "has_dtcs": BOOLEAN, + "has_rx_dtcs": BOOLEAN, + "has_dtcs_polarity": BOOLEAN, + "has_mode": BOOLEAN, + "has_offset": BOOLEAN, + "has_name": BOOLEAN, + "has_bank": BOOLEAN, + "has_bank_names": BOOLEAN, + "has_tuning_step": BOOLEAN, + "has_ctone": BOOLEAN, + "has_cross": BOOLEAN, + "has_infinite_number": BOOLEAN, + "has_nostep_tuning": BOOLEAN, + "has_comment": BOOLEAN, + "has_settings": BOOLEAN, + + # Attributes + "valid_modes": [], + "valid_tmodes": [], + "valid_duplexes": [], + "valid_tuning_steps": [], + "valid_bands": [], + "valid_skips": [], + "valid_power_levels": [], + "valid_characters": "", + "valid_name_length": 0, + "valid_cross_modes": [], + "valid_dtcs_pols": [], + "valid_dtcs_codes": [], + "valid_special_chans": [], + + "has_sub_devices": BOOLEAN, + "memory_bounds": (0, 0), + "can_odd_split": BOOLEAN, + "can_delete": BOOLEAN, + + # D-STAR + "requires_call_lists": BOOLEAN, + "has_implicit_calls": BOOLEAN, + } + + def __setattr__(self, name, val): + if name.startswith("_"): + self.__dict__[name] = val + return + elif name not in list(self._valid_map.keys()): + raise ValueError("No such attribute `%s'" % name) + + if type(self._valid_map[name]) == tuple: + # Tuple, cardinality must match + if type(val) != tuple or len(val) != len(self._valid_map[name]): + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif type(self._valid_map[name]) == list and not self._valid_map[name]: + # Empty list, must be another list + if type(val) != list: + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif type(self._valid_map[name]) == str: + if type(val) != str: + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif type(self._valid_map[name]) == int: + if type(val) != int: + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif val not in self._valid_map[name]: + # Value not in the list of valid values + raise ValueError("Invalid value `%s' for attribute `%s'" % (val, + name)) + self.__dict__[name] = val + + def __getattr__(self, name): + raise AttributeError("pylint is confused by RadioFeatures") + + def init(self, attribute, default, doc=None): + """Initialize a feature flag @attribute with default value @default, + and documentation string @doc""" + self.__setattr__(attribute, default) + self.__docs[attribute] = doc + + def get_doc(self, attribute): + """Return the description of @attribute""" + return self.__docs[attribute] + + def __init__(self): + self.__docs = {} + self.init("has_bank_index", False, + "Indicates that memories in a bank can be stored in " + + "an order other than in main memory") + self.init("has_dtcs", True, + "Indicates that DTCS tone mode is available") + self.init("has_rx_dtcs", False, + "Indicates that radio can use two different " + + "DTCS codes for rx and tx") + self.init("has_dtcs_polarity", True, + "Indicates that the DTCS polarity can be changed") + self.init("has_mode", True, + "Indicates that multiple emission modes are supported") + self.init("has_offset", True, + "Indicates that the TX offset memory property is supported") + self.init("has_name", True, + "Indicates that an alphanumeric memory name is supported") + self.init("has_bank", True, + "Indicates that memories may be placed into banks") + self.init("has_bank_names", False, + "Indicates that banks may be named") + self.init("has_tuning_step", True, + "Indicates that memories store their tuning step") + self.init("has_ctone", True, + "Indicates that the radio keeps separate tone frequencies " + + "for repeater and CTCSS operation") + self.init("has_cross", False, + "Indicates that the radios supports different tone modes " + + "on transmit and receive") + self.init("has_infinite_number", False, + "Indicates that the radio is not constrained in the " + + "number of memories that it can store") + self.init("has_nostep_tuning", False, + "Indicates that the radio does not require a valid " + + "tuning step to store a frequency") + self.init("has_comment", False, + "Indicates that the radio supports storing a comment " + + "with each memory") + self.init("has_settings", False, + "Indicates that the radio supports general settings") + + self.init("valid_modes", list(MODES), + "Supported emission (or receive) modes") + self.init("valid_tmodes", [], + "Supported tone squelch modes") + self.init("valid_duplexes", ["", "+", "-"], + "Supported duplex modes") + self.init("valid_tuning_steps", list(COMMON_TUNING_STEPS), + "Supported tuning steps") + self.init("valid_bands", [], + "Supported frequency ranges") + self.init("valid_skips", ["", "S"], + "Supported memory scan skip settings") + self.init("valid_power_levels", [], + "Supported power levels") + self.init("valid_characters", CHARSET_UPPER_NUMERIC, + "Supported characters for a memory's alphanumeric tag") + self.init("valid_name_length", 6, + "The maximum number of characters in a memory's " + + "alphanumeric tag") + self.init("valid_cross_modes", list(CROSS_MODES), + "Supported tone cross modes") + self.init("valid_dtcs_pols", ["NN", "RN", "NR", "RR"], + "Supported DTCS polarities") + self.init("valid_dtcs_codes", list(DTCS_CODES), + "Supported DTCS codes") + self.init("valid_special_chans", [], + "Supported special channel names") + + self.init("has_sub_devices", False, + "Indicates that the radio behaves as two semi-independent " + + "devices") + self.init("memory_bounds", (0, 1), + "The minimum and maximum channel numbers") + self.init("can_odd_split", False, + "Indicates that the radio can store an independent " + + "transmit frequency") + self.init("can_delete", True, + "Indicates that the radio can delete memories") + self.init("requires_call_lists", True, + "[D-STAR] Indicates that the radio requires all callsigns " + + "to be in the master list and cannot be stored " + + "arbitrarily in each memory channel") + self.init("has_implicit_calls", False, + "[D-STAR] Indicates that the radio has an implied " + + "callsign at the beginning of the master URCALL list") + + def is_a_feature(self, name): + """Returns True if @name is a valid feature flag name""" + return name in list(self._valid_map.keys()) + + def __getitem__(self, name): + return self.__dict__[name] + + def validate_memory(self, mem): + """Return a list of warnings and errors that will be encoundered + if trying to set @mem on the current radio""" + msgs = [] + + lo, hi = self.memory_bounds + if not self.has_infinite_number and \ + (mem.number < lo or mem.number > hi) and \ + mem.extd_number not in self.valid_special_chans: + msg = ValidationWarning("Location %i is out of range" % mem.number) + msgs.append(msg) + + if (self.valid_modes and + mem.mode not in self.valid_modes and + mem.mode != "Auto"): + msg = ValidationError("Mode %s not supported" % mem.mode) + msgs.append(msg) + + if self.valid_tmodes and mem.tmode not in self.valid_tmodes: + msg = ValidationError("Tone mode %s not supported" % mem.tmode) + msgs.append(msg) + else: + if mem.tmode == "Cross": + if self.valid_cross_modes and \ + mem.cross_mode not in self.valid_cross_modes: + msg = ValidationError("Cross tone mode %s not supported" % + mem.cross_mode) + msgs.append(msg) + + if self.has_dtcs_polarity and \ + mem.dtcs_polarity not in self.valid_dtcs_pols: + msg = ValidationError("DTCS Polarity %s not supported" % + mem.dtcs_polarity) + msgs.append(msg) + + if self.valid_dtcs_codes and \ + mem.dtcs not in self.valid_dtcs_codes: + msg = ValidationError("DTCS Code %03i not supported" % mem.dtcs) + if self.valid_dtcs_codes and \ + mem.rx_dtcs not in self.valid_dtcs_codes: + msg = ValidationError("DTCS Code %03i not supported" % mem.rx_dtcs) + + if self.valid_duplexes and mem.duplex not in self.valid_duplexes: + msg = ValidationError("Duplex %s not supported" % mem.duplex) + msgs.append(msg) + + ts = mem.tuning_step + if self.valid_tuning_steps and ts not in self.valid_tuning_steps and \ + not self.has_nostep_tuning: + msg = ValidationError("Tuning step %.2f not supported" % ts) + msgs.append(msg) + + if self.valid_bands: + valid = False + for lo, hi in self.valid_bands: + if lo <= mem.freq < hi: + valid = True + break + if not valid: + msg = ValidationError( + ("Frequency {freq} is out " + "of supported range").format(freq=format_freq(mem.freq))) + msgs.append(msg) + + if self.valid_bands and \ + self.valid_duplexes and \ + mem.duplex in ["split", "-", "+"]: + if mem.duplex == "split": + freq = mem.offset + elif mem.duplex == "-": + freq = mem.freq - mem.offset + elif mem.duplex == "+": + freq = mem.freq + mem.offset + valid = False + for lo, hi in self.valid_bands: + if lo <= freq < hi: + valid = True + break + if not valid: + msg = ValidationError( + ("Tx freq {freq} is out " + "of supported range").format(freq=format_freq(freq))) + msgs.append(msg) + + if mem.power and \ + self.valid_power_levels and \ + mem.power not in self.valid_power_levels: + msg = ValidationWarning("Power level %s not supported" % mem.power) + msgs.append(msg) + + if self.valid_tuning_steps and not self.has_nostep_tuning: + try: + step = required_step(mem.freq) + if step not in self.valid_tuning_steps: + msg = ValidationError("Frequency requires %.2fkHz step" % + required_step(mem.freq)) + msgs.append(msg) + except errors.InvalidDataError as e: + msgs.append(str(e)) + + if self.valid_characters: + for char in mem.name: + if char not in self.valid_characters: + msgs.append(ValidationWarning("Name character " + + "`%s'" % char + + " not supported")) + break + + return msgs + + +class ValidationMessage(str): + """Base class for Validation Errors and Warnings""" + pass + + +class ValidationWarning(ValidationMessage): + """A non-fatal warning during memory validation""" + pass + + +class ValidationError(ValidationMessage): + """A fatal error during memory validation""" + pass + + +class Alias(object): + VENDOR = "Unknown" + MODEL = "Unknown" + VARIANT = "" + + +class Radio(Alias): + """Base class for all Radio drivers""" + BAUD_RATE = 9600 + HARDWARE_FLOW = False + ALIASES = [] + NEEDS_COMPAT_SERIAL = True + + def status_fn(self, status): + """Deliver @status to the UI""" + console_status(status) + + def __init__(self, pipe): + self.errors = [] + self.pipe = pipe + + def get_features(self): + """Return a RadioFeatures object for this radio""" + return RadioFeatures() + + @classmethod + def get_name(cls): + """Return a printable name for this radio""" + return "%s %s" % (cls.VENDOR, cls.MODEL) + + @classmethod + def get_prompts(cls): + """Return a set of strings for use in prompts""" + return RadioPrompts() + + def set_pipe(self, pipe): + """Set the serial object to be used for communications""" + self.pipe = pipe + + def get_memory(self, number): + """Return a Memory object for the memory at location @number""" + pass + + def erase_memory(self, number): + """Erase memory at location @number""" + mem = Memory() + mem.number = number + mem.empty = True + self.set_memory(mem) + + def get_memories(self, lo=None, hi=None): + """Get all the memories between @lo and @hi""" + pass + + def set_memory(self, memory): + """Set the memory object @memory""" + pass + + def get_mapping_models(self): + """Returns a list of MappingModel objects (or an empty list)""" + if hasattr(self, "get_bank_model"): + # FIXME: Backwards compatibility for old bank models + bank_model = self.get_bank_model() + if bank_model: + return [bank_model] + return [] + + def get_raw_memory(self, number): + """Return a raw string describing the memory at @number""" + pass + + def filter_name(self, name): + """Filter @name to just the length and characters supported""" + rf = self.get_features() + if rf.valid_characters == rf.valid_characters.upper(): + # Radio only supports uppercase, so help out here + name = name.upper() + return "".join([x for x in name[:rf.valid_name_length] + if x in rf.valid_characters]) + + def get_sub_devices(self): + """Return a list of sub-device Radio objects, if + RadioFeatures.has_sub_devices is True""" + return [] + + def validate_memory(self, mem): + """Return a list of warnings and errors that will be encoundered + if trying to set @mem on the current radio""" + rf = self.get_features() + return rf.validate_memory(mem) + + def get_settings(self): + """Returns a RadioSettings list containing one or more + RadioSettingGroup or RadioSetting objects. These represent general + setting knobs and dials that can be adjusted on the radio. If this + function is implemented, the has_settings RadioFeatures flag should + be True and set_settings() must be implemented as well.""" + pass + + def set_settings(self, settings): + """Accepts the top-level RadioSettingGroup returned from get_settings() + and adjusts the values in the radio accordingly. This function expects + the entire RadioSettingGroup hierarchy returned from get_settings(). + If this function is implemented, the has_settings RadioFeatures flag + should be True and get_settings() must be implemented as well.""" + pass + + +class FileBackedRadio(Radio): + """A file-backed radio stores its data in a file""" + FILE_EXTENSION = "img" + MAGIC = b'\x00\xffchirp\xeeimg\x00\x01' + + def __init__(self, *args, **kwargs): + Radio.__init__(self, *args, **kwargs) + self._memobj = None + self._metadata = {} + + def save(self, filename): + """Save the radio's memory map to @filename""" + self.save_mmap(filename) + + def load(self, filename): + """Load the radio's memory map object from @filename""" + self.load_mmap(filename) + + def process_mmap(self): + """Process a newly-loaded or downloaded memory map""" + pass + + @classmethod + def _strip_metadata(cls, raw_data): + try: + idx = raw_data.index(cls.MAGIC) + except ValueError: + LOG.debug('Image data has no metadata blob') + return raw_data, {} + + # Find the beginning of the base64 blob + raw_metadata = raw_data[idx + len(cls.MAGIC):] + metadata = {} + try: + metadata = json.loads(base64.b64decode(raw_metadata).decode()) + except ValueError as e: + LOG.error('Failed to parse decoded metadata blob: %s' % e) + except TypeError as e: + LOG.error('Failed to decode metadata blob: %s' % e) + + if metadata: + LOG.debug('Loaded metadata: %s' % metadata) + + return raw_data[:idx], metadata + + @classmethod + def _make_metadata(cls): + return base64.b64encode(json.dumps( + {'rclass': cls.__name__, + 'vendor': cls.VENDOR, + 'model': cls.MODEL, + 'variant': cls.VARIANT, + 'chirp_version': CHIRP_VERSION, + }).encode()) + + def load_mmap(self, filename): + """Load the radio's memory map from @filename""" + mapfile = open(filename, "rb") + data = mapfile.read() + if self.MAGIC in data: + data, self._metadata = self._strip_metadata(data) + if ('chirp_version' in self._metadata and + is_version_newer(self._metadata.get('chirp_version'))): + LOG.warning('Image is from version %s but we are %s' % ( + self._metadata.get('chirp_version'), CHIRP_VERSION)) + if self.NEEDS_COMPAT_SERIAL: + self._mmap = memmap.MemoryMap(data) + else: + self._mmap = memmap.MemoryMapBytes(bytes(data)) + mapfile.close() + self.process_mmap() + + def save_mmap(self, filename): + """ + try to open a file and write to it + If IOError raise a File Access Error Exception + """ + try: + mapfile = open(filename, "wb") + mapfile.write(self._mmap.get_byte_compatible().get_packed()) + if filename.lower().endswith(".img"): + mapfile.write(self.MAGIC) + mapfile.write(self._make_metadata()) + mapfile.close() + except IOError: + raise Exception("File Access Error") + + def get_mmap(self): + """Return the radio's memory map object""" + return self._mmap + + @property + def metadata(self): + return dict(self._metadata) + + @metadata.setter + def metadata(self, value): + self._metadata.update(values) + + +class CloneModeRadio(FileBackedRadio): + """A clone-mode radio does a full memory dump in and out and we store + an image of the radio into an image file""" + + _memsize = 0 + + def __init__(self, pipe): + self.errors = [] + self._mmap = None + + if isinstance(pipe, str): + self.pipe = None + self.load_mmap(pipe) + elif isinstance(pipe, memmap.MemoryMap): + self.pipe = None + self._mmap = pipe + self.process_mmap() + else: + FileBackedRadio.__init__(self, pipe) + + def get_memsize(self): + """Return the radio's memory size""" + return self._memsize + + @classmethod + def match_model(cls, filedata, filename): + """Given contents of a stored file (@filedata), return True if + this radio driver handles the represented model""" + + # Unless the radio driver does something smarter, claim + # support if the data is the same size as our memory. + # Ideally, each radio would perform an intelligent analysis to + # make this determination to avoid model conflicts with + # memories of the same size. + return len(filedata) == cls._memsize + + def sync_in(self): + "Initiate a radio-to-PC clone operation" + pass + + def sync_out(self): + "Initiate a PC-to-radio clone operation" + pass + + +class LiveRadio(Radio): + """Base class for all Live-Mode radios""" + pass + + +class NetworkSourceRadio(Radio): + """Base class for all radios based on a network source""" + + def do_fetch(self): + """Fetch the source data from the network""" + pass + + +class IcomDstarSupport: + """Base interface for radios supporting Icom's D-STAR technology""" + MYCALL_LIMIT = (1, 1) + URCALL_LIMIT = (1, 1) + RPTCALL_LIMIT = (1, 1) + + def get_urcall_list(self): + """Return a list of URCALL callsigns""" + return [] + + def get_repeater_call_list(self): + """Return a list of RPTCALL callsigns""" + return [] + + def get_mycall_list(self): + """Return a list of MYCALL callsigns""" + return [] + + def set_urcall_list(self, calls): + """Set the URCALL callsign list""" + pass + + def set_repeater_call_list(self, calls): + """Set the RPTCALL callsign list""" + pass + + def set_mycall_list(self, calls): + """Set the MYCALL callsign list""" + pass + + +class ExperimentalRadio: + """Interface for experimental radios""" + @classmethod + def get_experimental_warning(cls): + return ("This radio's driver is marked as experimental and may " + + "be unstable or unsafe to use.") + + +class Status: + """Clone status object for conveying clone progress to the UI""" + name = "Job" + msg = "Unknown" + max = 100 + cur = 0 + + def __str__(self): + try: + pct = (self.cur / float(self.max)) * 100 + nticks = int(pct) // 10 + ticks = "=" * nticks + except ValueError: + pct = 0.0 + ticks = "?" * 10 + + return "|%-10s| %2.1f%% %s" % (ticks, pct, self.msg) + + +def is_fractional_step(freq): + """Returns True if @freq requires a 12.5kHz or 6.25kHz step""" + return not is_5_0(freq) and (is_12_5(freq) or is_6_25(freq)) + + +def is_5_0(freq): + """Returns True if @freq is reachable by a 5kHz step""" + return (freq % 5000) == 0 + + +def is_12_5(freq): + """Returns True if @freq is reachable by a 12.5kHz step""" + return (freq % 12500) == 0 + + +def is_6_25(freq): + """Returns True if @freq is reachable by a 6.25kHz step""" + return (freq % 6250) == 0 + + +def is_2_5(freq): + """Returns True if @freq is reachable by a 2.5kHz step""" + return (freq % 2500) == 0 + + +def is_8_33(freq): + """Returns True if @freq is reachable by a 8.33kHz step""" + return (freq % 25000) in [0, 8330, 16660] + + +def required_step(freq): + """Returns the simplest tuning step that is required to reach @freq""" + if is_5_0(freq): + return 5.0 + elif is_12_5(freq): + return 12.5 + elif is_6_25(freq): + return 6.25 + elif is_2_5(freq): + return 2.5 + elif is_8_33(freq): + return 8.33 + else: + raise errors.InvalidDataError("Unable to calculate the required " + + "tuning step for %i.%5i" % + (freq / 1000000, freq % 1000000)) + + +def fix_rounded_step(freq): + """Some radios imply the last bit of 12.5kHz and 6.25kHz step + frequencies. Take the base @freq and return the corrected one""" + try: + required_step(freq) + return freq + except errors.InvalidDataError: + pass + + try: + required_step(freq + 500) + return freq + 500 + except errors.InvalidDataError: + pass + + try: + required_step(freq + 250) + return freq + 250 + except errors.InvalidDataError: + pass + + try: + required_step(freq + 750) + return float(freq + 750) + except errors.InvalidDataError: + pass + + try: + required_step(freq + 330) + return float(freq + 330) + except errors.InvalidDataError: + pass + + try: + required_step(freq + 660) + return float(freq + 660) + except errors.InvalidDataError: + pass + + raise errors.InvalidDataError("Unable to correct rounded frequency " + + format_freq(freq)) + + +def _name(name, len, just_upper): + """Justify @name to @len, optionally converting to all uppercase""" + if just_upper: + name = name.upper() + return name.ljust(len)[:len] + + +def name6(name, just_upper=True): + """6-char name""" + return _name(name, 6, just_upper) + + +def name8(name, just_upper=False): + """8-char name""" + return _name(name, 8, just_upper) + + +def name16(name, just_upper=False): + """16-char name""" + return _name(name, 16, just_upper) + + +def to_GHz(val): + """Convert @val in GHz to Hz""" + return val * 1000000000 + + +def to_MHz(val): + """Convert @val in MHz to Hz""" + return val * 1000000 + + +def to_kHz(val): + """Convert @val in kHz to Hz""" + return val * 1000 + + +def from_GHz(val): + """Convert @val in Hz to GHz""" + return val // 100000000 + + +def from_MHz(val): + """Convert @val in Hz to MHz""" + return val // 100000 + + +def from_kHz(val): + """Convert @val in Hz to kHz""" + return val // 100 + + +def split_tone_decode(mem, txtone, rxtone): + """ + Set tone mode and values on @mem based on txtone and rxtone specs like: + None, None, None + "Tone", 123.0, None + "DTCS", 23, "N" + """ + txmode, txval, txpol = txtone + rxmode, rxval, rxpol = rxtone + + mem.dtcs_polarity = "%s%s" % (txpol or "N", rxpol or "N") + + if not txmode and not rxmode: + # No tone + return + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + mem.rtone = txval + return + + if txmode == rxmode == "Tone" and txval == rxval: + # TX and RX same tone -> TSQL + mem.tmode = "TSQL" + mem.ctone = txval + return + + if txmode == rxmode == "DTCS" and txval == rxval: + mem.tmode = "DTCS" + mem.dtcs = txval + return + + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode or "", rxmode or "") + + if txmode == "Tone": + mem.rtone = txval + elif txmode == "DTCS": + mem.dtcs = txval + + if rxmode == "Tone": + mem.ctone = rxval + elif rxmode == "DTCS": + mem.rx_dtcs = rxval + + +def split_tone_encode(mem): + """ + Returns TX, RX tone specs based on @mem like: + None, None, None + "Tone", 123.0, None + "DTCS", 23, "N" + """ + + txmode = '' + rxmode = '' + txval = None + rxval = None + + if mem.tmode == "Tone": + txmode = "Tone" + txval = mem.rtone + elif mem.tmode == "TSQL": + txmode = rxmode = "Tone" + txval = rxval = mem.ctone + elif mem.tmode == "DTCS": + txmode = rxmode = "DTCS" + txval = rxval = mem.dtcs + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + txval = mem.rtone + elif txmode == "DTCS": + txval = mem.dtcs + if rxmode == "Tone": + rxval = mem.ctone + elif rxmode == "DTCS": + rxval = mem.rx_dtcs + + if txmode == "DTCS": + txpol = mem.dtcs_polarity[0] + else: + txpol = None + if rxmode == "DTCS": + rxpol = mem.dtcs_polarity[1] + else: + rxpol = None + + return ((txmode, txval, txpol), + (rxmode, rxval, rxpol)) + + +def sanitize_string(astring, validcharset=CHARSET_ASCII, replacechar='*'): + myfilter = ''.join( + [ + [replacechar, chr(x)][chr(x) in validcharset] + for x in range(256) + ]) + return astring.translate(myfilter) + + +def is_version_newer(version): + """Return True if version is newer than ours""" + + def get_version(v): + if v.startswith('daily-'): + _, stamp = v.split('-', 1) + ver = (int(stamp),) + elif '.' in v: + ver = tuple(int(p) for p in v.split('.')) + else: + ver = (0,) + LOG.debug('Parsed version %r to %r' % (v, ver)) + return ver + + from chirp import CHIRP_VERSION + + try: + version = get_version(version) + except ValueError as e: + LOG.error('Failed to parse version %r: %s' % (version, e)) + version = (0,) + try: + my_version = get_version(CHIRP_VERSION) + except ValueError as e: + LOG.error('Failed to parse my version %r: %s' % (CHIRP_VERSION, e)) + my_version = (0,) + + return version > my_version + + +def http_user_agent(): + ver = sys.version_info + return 'chirp/%s (Python %i.%i.%i on %s)' % ( + CHIRP_VERSION, + ver.major, ver.minor, ver.micro, + sys.platform) + + +def urlretrieve(url, fn): + """Grab an URL and save it in a specified file""" + + standard_library.install_aliases() + import urllib.request + import urllib.error + + headers = { + 'User-Agent': http_user_agent(), + } + req = urllib.request.Request(url, headers=headers) + resp = urllib.request.urlopen(req) + with open(fn, 'wb') as f: + f.write(resp.read()) diff --git a/chirp/detect.py b/chirp/detect.py new file mode 100644 index 0000000..ffa9024 --- /dev/null +++ b/chirp/detect.py @@ -0,0 +1,108 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import serial +import logging + +from chirp import errors, directory +from chirp.drivers import ic9x_ll, icf, kenwood_live, icomciv + +LOG = logging.getLogger(__name__) + + +def _icom_model_data_to_rclass(md): + for _rtype, rclass in list(directory.DRV_TO_RADIO.items()): + if rclass.VENDOR != "Icom": + continue + if not hasattr(rclass, 'get_model') or not rclass.get_model(): + continue + if rclass.get_model()[:4] == md[:4]: + return rclass + + raise errors.RadioError("Unknown radio type %02x%02x%02x%02x" % + (ord(md[0]), ord(md[1]), ord(md[2]), ord(md[3]))) + + +def _detect_icom_radio(ser): + # ICOM VHF/UHF Clone-type radios @ 9600 baud + + try: + ser.baudrate = 9600 + md = icf.get_model_data(ser) + return _icom_model_data_to_rclass(md) + except errors.RadioError as e: + LOG.error("_detect_icom_radio: %s", e) + + # ICOM IC-91/92 Live-mode radios @ 4800/38400 baud + + ser.baudrate = 4800 + try: + ic9x_ll.send_magic(ser) + return _icom_model_data_to_rclass("ic9x") + except errors.RadioError: + pass + + # ICOM CI/V Radios @ various bauds + + for rate in [9600, 4800, 19200]: + try: + ser.baudrate = rate + return icomciv.probe_model(ser) + except errors.RadioError: + pass + + ser.close() + + raise errors.RadioError("Unable to get radio model") + + +def detect_icom_radio(port): + """Detect which Icom model is connected to @port""" + ser = serial.Serial(port=port, timeout=0.5) + + try: + result = _detect_icom_radio(ser) + except Exception: + ser.close() + raise + + ser.close() + + LOG.info("Auto-detected %s %s on %s" % + (result.VENDOR, result.MODEL, port)) + + return result + + +def detect_kenwoodlive_radio(port): + """Detect which Kenwood model is connected to @port""" + ser = serial.Serial(port=port, baudrate=9600, timeout=0.5) + r_id = kenwood_live.get_id(ser) + ser.close() + + models = {} + for rclass in list(directory.DRV_TO_RADIO.values()): + if rclass.VENDOR == "Kenwood": + models[rclass.MODEL] = rclass + + if r_id in list(models.keys()): + return models[r_id] + else: + raise errors.RadioError("Unsupported model `%s'" % r_id) + +DETECT_FUNCTIONS = { + "Icom": detect_icom_radio, + "Kenwood": detect_kenwoodlive_radio, +} diff --git a/chirp/directory.py b/chirp/directory.py new file mode 100644 index 0000000..f79f23d --- /dev/null +++ b/chirp/directory.py @@ -0,0 +1,229 @@ +# Copyright 2010 Dan Smith +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import glob +import os +import tempfile +import logging +import sys + +import six + +from chirp.drivers import icf # , rfinder +from chirp import chirp_common, util, radioreference, errors + +LOG = logging.getLogger(__name__) + + +def radio_class_id(cls): + """Return a unique identification string for @cls""" + ident = "%s_%s" % (cls.VENDOR, cls.MODEL) + if cls.VARIANT: + ident += "_%s" % cls.VARIANT + ident = ident.replace("/", "_") + ident = ident.replace(" ", "_") + ident = ident.replace("(", "") + ident = ident.replace(")", "") + return ident + + +ALLOW_DUPS = False + + +def enable_reregistrations(): + """Set the global flag ALLOW_DUPS=True, which will enable a driver + to re-register for a slot in the directory without triggering an + exception""" + global ALLOW_DUPS + if not ALLOW_DUPS: + LOG.info("driver re-registration enabled") + ALLOW_DUPS = True + + +def register(cls): + """Register radio @cls with the directory""" + global DRV_TO_RADIO + ident = radio_class_id(cls) + if ident in list(DRV_TO_RADIO.keys()): + if ALLOW_DUPS: + LOG.warn("Replacing existing driver id `%s'" % ident) + else: + raise Exception("Duplicate radio driver id `%s'" % ident) + DRV_TO_RADIO[ident] = cls + RADIO_TO_DRV[cls] = ident + LOG.info("Registered %s = %s" % (ident, cls.__name__)) + + return cls + + +DRV_TO_RADIO = {} +RADIO_TO_DRV = {} + + +def get_radio(driver): + """Get radio driver class by identification string""" + if driver in DRV_TO_RADIO: + return DRV_TO_RADIO[driver] + else: + raise Exception("Unknown radio type `%s'" % driver) + + +def get_driver(rclass): + """Get the identification string for a given class""" + if rclass in RADIO_TO_DRV: + return RADIO_TO_DRV[rclass] + elif rclass.__bases__[0] in RADIO_TO_DRV: + return RADIO_TO_DRV[rclass.__bases__[0]] + else: + raise Exception("Unknown radio type `%s'" % rclass) + + +def icf_to_image(icf_file, img_file): + # FIXME: Why is this here? + """Convert an ICF file to a .img file""" + mdata, mmap = icf.read_file(icf_file) + img_data = None + + for model in list(DRV_TO_RADIO.values()): + try: + if model._model == mdata: + img_data = mmap.get_packed()[:model._memsize] + break + except Exception: + pass # Skip non-Icoms + + if img_data: + f = file(img_file, "wb") + f.write(img_data) + f.close() + else: + LOG.error("Unsupported model data: %s" % util.hexprint(mdata)) + raise Exception("Unsupported model") + + +def get_radio_by_image(image_file): + """Attempt to get the radio class that owns @image_file""" + if image_file.startswith("radioreference://"): + _, _, zipcode, username, password = image_file.split("/", 4) + rr = radioreference.RadioReferenceRadio(None) + rr.set_params(zipcode, username, password) + return rr + + # FIXME: Disable rfinder until the module is fixed + if image_file.startswith("rfinder://") and False: + _, _, email, passwd, lat, lon, miles = image_file.split("/") + rf = rfinder.RFinderRadio(None) + rf.set_params((float(lat), float(lon)), int(miles), email, passwd) + return rf + + if os.path.exists(image_file) and icf.is_icf_file(image_file): + tempf = tempfile.mktemp() + icf_to_image(image_file, tempf) + LOG.info("Auto-converted %s -> %s" % (image_file, tempf)) + image_file = tempf + + if os.path.exists(image_file): + with open(image_file, "rb") as f: + filedata = f.read() + else: + filedata = b"" + + data, metadata = chirp_common.FileBackedRadio._strip_metadata(filedata) + + # NOTE: See warning below + if six.PY3: + filestring = ''.join(chr(c) for c in filedata) + else: + filestring = filedata + + for rclass in list(DRV_TO_RADIO.values()): + if not issubclass(rclass, chirp_common.FileBackedRadio): + continue + + if not metadata: + # If no metadata, we do the old thing + error = None + try: + if rclass.match_model(filedata, image_file): + return rclass(image_file) + except Exception as e: + error = e + + # NOTE: For compatibility, try a straight up conversion to + # string and log a warning + if six.PY3: + try: + if rclass.match_model(filestring, image_file): + LOG.warning(('Radio driver %s needs py3 ' + 'match_model conversion!') % ( + rclass.__name__)) + return rclass(image_file) + except Exception as e: + error = e + + if error: + LOG.error('Radio class %s failed during detection: %s' % ( + rclass.__name__, error)) + + # If metadata, then it has to match one of the aliases or the parent + for alias in rclass.ALIASES + [rclass]: + if (alias.VENDOR == metadata.get('vendor') and + alias.MODEL == metadata.get('model')): + + class DynamicRadioAlias(rclass): + _orig_rclass = rclass + VENDOR = metadata.get('vendor') + MODEL = metadata.get('model') + VARIANT = metadata.get('variant') + + return DynamicRadioAlias(image_file) + + if metadata: + e = errors.ImageMetadataInvalidModel("Unsupported model %s %s" % ( + metadata.get("vendor"), metadata.get("model"))) + e.metadata = metadata + raise e + else: + raise errors.ImageDetectFailed("Unknown file format") + + +def safe_import_drivers(limit=None): + if sys.platform == 'win32': + # Assume we are in a frozen win32 build, so we can not glob + # the driver files, but we do not need to anyway + import chirp.drivers + for module in chirp.drivers.__all__: + try: + __import__('chirp.drivers.%s' % module) + except Exception as e: + print('Failed to import %s: %s' % (module, e)) + return + + # Safe import of everything in chirp/drivers. We need to import them + # to get them to register, but should not abort if one import fails + chirp_module_base = os.path.dirname(os.path.abspath(__file__)) + driver_files = glob.glob(os.path.join(chirp_module_base, + 'drivers', + '*.py')) + for driver_file in driver_files: + module, ext = os.path.splitext(driver_file) + driver_module = os.path.basename(module) + if limit and driver_module not in limit: + continue + try: + __import__('chirp.drivers.%s' % driver_module) + except Exception as e: + print('Failed to import %s: %s' % (module, e)) diff --git a/chirp/dmrmarc.py b/chirp/dmrmarc.py new file mode 100644 index 0000000..d66328f --- /dev/null +++ b/chirp/dmrmarc.py @@ -0,0 +1,139 @@ +# Copyright 2016 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +import logging +import tempfile +import urllib +from chirp import chirp_common, errors +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList + +LOG = logging.getLogger(__name__) + + +def list_filter(haystack, attr, needles): + if not needles or not needles[0]: + return haystack + return [x for x in haystack if x[attr] in needles] + + +class DMRMARCRadio(chirp_common.NetworkSourceRadio): + """DMR-MARC data source""" + VENDOR = "DMR-MARC" + MODEL = "Repeater database" + + URL = "http://www.dmr-marc.net/cgi-bin/trbo-database/datadump.cgi?" \ + "table=repeaters&format=json" + + def __init__(self, *args, **kwargs): + chirp_common.NetworkSourceRadio.__init__(self, *args, **kwargs) + self._repeaters = None + + def set_params(self, city, state, country): + """Set the parameters to be used for a query""" + self._city = city and [x.strip() for x in city.split(",")] or [''] + self._state = state and [x.strip() for x in state.split(",")] or [''] + self._country = country and [x.strip() for x in country.split(",")] \ + or [''] + + def do_fetch(self): + fn = tempfile.mktemp(".json") + filename, headers = urllib.urlretrieve(self.URL, fn) + with open(fn, 'r') as f: + try: + self._repeaters = json.load(f)['repeaters'] + except AttributeError: + raise errors.RadioError( + "Unexpected response from %s" % self.URL) + except ValueError as e: + raise errors.RadioError( + "Invalid JSON from %s. %s" % (self.URL, str(e))) + + self._repeaters = list_filter(self._repeaters, "city", self._city) + self._repeaters = list_filter(self._repeaters, "state", self._state) + self._repeaters = list_filter(self._repeaters, "country", + self._country) + + def get_features(self): + if not self._repeaters: + self.do_fetch() + + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, len(self._repeaters)-1) + rf.has_bank = False + rf.has_comment = True + rf.has_ctone = False + rf.valid_tmodes = [""] + return rf + + def get_raw_memory(self, number): + return repr(self._repeaters[number]) + + def get_memory(self, number): + if not self._repeaters: + self.do_fetch() + + repeater = self._repeaters[number] + + mem = chirp_common.Memory() + mem.number = number + + mem.name = repeater.get('city') + mem.freq = chirp_common.parse_freq(repeater.get('frequency')) + offset = chirp_common.parse_freq(repeater.get('offset', '0')) + if offset > 0: + mem.duplex = "+" + elif offset < 0: + mem.duplex = "-" + else: + mem.duplex = "" + mem.offset = abs(offset) + mem.mode = 'DMR' + mem.comment = repeater.get('map_info') + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting( + "color_code", "Color Code", RadioSettingValueList( + range(16), int(repeater.get('color_code', 0)))) + mem.extra.append(rs) + + return mem + + +def main(): + import argparse + from pprint import PrettyPrinter + + parser = argparse.ArgumentParser(description="Fetch DMR-MARC repeater " + "database and filter by city, state, and/or country. Multiple items " + "combined with a , will be filtered with logical OR.") + parser.add_argument("-c", "--city", + help="Comma-separated list of cities to include in output.") + parser.add_argument("-s", "--state", + help="Comma-separated list of states to include in output.") + parser.add_argument("--country", + help="Comma-separated list of countries to include in output.") + args = parser.parse_args() + + dmrmarc = DMRMARCRadio(None) + dmrmarc.set_params(**vars(args)) + dmrmarc.do_fetch() + pp = PrettyPrinter(indent=2) + pp.pprint(dmrmarc._repeaters) + +if __name__ == "__main__": + main() diff --git a/chirp/drivers/__init__.py b/chirp/drivers/__init__.py new file mode 100644 index 0000000..ea9dd2c --- /dev/null +++ b/chirp/drivers/__init__.py @@ -0,0 +1,10 @@ +import os +import sys +from glob import glob + +module_dir = os.path.dirname(sys.modules["chirp.drivers"].__file__) +__all__ = [] +for i in sorted(glob(os.path.join(module_dir, "*.py"))): + name = os.path.basename(i)[:-3] + if not name.startswith("__"): + __all__.append(name) diff --git a/chirp/drivers/__pycache__/__init__.cpython-37.pyc b/chirp/drivers/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..ba88cab Binary files /dev/null and b/chirp/drivers/__pycache__/__init__.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/__init__.cpython-38.pyc b/chirp/drivers/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..062aaf7 Binary files /dev/null and b/chirp/drivers/__pycache__/__init__.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/baofeng_wp970i.cpython-37.pyc b/chirp/drivers/__pycache__/baofeng_wp970i.cpython-37.pyc new file mode 100644 index 0000000..7ea9369 Binary files /dev/null and b/chirp/drivers/__pycache__/baofeng_wp970i.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/baofeng_wp970i.cpython-38.pyc b/chirp/drivers/__pycache__/baofeng_wp970i.cpython-38.pyc new file mode 100644 index 0000000..48bb95f Binary files /dev/null and b/chirp/drivers/__pycache__/baofeng_wp970i.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/boblov_x3plus.cpython-37.pyc b/chirp/drivers/__pycache__/boblov_x3plus.cpython-37.pyc new file mode 100644 index 0000000..6ef5de2 Binary files /dev/null and b/chirp/drivers/__pycache__/boblov_x3plus.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/boblov_x3plus.cpython-38.pyc b/chirp/drivers/__pycache__/boblov_x3plus.cpython-38.pyc new file mode 100644 index 0000000..9ad1656 Binary files /dev/null and b/chirp/drivers/__pycache__/boblov_x3plus.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/btech.cpython-37.pyc b/chirp/drivers/__pycache__/btech.cpython-37.pyc new file mode 100644 index 0000000..30ad516 Binary files /dev/null and b/chirp/drivers/__pycache__/btech.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/btech.cpython-38.pyc b/chirp/drivers/__pycache__/btech.cpython-38.pyc new file mode 100644 index 0000000..187b687 Binary files /dev/null and b/chirp/drivers/__pycache__/btech.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft1500m.cpython-37.pyc b/chirp/drivers/__pycache__/ft1500m.cpython-37.pyc new file mode 100644 index 0000000..bf58309 Binary files /dev/null and b/chirp/drivers/__pycache__/ft1500m.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft1500m.cpython-38.pyc b/chirp/drivers/__pycache__/ft1500m.cpython-38.pyc new file mode 100644 index 0000000..82b3dcd Binary files /dev/null and b/chirp/drivers/__pycache__/ft1500m.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft1802.cpython-37.pyc b/chirp/drivers/__pycache__/ft1802.cpython-37.pyc new file mode 100644 index 0000000..c1131b6 Binary files /dev/null and b/chirp/drivers/__pycache__/ft1802.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft1802.cpython-38.pyc b/chirp/drivers/__pycache__/ft1802.cpython-38.pyc new file mode 100644 index 0000000..940e490 Binary files /dev/null and b/chirp/drivers/__pycache__/ft1802.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft2d.cpython-37.pyc b/chirp/drivers/__pycache__/ft2d.cpython-37.pyc new file mode 100644 index 0000000..7c66413 Binary files /dev/null and b/chirp/drivers/__pycache__/ft2d.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft2d.cpython-38.pyc b/chirp/drivers/__pycache__/ft2d.cpython-38.pyc new file mode 100644 index 0000000..e07a727 Binary files /dev/null and b/chirp/drivers/__pycache__/ft2d.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft4.cpython-37.pyc b/chirp/drivers/__pycache__/ft4.cpython-37.pyc new file mode 100644 index 0000000..aef8ddd Binary files /dev/null and b/chirp/drivers/__pycache__/ft4.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft4.cpython-38.pyc b/chirp/drivers/__pycache__/ft4.cpython-38.pyc new file mode 100644 index 0000000..672647d Binary files /dev/null and b/chirp/drivers/__pycache__/ft4.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft7800.cpython-37.pyc b/chirp/drivers/__pycache__/ft7800.cpython-37.pyc new file mode 100644 index 0000000..09d7ca3 Binary files /dev/null and b/chirp/drivers/__pycache__/ft7800.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft7800.cpython-38.pyc b/chirp/drivers/__pycache__/ft7800.cpython-38.pyc new file mode 100644 index 0000000..032656d Binary files /dev/null and b/chirp/drivers/__pycache__/ft7800.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft817.cpython-37.pyc b/chirp/drivers/__pycache__/ft817.cpython-37.pyc new file mode 100644 index 0000000..069221b Binary files /dev/null and b/chirp/drivers/__pycache__/ft817.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft817.cpython-38.pyc b/chirp/drivers/__pycache__/ft817.cpython-38.pyc new file mode 100644 index 0000000..2f8f6c0 Binary files /dev/null and b/chirp/drivers/__pycache__/ft817.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft818.cpython-37.pyc b/chirp/drivers/__pycache__/ft818.cpython-37.pyc new file mode 100644 index 0000000..1e32223 Binary files /dev/null and b/chirp/drivers/__pycache__/ft818.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft818.cpython-38.pyc b/chirp/drivers/__pycache__/ft818.cpython-38.pyc new file mode 100644 index 0000000..5efb50d Binary files /dev/null and b/chirp/drivers/__pycache__/ft818.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ft857.cpython-37.pyc b/chirp/drivers/__pycache__/ft857.cpython-37.pyc new file mode 100644 index 0000000..72926eb Binary files /dev/null and b/chirp/drivers/__pycache__/ft857.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ft857.cpython-38.pyc b/chirp/drivers/__pycache__/ft857.cpython-38.pyc new file mode 100644 index 0000000..d1d6249 Binary files /dev/null and b/chirp/drivers/__pycache__/ft857.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ftm3200d.cpython-37.pyc b/chirp/drivers/__pycache__/ftm3200d.cpython-37.pyc new file mode 100644 index 0000000..de6de5b Binary files /dev/null and b/chirp/drivers/__pycache__/ftm3200d.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ftm3200d.cpython-38.pyc b/chirp/drivers/__pycache__/ftm3200d.cpython-38.pyc new file mode 100644 index 0000000..bc4b646 Binary files /dev/null and b/chirp/drivers/__pycache__/ftm3200d.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/generic_csv.cpython-37.pyc b/chirp/drivers/__pycache__/generic_csv.cpython-37.pyc new file mode 100644 index 0000000..3e33643 Binary files /dev/null and b/chirp/drivers/__pycache__/generic_csv.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/generic_csv.cpython-38.pyc b/chirp/drivers/__pycache__/generic_csv.cpython-38.pyc new file mode 100644 index 0000000..fb61b9f Binary files /dev/null and b/chirp/drivers/__pycache__/generic_csv.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/generic_tpe.cpython-37.pyc b/chirp/drivers/__pycache__/generic_tpe.cpython-37.pyc new file mode 100644 index 0000000..48edd10 Binary files /dev/null and b/chirp/drivers/__pycache__/generic_tpe.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/generic_tpe.cpython-38.pyc b/chirp/drivers/__pycache__/generic_tpe.cpython-38.pyc new file mode 100644 index 0000000..abbe5f1 Binary files /dev/null and b/chirp/drivers/__pycache__/generic_tpe.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/gmrsuv1.cpython-37.pyc b/chirp/drivers/__pycache__/gmrsuv1.cpython-37.pyc new file mode 100644 index 0000000..3187db0 Binary files /dev/null and b/chirp/drivers/__pycache__/gmrsuv1.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/gmrsuv1.cpython-38.pyc b/chirp/drivers/__pycache__/gmrsuv1.cpython-38.pyc new file mode 100644 index 0000000..f80c004 Binary files /dev/null and b/chirp/drivers/__pycache__/gmrsuv1.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/h777.cpython-37.pyc b/chirp/drivers/__pycache__/h777.cpython-37.pyc new file mode 100644 index 0000000..b1720f2 Binary files /dev/null and b/chirp/drivers/__pycache__/h777.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/h777.cpython-38.pyc b/chirp/drivers/__pycache__/h777.cpython-38.pyc new file mode 100644 index 0000000..4a37b04 Binary files /dev/null and b/chirp/drivers/__pycache__/h777.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/hobbypcb.cpython-37.pyc b/chirp/drivers/__pycache__/hobbypcb.cpython-37.pyc new file mode 100644 index 0000000..4ea7be6 Binary files /dev/null and b/chirp/drivers/__pycache__/hobbypcb.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/hobbypcb.cpython-38.pyc b/chirp/drivers/__pycache__/hobbypcb.cpython-38.pyc new file mode 100644 index 0000000..4c014fd Binary files /dev/null and b/chirp/drivers/__pycache__/hobbypcb.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic208.cpython-37.pyc b/chirp/drivers/__pycache__/ic208.cpython-37.pyc new file mode 100644 index 0000000..de8960e Binary files /dev/null and b/chirp/drivers/__pycache__/ic208.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic208.cpython-38.pyc b/chirp/drivers/__pycache__/ic208.cpython-38.pyc new file mode 100644 index 0000000..28e6846 Binary files /dev/null and b/chirp/drivers/__pycache__/ic208.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic2100.cpython-37.pyc b/chirp/drivers/__pycache__/ic2100.cpython-37.pyc new file mode 100644 index 0000000..8478e03 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2100.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic2100.cpython-38.pyc b/chirp/drivers/__pycache__/ic2100.cpython-38.pyc new file mode 100644 index 0000000..bacedec Binary files /dev/null and b/chirp/drivers/__pycache__/ic2100.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic2200.cpython-37.pyc b/chirp/drivers/__pycache__/ic2200.cpython-37.pyc new file mode 100644 index 0000000..c06f6f0 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2200.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic2200.cpython-38.pyc b/chirp/drivers/__pycache__/ic2200.cpython-38.pyc new file mode 100644 index 0000000..d266319 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2200.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic2300.cpython-37.pyc b/chirp/drivers/__pycache__/ic2300.cpython-37.pyc new file mode 100644 index 0000000..b423cd2 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2300.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic2300.cpython-38.pyc b/chirp/drivers/__pycache__/ic2300.cpython-38.pyc new file mode 100644 index 0000000..2ab404b Binary files /dev/null and b/chirp/drivers/__pycache__/ic2300.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic2720.cpython-37.pyc b/chirp/drivers/__pycache__/ic2720.cpython-37.pyc new file mode 100644 index 0000000..88ab0b2 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2720.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic2720.cpython-38.pyc b/chirp/drivers/__pycache__/ic2720.cpython-38.pyc new file mode 100644 index 0000000..554fa8f Binary files /dev/null and b/chirp/drivers/__pycache__/ic2720.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic2730.cpython-37.pyc b/chirp/drivers/__pycache__/ic2730.cpython-37.pyc new file mode 100644 index 0000000..d5611f3 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2730.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic2730.cpython-38.pyc b/chirp/drivers/__pycache__/ic2730.cpython-38.pyc new file mode 100644 index 0000000..d733af0 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2730.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic2820.cpython-37.pyc b/chirp/drivers/__pycache__/ic2820.cpython-37.pyc new file mode 100644 index 0000000..07ca55c Binary files /dev/null and b/chirp/drivers/__pycache__/ic2820.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic2820.cpython-38.pyc b/chirp/drivers/__pycache__/ic2820.cpython-38.pyc new file mode 100644 index 0000000..c9ef490 Binary files /dev/null and b/chirp/drivers/__pycache__/ic2820.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x.cpython-37.pyc b/chirp/drivers/__pycache__/ic9x.cpython-37.pyc new file mode 100644 index 0000000..402f5fc Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x.cpython-38.pyc b/chirp/drivers/__pycache__/ic9x.cpython-38.pyc new file mode 100644 index 0000000..38cf2ae Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x_icf.cpython-37.pyc b/chirp/drivers/__pycache__/ic9x_icf.cpython-37.pyc new file mode 100644 index 0000000..561ea7b Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x_icf.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x_icf.cpython-38.pyc b/chirp/drivers/__pycache__/ic9x_icf.cpython-38.pyc new file mode 100644 index 0000000..bae9ec9 Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x_icf.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x_icf_ll.cpython-37.pyc b/chirp/drivers/__pycache__/ic9x_icf_ll.cpython-37.pyc new file mode 100644 index 0000000..7abac8e Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x_icf_ll.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x_icf_ll.cpython-38.pyc b/chirp/drivers/__pycache__/ic9x_icf_ll.cpython-38.pyc new file mode 100644 index 0000000..0a69587 Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x_icf_ll.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x_ll.cpython-37.pyc b/chirp/drivers/__pycache__/ic9x_ll.cpython-37.pyc new file mode 100644 index 0000000..af4da98 Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x_ll.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ic9x_ll.cpython-38.pyc b/chirp/drivers/__pycache__/ic9x_ll.cpython-38.pyc new file mode 100644 index 0000000..2ba3257 Binary files /dev/null and b/chirp/drivers/__pycache__/ic9x_ll.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icf.cpython-37.pyc b/chirp/drivers/__pycache__/icf.cpython-37.pyc new file mode 100644 index 0000000..10cf53e Binary files /dev/null and b/chirp/drivers/__pycache__/icf.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icf.cpython-38.pyc b/chirp/drivers/__pycache__/icf.cpython-38.pyc new file mode 100644 index 0000000..7eab442 Binary files /dev/null and b/chirp/drivers/__pycache__/icf.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icomciv.cpython-37.pyc b/chirp/drivers/__pycache__/icomciv.cpython-37.pyc new file mode 100644 index 0000000..026b187 Binary files /dev/null and b/chirp/drivers/__pycache__/icomciv.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icomciv.cpython-38.pyc b/chirp/drivers/__pycache__/icomciv.cpython-38.pyc new file mode 100644 index 0000000..4bba314 Binary files /dev/null and b/chirp/drivers/__pycache__/icomciv.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icp7.cpython-37.pyc b/chirp/drivers/__pycache__/icp7.cpython-37.pyc new file mode 100644 index 0000000..2f50b07 Binary files /dev/null and b/chirp/drivers/__pycache__/icp7.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icp7.cpython-38.pyc b/chirp/drivers/__pycache__/icp7.cpython-38.pyc new file mode 100644 index 0000000..a535000 Binary files /dev/null and b/chirp/drivers/__pycache__/icp7.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icq7.cpython-37.pyc b/chirp/drivers/__pycache__/icq7.cpython-37.pyc new file mode 100644 index 0000000..796632f Binary files /dev/null and b/chirp/drivers/__pycache__/icq7.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icq7.cpython-38.pyc b/chirp/drivers/__pycache__/icq7.cpython-38.pyc new file mode 100644 index 0000000..06148e3 Binary files /dev/null and b/chirp/drivers/__pycache__/icq7.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ict70.cpython-37.pyc b/chirp/drivers/__pycache__/ict70.cpython-37.pyc new file mode 100644 index 0000000..77c6c8e Binary files /dev/null and b/chirp/drivers/__pycache__/ict70.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ict70.cpython-38.pyc b/chirp/drivers/__pycache__/ict70.cpython-38.pyc new file mode 100644 index 0000000..1aa2ce1 Binary files /dev/null and b/chirp/drivers/__pycache__/ict70.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ict7h.cpython-37.pyc b/chirp/drivers/__pycache__/ict7h.cpython-37.pyc new file mode 100644 index 0000000..48444d9 Binary files /dev/null and b/chirp/drivers/__pycache__/ict7h.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ict7h.cpython-38.pyc b/chirp/drivers/__pycache__/ict7h.cpython-38.pyc new file mode 100644 index 0000000..12faaf4 Binary files /dev/null and b/chirp/drivers/__pycache__/ict7h.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ict8.cpython-37.pyc b/chirp/drivers/__pycache__/ict8.cpython-37.pyc new file mode 100644 index 0000000..da71ac7 Binary files /dev/null and b/chirp/drivers/__pycache__/ict8.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ict8.cpython-38.pyc b/chirp/drivers/__pycache__/ict8.cpython-38.pyc new file mode 100644 index 0000000..e2add62 Binary files /dev/null and b/chirp/drivers/__pycache__/ict8.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icw32.cpython-37.pyc b/chirp/drivers/__pycache__/icw32.cpython-37.pyc new file mode 100644 index 0000000..08aadeb Binary files /dev/null and b/chirp/drivers/__pycache__/icw32.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icw32.cpython-38.pyc b/chirp/drivers/__pycache__/icw32.cpython-38.pyc new file mode 100644 index 0000000..db19a0f Binary files /dev/null and b/chirp/drivers/__pycache__/icw32.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icx8x.cpython-37.pyc b/chirp/drivers/__pycache__/icx8x.cpython-37.pyc new file mode 100644 index 0000000..b7593aa Binary files /dev/null and b/chirp/drivers/__pycache__/icx8x.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icx8x.cpython-38.pyc b/chirp/drivers/__pycache__/icx8x.cpython-38.pyc new file mode 100644 index 0000000..29566c3 Binary files /dev/null and b/chirp/drivers/__pycache__/icx8x.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/icx8x_ll.cpython-37.pyc b/chirp/drivers/__pycache__/icx8x_ll.cpython-37.pyc new file mode 100644 index 0000000..b188a31 Binary files /dev/null and b/chirp/drivers/__pycache__/icx8x_ll.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/icx8x_ll.cpython-38.pyc b/chirp/drivers/__pycache__/icx8x_ll.cpython-38.pyc new file mode 100644 index 0000000..26e0772 Binary files /dev/null and b/chirp/drivers/__pycache__/icx8x_ll.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/id31.cpython-37.pyc b/chirp/drivers/__pycache__/id31.cpython-37.pyc new file mode 100644 index 0000000..e2a813c Binary files /dev/null and b/chirp/drivers/__pycache__/id31.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/id31.cpython-38.pyc b/chirp/drivers/__pycache__/id31.cpython-38.pyc new file mode 100644 index 0000000..51cd71a Binary files /dev/null and b/chirp/drivers/__pycache__/id31.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/id51.cpython-37.pyc b/chirp/drivers/__pycache__/id51.cpython-37.pyc new file mode 100644 index 0000000..7be4db4 Binary files /dev/null and b/chirp/drivers/__pycache__/id51.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/id51.cpython-38.pyc b/chirp/drivers/__pycache__/id51.cpython-38.pyc new file mode 100644 index 0000000..5c5f593 Binary files /dev/null and b/chirp/drivers/__pycache__/id51.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/id51plus.cpython-37.pyc b/chirp/drivers/__pycache__/id51plus.cpython-37.pyc new file mode 100644 index 0000000..6fcb258 Binary files /dev/null and b/chirp/drivers/__pycache__/id51plus.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/id51plus.cpython-38.pyc b/chirp/drivers/__pycache__/id51plus.cpython-38.pyc new file mode 100644 index 0000000..ff19281 Binary files /dev/null and b/chirp/drivers/__pycache__/id51plus.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/id800.cpython-37.pyc b/chirp/drivers/__pycache__/id800.cpython-37.pyc new file mode 100644 index 0000000..369b2ea Binary files /dev/null and b/chirp/drivers/__pycache__/id800.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/id800.cpython-38.pyc b/chirp/drivers/__pycache__/id800.cpython-38.pyc new file mode 100644 index 0000000..b30d4e8 Binary files /dev/null and b/chirp/drivers/__pycache__/id800.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/id880.cpython-37.pyc b/chirp/drivers/__pycache__/id880.cpython-37.pyc new file mode 100644 index 0000000..af6f2ab Binary files /dev/null and b/chirp/drivers/__pycache__/id880.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/id880.cpython-38.pyc b/chirp/drivers/__pycache__/id880.cpython-38.pyc new file mode 100644 index 0000000..b78b9c9 Binary files /dev/null and b/chirp/drivers/__pycache__/id880.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/idrp.cpython-37.pyc b/chirp/drivers/__pycache__/idrp.cpython-37.pyc new file mode 100644 index 0000000..3b29aa8 Binary files /dev/null and b/chirp/drivers/__pycache__/idrp.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/idrp.cpython-38.pyc b/chirp/drivers/__pycache__/idrp.cpython-38.pyc new file mode 100644 index 0000000..2af2554 Binary files /dev/null and b/chirp/drivers/__pycache__/idrp.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/kenwood_hmk.cpython-37.pyc b/chirp/drivers/__pycache__/kenwood_hmk.cpython-37.pyc new file mode 100644 index 0000000..643ede0 Binary files /dev/null and b/chirp/drivers/__pycache__/kenwood_hmk.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/kenwood_hmk.cpython-38.pyc b/chirp/drivers/__pycache__/kenwood_hmk.cpython-38.pyc new file mode 100644 index 0000000..8439fb3 Binary files /dev/null and b/chirp/drivers/__pycache__/kenwood_hmk.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/kenwood_itm.cpython-37.pyc b/chirp/drivers/__pycache__/kenwood_itm.cpython-37.pyc new file mode 100644 index 0000000..ece08f2 Binary files /dev/null and b/chirp/drivers/__pycache__/kenwood_itm.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/kenwood_itm.cpython-38.pyc b/chirp/drivers/__pycache__/kenwood_itm.cpython-38.pyc new file mode 100644 index 0000000..b61a146 Binary files /dev/null and b/chirp/drivers/__pycache__/kenwood_itm.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/kenwood_live.cpython-37.pyc b/chirp/drivers/__pycache__/kenwood_live.cpython-37.pyc new file mode 100644 index 0000000..a40ad44 Binary files /dev/null and b/chirp/drivers/__pycache__/kenwood_live.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/kenwood_live.cpython-38.pyc b/chirp/drivers/__pycache__/kenwood_live.cpython-38.pyc new file mode 100644 index 0000000..24cf3d3 Binary files /dev/null and b/chirp/drivers/__pycache__/kenwood_live.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/mursv1.cpython-37.pyc b/chirp/drivers/__pycache__/mursv1.cpython-37.pyc new file mode 100644 index 0000000..62d311d Binary files /dev/null and b/chirp/drivers/__pycache__/mursv1.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/mursv1.cpython-38.pyc b/chirp/drivers/__pycache__/mursv1.cpython-38.pyc new file mode 100644 index 0000000..885783f Binary files /dev/null and b/chirp/drivers/__pycache__/mursv1.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/puxing_px888k.cpython-37.pyc b/chirp/drivers/__pycache__/puxing_px888k.cpython-37.pyc new file mode 100644 index 0000000..e8a4628 Binary files /dev/null and b/chirp/drivers/__pycache__/puxing_px888k.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/puxing_px888k.cpython-38.pyc b/chirp/drivers/__pycache__/puxing_px888k.cpython-38.pyc new file mode 100644 index 0000000..158feeb Binary files /dev/null and b/chirp/drivers/__pycache__/puxing_px888k.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/repeaterbook.cpython-37.pyc b/chirp/drivers/__pycache__/repeaterbook.cpython-37.pyc new file mode 100644 index 0000000..f389b5d Binary files /dev/null and b/chirp/drivers/__pycache__/repeaterbook.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/repeaterbook.cpython-38.pyc b/chirp/drivers/__pycache__/repeaterbook.cpython-38.pyc new file mode 100644 index 0000000..e881a29 Binary files /dev/null and b/chirp/drivers/__pycache__/repeaterbook.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/template.cpython-37.pyc b/chirp/drivers/__pycache__/template.cpython-37.pyc new file mode 100644 index 0000000..2e52258 Binary files /dev/null and b/chirp/drivers/__pycache__/template.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/template.cpython-38.pyc b/chirp/drivers/__pycache__/template.cpython-38.pyc new file mode 100644 index 0000000..1ef93ca Binary files /dev/null and b/chirp/drivers/__pycache__/template.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/th350.cpython-37.pyc b/chirp/drivers/__pycache__/th350.cpython-37.pyc new file mode 100644 index 0000000..98690ae Binary files /dev/null and b/chirp/drivers/__pycache__/th350.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/th350.cpython-38.pyc b/chirp/drivers/__pycache__/th350.cpython-38.pyc new file mode 100644 index 0000000..3fad72e Binary files /dev/null and b/chirp/drivers/__pycache__/th350.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/th_uv3r.cpython-37.pyc b/chirp/drivers/__pycache__/th_uv3r.cpython-37.pyc new file mode 100644 index 0000000..78260d4 Binary files /dev/null and b/chirp/drivers/__pycache__/th_uv3r.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/th_uv3r.cpython-38.pyc b/chirp/drivers/__pycache__/th_uv3r.cpython-38.pyc new file mode 100644 index 0000000..1d95bea Binary files /dev/null and b/chirp/drivers/__pycache__/th_uv3r.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/th_uv3r25.cpython-37.pyc b/chirp/drivers/__pycache__/th_uv3r25.cpython-37.pyc new file mode 100644 index 0000000..c1a036f Binary files /dev/null and b/chirp/drivers/__pycache__/th_uv3r25.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/th_uv3r25.cpython-38.pyc b/chirp/drivers/__pycache__/th_uv3r25.cpython-38.pyc new file mode 100644 index 0000000..0947432 Binary files /dev/null and b/chirp/drivers/__pycache__/th_uv3r25.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/th_uvf8d.cpython-37.pyc b/chirp/drivers/__pycache__/th_uvf8d.cpython-37.pyc new file mode 100644 index 0000000..89a06c2 Binary files /dev/null and b/chirp/drivers/__pycache__/th_uvf8d.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/th_uvf8d.cpython-38.pyc b/chirp/drivers/__pycache__/th_uvf8d.cpython-38.pyc new file mode 100644 index 0000000..b581d79 Binary files /dev/null and b/chirp/drivers/__pycache__/th_uvf8d.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/tk270.cpython-37.pyc b/chirp/drivers/__pycache__/tk270.cpython-37.pyc new file mode 100644 index 0000000..b97c12d Binary files /dev/null and b/chirp/drivers/__pycache__/tk270.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/tk270.cpython-38.pyc b/chirp/drivers/__pycache__/tk270.cpython-38.pyc new file mode 100644 index 0000000..4f9508c Binary files /dev/null and b/chirp/drivers/__pycache__/tk270.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/tk760.cpython-37.pyc b/chirp/drivers/__pycache__/tk760.cpython-37.pyc new file mode 100644 index 0000000..6c98dd3 Binary files /dev/null and b/chirp/drivers/__pycache__/tk760.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/tk760.cpython-38.pyc b/chirp/drivers/__pycache__/tk760.cpython-38.pyc new file mode 100644 index 0000000..71ed883 Binary files /dev/null and b/chirp/drivers/__pycache__/tk760.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/tk8102.cpython-37.pyc b/chirp/drivers/__pycache__/tk8102.cpython-37.pyc new file mode 100644 index 0000000..4e8230c Binary files /dev/null and b/chirp/drivers/__pycache__/tk8102.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/tk8102.cpython-38.pyc b/chirp/drivers/__pycache__/tk8102.cpython-38.pyc new file mode 100644 index 0000000..0c10ab8 Binary files /dev/null and b/chirp/drivers/__pycache__/tk8102.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/tk8180.cpython-37.pyc b/chirp/drivers/__pycache__/tk8180.cpython-37.pyc new file mode 100644 index 0000000..15e3566 Binary files /dev/null and b/chirp/drivers/__pycache__/tk8180.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/tk8180.cpython-38.pyc b/chirp/drivers/__pycache__/tk8180.cpython-38.pyc new file mode 100644 index 0000000..a8053b5 Binary files /dev/null and b/chirp/drivers/__pycache__/tk8180.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/tmv71.cpython-37.pyc b/chirp/drivers/__pycache__/tmv71.cpython-37.pyc new file mode 100644 index 0000000..6f02f34 Binary files /dev/null and b/chirp/drivers/__pycache__/tmv71.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/tmv71.cpython-38.pyc b/chirp/drivers/__pycache__/tmv71.cpython-38.pyc new file mode 100644 index 0000000..f008d30 Binary files /dev/null and b/chirp/drivers/__pycache__/tmv71.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/tmv71_ll.cpython-37.pyc b/chirp/drivers/__pycache__/tmv71_ll.cpython-37.pyc new file mode 100644 index 0000000..9c8aa44 Binary files /dev/null and b/chirp/drivers/__pycache__/tmv71_ll.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/tmv71_ll.cpython-38.pyc b/chirp/drivers/__pycache__/tmv71_ll.cpython-38.pyc new file mode 100644 index 0000000..4d9332e Binary files /dev/null and b/chirp/drivers/__pycache__/tmv71_ll.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/ts850.cpython-37.pyc b/chirp/drivers/__pycache__/ts850.cpython-37.pyc new file mode 100644 index 0000000..a1d8a80 Binary files /dev/null and b/chirp/drivers/__pycache__/ts850.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/ts850.cpython-38.pyc b/chirp/drivers/__pycache__/ts850.cpython-38.pyc new file mode 100644 index 0000000..b807f74 Binary files /dev/null and b/chirp/drivers/__pycache__/ts850.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/uv5r.cpython-37.pyc b/chirp/drivers/__pycache__/uv5r.cpython-37.pyc new file mode 100644 index 0000000..14deeab Binary files /dev/null and b/chirp/drivers/__pycache__/uv5r.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/uv5r.cpython-38.pyc b/chirp/drivers/__pycache__/uv5r.cpython-38.pyc new file mode 100644 index 0000000..a712050 Binary files /dev/null and b/chirp/drivers/__pycache__/uv5r.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/uv5x3.cpython-37.pyc b/chirp/drivers/__pycache__/uv5x3.cpython-37.pyc new file mode 100644 index 0000000..6a6c8c5 Binary files /dev/null and b/chirp/drivers/__pycache__/uv5x3.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/uv5x3.cpython-38.pyc b/chirp/drivers/__pycache__/uv5x3.cpython-38.pyc new file mode 100644 index 0000000..4081cac Binary files /dev/null and b/chirp/drivers/__pycache__/uv5x3.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/uv6r.cpython-37.pyc b/chirp/drivers/__pycache__/uv6r.cpython-37.pyc new file mode 100644 index 0000000..e23bd9a Binary files /dev/null and b/chirp/drivers/__pycache__/uv6r.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/uv6r.cpython-38.pyc b/chirp/drivers/__pycache__/uv6r.cpython-38.pyc new file mode 100644 index 0000000..f0508db Binary files /dev/null and b/chirp/drivers/__pycache__/uv6r.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/uvb5.cpython-37.pyc b/chirp/drivers/__pycache__/uvb5.cpython-37.pyc new file mode 100644 index 0000000..c4ba329 Binary files /dev/null and b/chirp/drivers/__pycache__/uvb5.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/uvb5.cpython-38.pyc b/chirp/drivers/__pycache__/uvb5.cpython-38.pyc new file mode 100644 index 0000000..2836a04 Binary files /dev/null and b/chirp/drivers/__pycache__/uvb5.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx170.cpython-37.pyc b/chirp/drivers/__pycache__/vx170.cpython-37.pyc new file mode 100644 index 0000000..0097dd5 Binary files /dev/null and b/chirp/drivers/__pycache__/vx170.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx170.cpython-38.pyc b/chirp/drivers/__pycache__/vx170.cpython-38.pyc new file mode 100644 index 0000000..26703a6 Binary files /dev/null and b/chirp/drivers/__pycache__/vx170.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx2.cpython-37.pyc b/chirp/drivers/__pycache__/vx2.cpython-37.pyc new file mode 100644 index 0000000..f831e03 Binary files /dev/null and b/chirp/drivers/__pycache__/vx2.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx2.cpython-38.pyc b/chirp/drivers/__pycache__/vx2.cpython-38.pyc new file mode 100644 index 0000000..2b94212 Binary files /dev/null and b/chirp/drivers/__pycache__/vx2.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx3.cpython-37.pyc b/chirp/drivers/__pycache__/vx3.cpython-37.pyc new file mode 100644 index 0000000..fd2563c Binary files /dev/null and b/chirp/drivers/__pycache__/vx3.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx3.cpython-38.pyc b/chirp/drivers/__pycache__/vx3.cpython-38.pyc new file mode 100644 index 0000000..2819427 Binary files /dev/null and b/chirp/drivers/__pycache__/vx3.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx5.cpython-37.pyc b/chirp/drivers/__pycache__/vx5.cpython-37.pyc new file mode 100644 index 0000000..bb77a19 Binary files /dev/null and b/chirp/drivers/__pycache__/vx5.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx5.cpython-38.pyc b/chirp/drivers/__pycache__/vx5.cpython-38.pyc new file mode 100644 index 0000000..58f2c81 Binary files /dev/null and b/chirp/drivers/__pycache__/vx5.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx510.cpython-37.pyc b/chirp/drivers/__pycache__/vx510.cpython-37.pyc new file mode 100644 index 0000000..bdf6c16 Binary files /dev/null and b/chirp/drivers/__pycache__/vx510.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx510.cpython-38.pyc b/chirp/drivers/__pycache__/vx510.cpython-38.pyc new file mode 100644 index 0000000..f711fff Binary files /dev/null and b/chirp/drivers/__pycache__/vx510.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx6.cpython-37.pyc b/chirp/drivers/__pycache__/vx6.cpython-37.pyc new file mode 100644 index 0000000..1953be9 Binary files /dev/null and b/chirp/drivers/__pycache__/vx6.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx6.cpython-38.pyc b/chirp/drivers/__pycache__/vx6.cpython-38.pyc new file mode 100644 index 0000000..b718375 Binary files /dev/null and b/chirp/drivers/__pycache__/vx6.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx7.cpython-37.pyc b/chirp/drivers/__pycache__/vx7.cpython-37.pyc new file mode 100644 index 0000000..d7576e1 Binary files /dev/null and b/chirp/drivers/__pycache__/vx7.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx7.cpython-38.pyc b/chirp/drivers/__pycache__/vx7.cpython-38.pyc new file mode 100644 index 0000000..fb432e4 Binary files /dev/null and b/chirp/drivers/__pycache__/vx7.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/vx8.cpython-37.pyc b/chirp/drivers/__pycache__/vx8.cpython-37.pyc new file mode 100644 index 0000000..59a79ea Binary files /dev/null and b/chirp/drivers/__pycache__/vx8.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/vx8.cpython-38.pyc b/chirp/drivers/__pycache__/vx8.cpython-38.pyc new file mode 100644 index 0000000..4096e3f Binary files /dev/null and b/chirp/drivers/__pycache__/vx8.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/wouxun_common.cpython-37.pyc b/chirp/drivers/__pycache__/wouxun_common.cpython-37.pyc new file mode 100644 index 0000000..9fe612f Binary files /dev/null and b/chirp/drivers/__pycache__/wouxun_common.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/wouxun_common.cpython-38.pyc b/chirp/drivers/__pycache__/wouxun_common.cpython-38.pyc new file mode 100644 index 0000000..b2f6c66 Binary files /dev/null and b/chirp/drivers/__pycache__/wouxun_common.cpython-38.pyc differ diff --git a/chirp/drivers/__pycache__/yaesu_clone.cpython-37.pyc b/chirp/drivers/__pycache__/yaesu_clone.cpython-37.pyc new file mode 100644 index 0000000..3c7a3e3 Binary files /dev/null and b/chirp/drivers/__pycache__/yaesu_clone.cpython-37.pyc differ diff --git a/chirp/drivers/__pycache__/yaesu_clone.cpython-38.pyc b/chirp/drivers/__pycache__/yaesu_clone.cpython-38.pyc new file mode 100644 index 0000000..d19bf83 Binary files /dev/null and b/chirp/drivers/__pycache__/yaesu_clone.cpython-38.pyc differ diff --git a/chirp/drivers/alinco.py b/chirp/drivers/alinco.py new file mode 100644 index 0000000..54d8ce3 --- /dev/null +++ b/chirp/drivers/alinco.py @@ -0,0 +1,868 @@ +# Copyright 2011 Dan Smith +# 2016 Matt Weyland +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, bitwise, memmap, errors, directory, util +from chirp.settings import RadioSettingGroup, RadioSetting +from chirp.settings import RadioSettingValueBoolean, RadioSettings + +from textwrap import dedent + +import time +import logging + +LOG = logging.getLogger(__name__) + + +DRX35_MEM_FORMAT = """ +#seekto 0x0120; +u8 used_flags[25]; + +#seekto 0x0200; +struct { + u8 new_used:1, + unknown1:1, + isnarrow:1, + isdigital:1, + ishigh:1, + unknown2:3; + u8 unknown3:6, + duplex:2; + u8 unknown4:4, + tmode:4; + u8 unknown5:4, + step:4; + bbcd freq[4]; + u8 unknown6[1]; + bbcd offset[3]; + u8 rtone; + u8 ctone; + u8 dtcs_tx; + u8 dtcs_rx; + u8 name[7]; + u8 unknown8[2]; + u8 unknown9:6, + power:2; + u8 unknownA[6]; +} memory[100]; + +#seekto 0x0130; +u8 skips[25]; +""" + +# 0000 0111 +# 0000 0010 + +# Response length is: +# 1. \r\n +# 2. Four-digit address, followed by a colon +# 3. 16 bytes in hex (32 characters) +# 4. \r\n +RLENGTH = 2 + 5 + 32 + 2 + +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0] + + +def isascii(data): + for byte in data: + if (ord(byte) < ord(" ") or ord(byte) > ord("~")) and \ + byte not in "\r\n": + return False + return True + + +def tohex(data): + if isascii(data): + return repr(data) + string = "" + for byte in data: + string += "%02X" % ord(byte) + return string + + +class AlincoStyleRadio(chirp_common.CloneModeRadio): + """Base class for all known Alinco radios""" + _memsize = 0 + _model = "NONE" + + def _send(self, data): + LOG.debug("PC->R: (%2i) %s" % (len(data), tohex(data))) + self.pipe.write(data) + self.pipe.read(len(data)) + + def _read(self, length): + data = self.pipe.read(length) + LOG.debug("R->PC: (%2i) %s" % (len(data), tohex(data))) + return data + + def _download_chunk(self, addr): + if addr % 16: + raise Exception("Addr 0x%04x not on 16-byte boundary" % addr) + + cmd = "AL~F%04XR\r\n" % addr + self._send(cmd) + + resp = self._read(RLENGTH).strip() + if len(resp) == 0: + raise errors.RadioError("No response from radio") + if ":" not in resp: + raise errors.RadioError("Unexpected response from radio") + addr, _data = resp.split(":", 1) + data = "" + for i in range(0, len(_data), 2): + data += chr(int(_data[i:i+2], 16)) + + if len(data) != 16: + LOG.debug("Response was:") + LOG.debug("|%s|") + LOG.debug("Which I converted to:") + LOG.debug(util.hexprint(data)) + raise Exception("Radio returned less than 16 bytes") + + return data + + def _download(self, limit): + self._identify() + + data = "" + for addr in range(0, limit, 16): + data += self._download_chunk(addr) + time.sleep(0.1) + + if self.status_fn: + status = chirp_common.Status() + status.cur = addr + 16 + status.max = self._memsize + status.msg = "Downloading from radio" + self.status_fn(status) + + self._send("AL~E\r\n") + self._read(20) + + return memmap.MemoryMap(data) + + def _identify(self): + for _i in range(0, 3): + self._send("%s\r\n" % self._model) + resp = self._read(6) + if resp.strip() == "OK": + return True + time.sleep(1) + + return False + + def _upload_chunk(self, addr): + if addr % 16: + raise Exception("Addr 0x%04x not on 16-byte boundary" % addr) + + _data = self._mmap[addr:addr+16] + data = "".join(["%02X" % ord(x) for x in _data]) + + cmd = "AL~F%04XW%s\r\n" % (addr, data) + self._send(cmd) + + def _upload(self, limit): + if not self._identify(): + raise Exception("I can't talk to this model") + + for addr in range(0x100, limit, 16): + self._upload_chunk(addr) + time.sleep(0.1) + + if self.status_fn: + status = chirp_common.Status() + status.cur = addr + 16 + status.max = self._memsize + status.msg = "Uploading to radio" + self.status_fn(status) + + self._send("AL~E\r\n") + self.pipe._read(20) + + def process_mmap(self): + self._memobj = bitwise.parse(DRX35_MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + self._mmap = self._download(self._memsize) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + self._upload(self._memsize) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + +DUPLEX = ["", "-", "+"] +TMODES = ["", "Tone", "", "TSQL"] + [""] * 12 +TMODES[12] = "DTCS" +DCS_CODES = { + "Alinco": chirp_common.DTCS_CODES, + "Jetstream": [17] + chirp_common.DTCS_CODES, +} + +CHARSET = (["\x00"] * 0x30) + \ + [chr(x + ord("0")) for x in range(0, 10)] + \ + [chr(x + ord("A")) for x in range(0, 26)] + [" "] + \ + list("\x00" * 128) + + +def _get_name(_mem): + name = "" + for i in _mem.name: + if i in [0x00, 0xFF]: + break + name += CHARSET[i] + return name + + +def _set_name(mem, _mem): + name = [0x00] * 7 + j = 0 + for i in range(0, 7): + try: + name[j] = CHARSET.index(mem.name[i]) + j += 1 + except IndexError: + pass + except ValueError: + pass + return name + +ALINCO_TONES = list(chirp_common.TONES) +ALINCO_TONES.remove(159.8) +ALINCO_TONES.remove(165.5) +ALINCO_TONES.remove(171.3) +ALINCO_TONES.remove(177.3) +ALINCO_TONES.remove(183.5) +ALINCO_TONES.remove(189.9) +ALINCO_TONES.remove(196.6) +ALINCO_TONES.remove(199.5) +ALINCO_TONES.remove(206.5) +ALINCO_TONES.remove(229.1) +ALINCO_TONES.remove(254.1) + + +class DRx35Radio(AlincoStyleRadio): + """Base class for the DR-x35 radios""" + _range = [(118000000, 155000000)] + _power_levels = [] + _valid_tones = ALINCO_TONES + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = ["FM", "NFM"] + rf.valid_skips = ["", "S"] + rf.valid_bands = self._range + rf.memory_bounds = (0, 99) + rf.has_ctone = True + rf.has_bank = False + rf.has_dtcs_polarity = False + rf.can_delete = False + rf.valid_tuning_steps = STEPS + rf.valid_name_length = 7 + rf.valid_power_levels = self._power_levels + return rf + + def _get_used(self, number): + _usd = self._memobj.used_flags[number / 8] + bit = (0x80 >> (number % 8)) + return _usd & bit + + def _set_used(self, number, is_used): + _usd = self._memobj.used_flags[number / 8] + bit = (0x80 >> (number % 8)) + if is_used: + _usd |= bit + else: + _usd &= ~bit + + def _get_power(self, _mem): + if self._power_levels: + return self._power_levels[_mem.ishigh] + return None + + def _set_power(self, _mem, mem): + if self._power_levels: + _mem.ishigh = mem.power is None or \ + mem.power == self._power_levels[1] + + def _get_extra(self, _mem, mem): + mem.extra = RadioSettingGroup("extra", "Extra") + dig = RadioSetting("isdigital", "Digital", + RadioSettingValueBoolean(bool(_mem.isdigital))) + dig.set_doc("Digital/Packet mode enabled") + mem.extra.append(dig) + + def _set_extra(self, _mem, mem): + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _skp = self._memobj.skips[number / 8] + _usd = self._memobj.used_flags[number / 8] + bit = (0x80 >> (number % 8)) + + mem = chirp_common.Memory() + mem.number = number + if not self._get_used(number) and self.MODEL != "JT220M": + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 100 + mem.rtone = self._valid_tones[_mem.rtone] + mem.ctone = self._valid_tones[_mem.ctone] + mem.duplex = DUPLEX[_mem.duplex] + mem.offset = int(_mem.offset) * 100 + mem.tmode = TMODES[_mem.tmode] + mem.dtcs = DCS_CODES[self.VENDOR][_mem.dtcs_tx] + mem.tuning_step = STEPS[_mem.step] + + if _mem.isnarrow: + mem.mode = "NFM" + + mem.power = self._get_power(_mem) + + if _skp & bit: + mem.skip = "S" + + mem.name = _get_name(_mem).rstrip() + + self._get_extra(_mem, mem) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _skp = self._memobj.skips[mem.number / 8] + _usd = self._memobj.used_flags[mem.number / 8] + bit = (0x80 >> (mem.number % 8)) + + if self._get_used(mem.number) and not mem.empty: + # Initialize the memory + _mem.set_raw("\x00" * 32) + + self._set_used(mem.number, not mem.empty) + if mem.empty: + return + + _mem.freq = mem.freq / 100 + + try: + _tone = mem.rtone + _mem.rtone = self._valid_tones.index(mem.rtone) + _tone = mem.ctone + _mem.ctone = self._valid_tones.index(mem.ctone) + except ValueError: + raise errors.UnsupportedToneError("This radio does not support " + + "tone %.1fHz" % _tone) + + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.offset = mem.offset / 100 + _mem.tmode = TMODES.index(mem.tmode) + _mem.dtcs_tx = DCS_CODES[self.VENDOR].index(mem.dtcs) + _mem.dtcs_rx = DCS_CODES[self.VENDOR].index(mem.dtcs) + _mem.step = STEPS.index(mem.tuning_step) + + _mem.isnarrow = mem.mode == "NFM" + self._set_power(_mem, mem) + + if mem.skip: + _skp |= bit + else: + _skp &= ~bit + + _mem.name = _set_name(mem, _mem) + + self._set_extra(_mem, mem) + + +@directory.register +class DR03Radio(DRx35Radio): + """Alinco DR03""" + VENDOR = "Alinco" + MODEL = "DR03T" + + _model = "DR135" + _memsize = 4096 + _range = [(28000000, 29695000)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x64] == chr(0x00) and filedata[0x65] == chr(0x28) + + +@directory.register +class DR06Radio(DRx35Radio): + """Alinco DR06""" + VENDOR = "Alinco" + MODEL = "DR06T" + + _model = "DR435" + _memsize = 4096 + _range = [(50000000, 53995000)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x64] == chr(0x00) and filedata[0x65] == chr(0x50) + + +@directory.register +class DR135Radio(DRx35Radio): + """Alinco DR135""" + VENDOR = "Alinco" + MODEL = "DR135T" + + _model = "DR135" + _memsize = 4096 + _range = [(118000000, 173000000)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x64] == chr(0x01) and filedata[0x65] == chr(0x44) + + +@directory.register +class DR235Radio(DRx35Radio): + """Alinco DR235""" + VENDOR = "Alinco" + MODEL = "DR235T" + + _model = "DR235" + _memsize = 4096 + _range = [(216000000, 280000000)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x64] == chr(0x02) and filedata[0x65] == chr(0x22) + + +@directory.register +class DR435Radio(DRx35Radio): + """Alinco DR435""" + VENDOR = "Alinco" + MODEL = "DR435T" + + _model = "DR435" + _memsize = 4096 + _range = [(350000000, 511000000)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x64] == chr(0x04) and filedata[0x65] == chr(0x00) + + +@directory.register +class DJ596Radio(DRx35Radio): + """Alinco DJ596""" + VENDOR = "Alinco" + MODEL = "DJ596" + + _model = "DJ596" + _memsize = 4096 + _range = [(136000000, 174000000), (400000000, 511000000)] + _power_levels = [chirp_common.PowerLevel("Low", watts=1.00), + chirp_common.PowerLevel("High", watts=5.00)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x64] == chr(0x45) and filedata[0x65] == chr(0x01) + + +@directory.register +class JT220MRadio(DRx35Radio): + """Jetstream JT220""" + VENDOR = "Jetstream" + MODEL = "JT220M" + + _model = "DR136" + _memsize = 8192 + _range = [(216000000, 280000000)] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0x60:0x64] == "2009" + + +@directory.register +class DJ175Radio(DRx35Radio): + """Alinco DJ175""" + VENDOR = "Alinco" + MODEL = "DJ175" + + _model = "DJ175" + _memsize = 6896 + _range = [(136000000, 174000000), (400000000, 511000000)] + _power_levels = [ + chirp_common.PowerLevel("Low", watts=0.50), + chirp_common.PowerLevel("Mid", watts=2.00), + chirp_common.PowerLevel("High", watts=5.00), + ] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def _get_used(self, number): + return self._memobj.memory[number].new_used + + def _set_used(self, number, is_used): + self._memobj.memory[number].new_used = is_used + + def _get_power(self, _mem): + return self._power_levels[_mem.power] + + def _set_power(self, _mem, mem): + if mem.power in self._power_levels: + _mem.power = self._power_levels.index(mem.power) + + def _download_chunk(self, addr): + if addr % 16: + raise Exception("Addr 0x%04x not on 16-byte boundary" % addr) + + cmd = "AL~F%04XR\r\n" % addr + self._send(cmd) + + _data = self._read(34).strip() + if len(_data) == 0: + raise errors.RadioError("No response from radio") + + data = "" + for i in range(0, len(_data), 2): + data += chr(int(_data[i:i+2], 16)) + + if len(data) != 16: + LOG.debug("Response was:") + LOG.debug("|%s|") + LOG.debug("Which I converted to:") + LOG.debug(util.hexprint(data)) + raise Exception("Radio returned less than 16 bytes") + + return data + + +DJG7EG_MEM_FORMAT = """ +#seekto 0x200; +ul16 bank[50]; +ul16 special_bank[7]; +#seekto 0x1200; +struct { + u8 empty; + ul32 freq; + u8 mode; + u8 step; + ul32 offset; + u8 duplex; + u8 squelch_type; + u8 tx_tone; + u8 rx_tone; + u8 dcs; + ul24 unknown1; + u8 skip; + ul32 unknown2; + ul32 unknown3; + ul32 unknown4; + char name[32]; +} memory[1000]; +""" + + +@directory.register +class AlincoDJG7EG(AlincoStyleRadio): + """Alinco DJ-G7EG""" + VENDOR = "Alinco" + MODEL = "DJ-G7EG" + BAUD_RATE = 57600 + + # Those are different from the other Alinco radios. + STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, + 100.0, 125.0, 150.0, 200.0, 500.0, 1000.0] + DUPLEX = ["", "+", "-"] + MODES = ["NFM", "FM", "AM", "WFM"] + TMODES = ["", "??1", "Tone", "TSQL", "TSQL-R", "DTCS"] + + # This is a bit of a hack to avoid overwriting _identify() + _model = "AL~DJ-G7EG" + _memsize = 0x1a7c0 + _range = [(500000, 1300000000)] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Ensure your firmware version is 4_10 or higher + 2. Turn radio off + 3. Connect your interface cable + 4. Turn radio on + 5. Press and release PTT 3 times while holding MONI key + 6. Supported baud rates: 57600 (default) and 19200 + (rotate dial while holding MONI to change) + 7. Click OK + """)) + rp.pre_upload = _(dedent("""\ + 1. Ensure your firmware version is 4_10 or higher + 2. Turn radio off + 3. Connect your interface cable + 4. Turn radio on + 5. Press and release PTT 3 times while holding MONI key + 6. Supported baud rates: 57600 (default) and 19200 + (rotate dial while holding MONI to change) + 7. Click OK + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_settings = False + + rf.valid_modes = self.MODES + rf.valid_tmodes = ["", "Tone", "TSQL", "Cross", "TSQL-R", "DTCS"] + rf.valid_tuning_steps = self.STEPS + rf.valid_bands = self._range + rf.valid_skips = ["", "S"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 16 + rf.memory_bounds = (0, 999) + + return rf + + def _download_chunk(self, addr): + if addr % 0x40: + raise Exception("Addr 0x%04x not on 64-byte boundary" % addr) + + cmd = "AL~F%05XR\r" % addr + self._send(cmd) + + # Response: "\r\n[ ... data ... ]\r\n + # data is encoded in hex, hence we read two chars per byte + _data = self._read(2+2*64+2).strip() + if len(_data) == 0: + raise errors.RadioError("No response from radio") + + data = "" + for i in range(0, len(_data), 2): + data += chr(int(_data[i:i+2], 16)) + + if len(data) != 64: + LOG.debug("Response was:") + LOG.debug("|%s|") + LOG.debug("Which I converted to:") + LOG.debug(util.hexprint(data)) + raise Exception("Chunk from radio has wrong size") + + return data + + def _detect_baudrate_and_identify(self): + if self._identify(): + return True + else: + # Apparenly Alinco support suggests to try again at a lower baud + # rate if their cable fails with the default rate. See #4355. + LOG.info("Could not talk to radio. Trying again at 19200 baud") + self.pipe.baudrate = 19200 + return self._identify() + + def _download(self, limit): + self._detect_baudrate_and_identify() + + data = "\x00"*0x200 + + for addr in range(0x200, limit, 0x40): + data += self._download_chunk(addr) + # Other Alinco drivers delay here, but doesn't seem to be necessary + # for this model. + + if self.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = limit + status.msg = "Downloading from radio" + self.status_fn(status) + return memmap.MemoryMap(data) + + def _upload_chunk(self, addr): + if addr % 0x40: + raise Exception("Addr 0x%04x not on 64-byte boundary" % addr) + + _data = self._mmap[addr:addr+0x40] + data = "".join(["%02X" % ord(x) for x in _data]) + + cmd = "AL~F%05XW%s\r" % (addr, data) + self._send(cmd) + + resp = self._read(6) + if resp.strip() != "OK": + raise Exception("Unexpected response from radio: %s" % resp) + + def _upload(self, limit): + if not self._detect_baudrate_and_identify(): + raise Exception("I can't talk to this model") + + for addr in range(0x200, self._memsize, 0x40): + self._upload_chunk(addr) + # Other Alinco drivers delay here, but doesn't seem to be necessary + # for this model. + + if self.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = self._memsize + status.msg = "Uploading to radio" + self.status_fn(status) + + def _get_empty_flag(self, freq, mode): + # Returns flag used to hide a channel from the main band. This occurs + # when the mode is anything but NFM or FM (main band can only do those) + # or when the frequency is outside of the range supported by the main + # band. + if mode not in ("NFM", "FM"): + return 0x01 + if (freq >= 136000000 and freq < 174000000) or \ + (freq >= 400000000 and freq < 470000000) or \ + (freq >= 1240000000 and freq < 1300000000): + return 0x02 + else: + return 0x01 + + def _check_channel_consistency(self, number): + _mem = self._memobj.memory[number] + if _mem.empty != 0x00: + if _mem.unknown1 == 0xffffff: + # Previous versions of this code have skipped the unknown + # fields. They contain bytes of value if the channel is empty + # and thus those bytes remain 0xff when the channel is put to + # use. The radio is totally fine with this but the Alinco + # programming software is not (see #5275). Here, we check for + # this and report if it is encountered. + LOG.warning("Channel %d is inconsistent: Found 0xff in " + "non-empty channel. Touch channel to fix." + % number) + + if _mem.empty != self._get_empty_flag(_mem.freq, + self.MODES[_mem.mode]): + LOG.warning("Channel %d is inconsistent: Found out of band " + "frequency. Touch channel to fix." % number) + + def process_mmap(self): + self._memobj = bitwise.parse(DJG7EG_MEM_FORMAT, self._mmap) + # We check all channels for corruption (see bug #5275) but we don't fix + # it automatically because it would be unpolite to modify something on + # a read operation. A log message is emitted though for the user to + # take actions. + for number in range(len(self._memobj.memory)): + self._check_channel_consistency(number) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + mem = chirp_common.Memory() + mem.number = number + if _mem.empty == 0: + mem.empty = True + else: + mem.freq = int(_mem.freq) + mem.mode = self.MODES[_mem.mode] + mem.tuning_step = self.STEPS[_mem.step] + mem.offset = int(_mem.offset) + mem.duplex = self.DUPLEX[_mem.duplex] + if self.TMODES[_mem.squelch_type] == "TSQL" and \ + _mem.tx_tone != _mem.rx_tone: + mem.tmode = "Cross" + mem.cross_mode = "Tone->Tone" + else: + mem.tmode = self.TMODES[_mem.squelch_type] + mem.rtone = ALINCO_TONES[_mem.tx_tone-1] + mem.ctone = ALINCO_TONES[_mem.rx_tone-1] + mem.dtcs = DCS_CODES[self.VENDOR][_mem.dcs] + if _mem.skip: + mem.skip = "S" + # FIXME find out what every other byte is used for. Japanese? + mem.name = str(_mem.name.get_raw()[::2]).rstrip('\0') + return mem + + def set_memory(self, mem): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number] + if mem.empty: + _mem.set_raw("\xff" * (_mem.size()/8)) + _mem.empty = 0x00 + else: + _mem.empty = self._get_empty_flag(mem.freq, mem.mode) + _mem.freq = mem.freq + _mem.mode = self.MODES.index(mem.mode) + _mem.step = self.STEPS.index(mem.tuning_step) + _mem.offset = mem.offset + _mem.duplex = self.DUPLEX.index(mem.duplex) + if mem.tmode == "Cross": + _mem.squelch_type = self.TMODES.index("TSQL") + try: + _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1 + except ValueError: + raise errors.UnsupportedToneError( + "This radio does not support tone %.1fHz" % mem.rtone) + try: + _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1 + except ValueError: + raise errors.UnsupportedToneError( + "This radio does not support tone %.1fHz" % mem.ctone) + elif mem.tmode == "TSQL": + _mem.squelch_type = self.TMODES.index("TSQL") + # Note how the same TSQL tone is copied to both memory + # locaations + try: + _mem.tx_tone = ALINCO_TONES.index(mem.ctone)+1 + _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1 + except ValueError: + raise errors.UnsupportedToneError( + "This radio does not support tone %.1fHz" % mem.ctone) + else: + _mem.squelch_type = self.TMODES.index(mem.tmode) + try: + _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1 + except ValueError: + raise errors.UnsupportedToneError( + "This radio does not support tone %.1fHz" % mem.rtone) + try: + _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1 + except ValueError: + raise errors.UnsupportedToneError( + "This radio does not support tone %.1fHz" % mem.ctone) + _mem.dcs = DCS_CODES[self.VENDOR].index(mem.dtcs) + _mem.skip = (mem.skip == "S") + _mem.name = "\x00".join(mem.name).ljust(32, "\x00") + _mem.unknown1 = 0x3e001c + _mem.unknown2 = 0x0000000a + _mem.unknown3 = 0x00000000 + _mem.unknown4 = 0x00000000 diff --git a/chirp/drivers/anytone.py b/chirp/drivers/anytone.py new file mode 100644 index 0000000..e99a25d --- /dev/null +++ b/chirp/drivers/anytone.py @@ -0,0 +1,569 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import struct +import time +import logging + +from chirp import bitwise +from chirp import chirp_common +from chirp import directory +from chirp import errors +from chirp import memmap +from chirp import util +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \ + RadioSettingValueList, RadioSettingValueString, RadioSettingValueBoolean + + +LOG = logging.getLogger(__name__) + +_mem_format = """ +#seekto 0x0100; +struct { + u8 even_unknown:2, + even_pskip:1, + even_skip:1, + odd_unknown:2, + odd_pskip:1, + odd_skip:1; +} flags[379]; +""" + +mem_format = _mem_format + """ +struct memory { + bbcd freq[4]; + bbcd offset[4]; + u8 unknownA:4, + tune_step:4; + u8 rxdcsextra:1, + txdcsextra:1, + rxinv:1, + txinv:1, + channel_width:2, + unknownB:2; + u8 unknown8:3, + is_am:1, + power:2, + duplex:2; + u8 unknown4:4, + rxtmode:2, + txtmode:2; + u8 unknown5:2, + txtone:6; + u8 unknown6:2, + rxtone:6; + u8 txcode; + u8 rxcode; + u8 unknown7[2]; + u8 unknown2[5]; + char name[7]; + u8 unknownZ[2]; +}; + +#seekto 0x0030; +struct { + char serial[16]; +} serial_no; + +#seekto 0x0050; +struct { + char date[16]; +} version; + +#seekto 0x0280; +struct { + u8 unknown1:6, + display:2; + u8 unknown2[11]; + u8 unknown3:3, + apo:5; + u8 unknown4a[2]; + u8 unknown4b:6, + mute:2; + u8 unknown4; + u8 unknown5:5, + beep:1, + unknown6:2; + u8 unknown[334]; + char welcome[8]; +} settings; + +#seekto 0x0540; +struct memory memblk1[12]; + +#seekto 0x2000; +struct memory memory[758]; + +#seekto 0x7ec0; +struct memory memblk2[10]; +""" + + +class FlagObj(object): + def __init__(self, flagobj, which): + self._flagobj = flagobj + self._which = which + + def _get(self, flag): + return getattr(self._flagobj, "%s_%s" % (self._which, flag)) + + def _set(self, flag, value): + return setattr(self._flagobj, "%s_%s" % (self._which, flag), value) + + def get_skip(self): + return self._get("skip") + + def set_skip(self, value): + self._set("skip", value) + + skip = property(get_skip, set_skip) + + def get_pskip(self): + return self._get("pskip") + + def set_pskip(self, value): + self._set("pskip", value) + + pskip = property(get_pskip, set_pskip) + + def set(self): + self._set("unknown", 3) + self._set("skip", 1) + self._set("pskip", 1) + + def clear(self): + self._set("unknown", 0) + self._set("skip", 0) + self._set("pskip", 0) + + def get(self): + return (self._get("unknown") << 2 | + self._get("skip") << 1 | + self._get("pskip")) + + def __repr__(self): + return repr(self._flagobj) + + +def _is_loc_used(memobj, loc): + return memobj.flags[loc / 2].get_raw() != "\xFF" + + +def _addr_to_loc(addr): + return (addr - 0x2000) / 32 + + +def _should_send_addr(memobj, addr): + if addr < 0x2000 or addr >= 0x7EC0: + return True + else: + return _is_loc_used(memobj, _addr_to_loc(addr)) + + +def _echo_write(radio, data): + try: + radio.pipe.write(data) + radio.pipe.read(len(data)) + except Exception, e: + LOG.error("Error writing to radio: %s" % e) + raise errors.RadioError("Unable to write to radio") + + +def _read(radio, length): + try: + data = radio.pipe.read(length) + except Exception, e: + LOG.error("Error reading from radio: %s" % e) + raise errors.RadioError("Unable to read from radio") + + if len(data) != length: + LOG.error("Short read from radio (%i, expected %i)" % + (len(data), length)) + LOG.debug(util.hexprint(data)) + raise errors.RadioError("Short read from radio") + return data + +valid_model = ['QX588UV', 'HR-2040', 'DB-50M\x00', 'DB-750X'] + + +def _ident(radio): + radio.pipe.timeout = 1 + _echo_write(radio, "PROGRAM") + response = radio.pipe.read(3) + if response != "QX\x06": + LOG.debug("Response was:\n%s" % util.hexprint(response)) + raise errors.RadioError("Unsupported model") + _echo_write(radio, "\x02") + response = radio.pipe.read(16) + LOG.debug(util.hexprint(response)) + if response[1:8] not in valid_model: + LOG.debug("Response was:\n%s" % util.hexprint(response)) + raise errors.RadioError("Unsupported model") + + +def _finish(radio): + endframe = "\x45\x4E\x44" + _echo_write(radio, endframe) + result = radio.pipe.read(1) + if result != "\x06": + LOG.debug("Got:\n%s" % util.hexprint(result)) + raise errors.RadioError("Radio did not finish cleanly") + + +def _checksum(data): + cs = 0 + for byte in data: + cs += ord(byte) + return cs % 256 + + +def _send(radio, cmd, addr, length, data=None): + frame = struct.pack(">cHb", cmd, addr, length) + if data: + frame += data + frame += chr(_checksum(frame[1:])) + frame += "\x06" + _echo_write(radio, frame) + LOG.debug("Sent:\n%s" % util.hexprint(frame)) + if data: + result = radio.pipe.read(1) + if result != "\x06": + LOG.debug("Ack was: %s" % repr(result)) + raise errors.RadioError( + "Radio did not accept block at %04x" % addr) + return + result = _read(radio, length + 6) + LOG.debug("Got:\n%s" % util.hexprint(result)) + header = result[0:4] + data = result[4:-2] + ack = result[-1] + if ack != "\x06": + LOG.debug("Ack was: %s" % repr(ack)) + raise errors.RadioError("Radio NAK'd block at %04x" % addr) + _cmd, _addr, _length = struct.unpack(">cHb", header) + if _addr != addr or _length != _length: + LOG.debug("Expected/Received:") + LOG.debug(" Length: %02x/%02x" % (length, _length)) + LOG.debug(" Addr: %04x/%04x" % (addr, _addr)) + raise errors.RadioError("Radio send unexpected block") + cs = _checksum(result[1:-2]) + if cs != ord(result[-2]): + LOG.debug("Calculated: %02x" % cs) + LOG.debug("Actual: %02x" % ord(result[-2])) + raise errors.RadioError("Block at 0x%04x failed checksum" % addr) + return data + + +def _download(radio): + _ident(radio) + + memobj = None + + data = "" + for start, end in radio._ranges: + for addr in range(start, end, 0x10): + if memobj is not None and not _should_send_addr(memobj, addr): + block = "\xFF" * 0x10 + else: + block = _send(radio, 'R', addr, 0x10) + data += block + + status = chirp_common.Status() + status.cur = len(data) + status.max = end + status.msg = "Cloning from radio" + radio.status_fn(status) + + if addr == 0x19F0: + memobj = bitwise.parse(_mem_format, data) + + _finish(radio) + + return memmap.MemoryMap(data) + + +def _upload(radio): + _ident(radio) + + for start, end in radio._ranges: + for addr in range(start, end, 0x10): + if addr < 0x0100: + continue + if not _should_send_addr(radio._memobj, addr): + continue + block = radio._mmap[addr:addr + 0x10] + _send(radio, 'W', addr, len(block), block) + + status = chirp_common.Status() + status.cur = addr + status.max = end + status.msg = "Cloning to radio" + radio.status_fn(status) + + _finish(radio) + + +TONES = [62.5] + list(chirp_common.TONES) +TMODES = ['', 'Tone', 'DTCS', ''] +DUPLEXES = ['', '-', '+', ''] +MODES = ["FM", "FM", "NFM"] +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50), + chirp_common.PowerLevel("Mid1", watts=25), + chirp_common.PowerLevel("Mid2", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + + +@directory.register +class AnyTone5888UVRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """AnyTone 5888UV""" + VENDOR = "AnyTone" + MODEL = "5888UV" + BAUD_RATE = 9600 + _file_ident = "QX588UV" + + # May try to mirror the OEM behavior later + _ranges = [ + (0x0000, 0x8000), + ] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("The Anytone driver is currently experimental. " + "There are no known issues with it, but you should " + "proceed with caution.") + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_cross = True + rf.has_tuning_step = False + rf.has_rx_dtcs = True + rf.valid_skips = ["", "S", "P"] + rf.valid_modes = ["FM", "NFM", "AM"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = ['Tone->DTCS', 'DTCS->Tone', + '->Tone', '->DTCS', 'Tone->Tone'] + rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES + rf.valid_bands = [(108000000, 500000000)] + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-" + rf.valid_name_length = 7 + rf.valid_power_levels = POWER_LEVELS + rf.memory_bounds = (1, 758) + return rf + + def sync_in(self): + self._mmap = _download(self) + self.process_mmap() + + def sync_out(self): + _upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def _get_memobjs(self, number): + number -= 1 + _mem = self._memobj.memory[number] + _flg = FlagObj(self._memobj.flags[number / 2], + number % 2 and "even" or "odd") + return _mem, _flg + + def _get_dcs_index(self, _mem, which): + base = getattr(_mem, '%scode' % which) + extra = getattr(_mem, '%sdcsextra' % which) + return (int(extra) << 8) | int(base) + + def _set_dcs_index(self, _mem, which, index): + base = getattr(_mem, '%scode' % which) + extra = getattr(_mem, '%sdcsextra' % which) + base.set_value(index & 0xFF) + extra.set_value(index >> 8) + + def get_raw_memory(self, number): + _mem, _flg = self._get_memobjs(number) + return repr(_mem) + repr(_flg) + + def get_memory(self, number): + _mem, _flg = self._get_memobjs(number) + mem = chirp_common.Memory() + mem.number = number + + if _flg.get() == 0x0F: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 100 + mem.offset = int(_mem.offset) * 100 + mem.name = str(_mem.name).rstrip() + mem.duplex = DUPLEXES[_mem.duplex] + mem.mode = _mem.is_am and "AM" or MODES[_mem.channel_width] + + rxtone = txtone = None + rxmode = TMODES[_mem.rxtmode] + txmode = TMODES[_mem.txtmode] + if txmode == "Tone": + txtone = TONES[_mem.txtone] + elif txmode == "DTCS": + txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, + 'tx')] + if rxmode == "Tone": + rxtone = TONES[_mem.rxtone] + elif rxmode == "DTCS": + rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, + 'rx')] + + rxpol = _mem.rxinv and "R" or "N" + txpol = _mem.txinv and "R" or "N" + + chirp_common.split_tone_decode(mem, + (txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) + + mem.skip = _flg.get_skip() and "S" or _flg.get_pskip() and "P" or "" + mem.power = POWER_LEVELS[_mem.power] + + return mem + + def set_memory(self, mem): + _mem, _flg = self._get_memobjs(mem.number) + if mem.empty: + _flg.set() + return + _flg.clear() + _mem.set_raw("\x00" * 32) + + _mem.freq = mem.freq / 100 + _mem.offset = mem.offset / 100 + _mem.name = mem.name.ljust(7) + _mem.is_am = mem.mode == "AM" + _mem.duplex = DUPLEXES.index(mem.duplex) + + try: + _mem.channel_width = MODES.index(mem.mode) + except ValueError: + _mem.channel_width = 0 + + ((txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) + + _mem.txtmode = TMODES.index(txmode) + _mem.rxtmode = TMODES.index(rxmode) + if txmode == "Tone": + _mem.txtone = TONES.index(txtone) + elif txmode == "DTCS": + self._set_dcs_index(_mem, 'tx', + chirp_common.ALL_DTCS_CODES.index(txtone)) + if rxmode == "Tone": + _mem.rxtone = TONES.index(rxtone) + elif rxmode == "DTCS": + self._set_dcs_index(_mem, 'rx', + chirp_common.ALL_DTCS_CODES.index(rxtone)) + + _mem.txinv = txpol == "R" + _mem.rxinv = rxpol == "R" + + _flg.set_skip(mem.skip == "S") + _flg.set_pskip(mem.skip == "P") + + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + settings = RadioSettings(basic) + + display = ["Frequency", "Channel", "Name"] + rs = RadioSetting("display", "Display", + RadioSettingValueList(display, + display[_settings.display])) + basic.append(rs) + + apo = ["Off"] + ['%.1f hour(s)' % (0.5 * x) for x in range(1, 25)] + rs = RadioSetting("apo", "Automatic Power Off", + RadioSettingValueList(apo, + apo[_settings.apo])) + basic.append(rs) + + def filter(s): + s_ = "" + for i in range(0, 8): + c = str(s[i]) + s_ += (c if c in chirp_common.CHARSET_ASCII else "") + return s_ + + rs = RadioSetting("welcome", "Welcome Message", + RadioSettingValueString(0, 8, + filter(_settings.welcome))) + basic.append(rs) + + rs = RadioSetting("beep", "Beep Enabled", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + mute = ["Off", "TX", "RX", "TX/RX"] + rs = RadioSetting("mute", "Sub Band Mute", + RadioSettingValueList(mute, + mute[_settings.mute])) + basic.append(rs) + + return settings + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + name = element.get_name() + setattr(_settings, name, element.value) + + @classmethod + def match_model(cls, filedata, filename): + return cls._file_ident in filedata[0x20:0x40] + + +@directory.register +class IntekHR2040Radio(AnyTone5888UVRadio): + """Intek HR-2040""" + VENDOR = "Intek" + MODEL = "HR-2040" + _file_ident = "HR-2040" + + +@directory.register +class PolmarDB50MRadio(AnyTone5888UVRadio): + """Polmar DB-50M""" + VENDOR = "Polmar" + MODEL = "DB-50M" + _file_ident = "DB-50M" + + +@directory.register +class PowerwerxDB750XRadio(AnyTone5888UVRadio): + """Powerwerx DB-750X""" + VENDOR = "Powerwerx" + MODEL = "DB-750X" + _file_ident = "DB-750X" + + def get_settings(self): + return {} diff --git a/chirp/drivers/anytone_ht.py b/chirp/drivers/anytone_ht.py new file mode 100644 index 0000000..faa9bb8 --- /dev/null +++ b/chirp/drivers/anytone_ht.py @@ -0,0 +1,946 @@ +# Copyright 2015 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import struct +import time +import logging + +from chirp import bitwise +from chirp import chirp_common +from chirp import directory +from chirp import errors +from chirp import memmap +from chirp import util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings + +LOG = logging.getLogger(__name__) + +mem_format = """ +struct memory { + bbcd freq[4]; + bbcd offset[4]; + u8 unknown1:4, + tune_step:4; + u8 unknown2:2, + txdcsextra:1, + txinv:1, + channel_width:2, + unknown3:1, + tx_off:1; + u8 unknown4:2, + rxdcsextra:1, + rxinv:1, + power:2, + duplex:2; + u8 unknown5:4, + rxtmode:2, + txtmode:2; + u8 unknown6:2, + txtone:6; + u8 unknown7:2, + rxtone:6; + u8 txcode; + u8 rxcode; + u8 unknown8[3]; + char name[6]; + u8 squelch:4, + unknown9:2, + bcl:2; + u8 unknownA; + u8 unknownB:7, + sqlmode:1; + u8 unknownC[4]; +}; + +#seekto 0x0010; +struct { + u8 unknown1; + u8 unknown2:5, + bands1:3; + char model[7]; + u8 unknown3:5, + bands2:3; + u8 unknown4[6]; + u8 unknown5[16]; + char date[9]; + u8 unknown6[7]; + u8 unknown7[16]; + u8 unknown8[16]; + char dealer[16]; + char stockdate[9]; + u8 unknown9[7]; + char selldate[9]; + u8 unknownA[7]; + char seller[16]; +} oem_info; + +#seekto 0x0100; +u8 used_flags[50]; + +#seekto 0x0120; +u8 skip_flags[50]; + +#seekto 0x0220; +struct { + u8 unknown220:6, + display:2; + u8 unknown221:7, + upvfomr:1; + u8 unknown222:7, + dnvfomr:1; + u8 unknown223:7, + fmvfomr:1; + u8 upmrch; + u8 dnmrch; + u8 unknown226:1, + fmmrch:7; + u8 unknown227; + u8 unknown228:7, + fastscano:1; // obltr-8r only + u8 unknown229:6, + pause:2; + u8 unknown22A:5, + stop:3; + u8 unknown22B:6, + backlight:2; + u8 unknown22C:6, + color:2; + u8 unknown22D:6, + vdisplay:2; + u8 unknown22E; + u8 unknown22F:5, + pf1key:3; + u8 beep:1, + alarmoff:1, + main:1, + radio:1, + unknown230:1, + allband:1, + elimtail:1, + monikey:1; + u8 fastscan:1, // termn-8r only + keylock:1, + unknown231:2, + lwenable:1, + swenable:1, + fmenable:1, + amenable:1; + u8 unknown232:3, + tot:5; + u8 unknown233:7, + amvfomr:1; + u8 unknown234:3, + apo:5; + u8 unknown235:5, + pf2key:3; // keylock for obltr-8r + u8 unknown236; + u8 unknown237:4, + save:4; + u8 unknown238:5, + tbst:3; + u8 unknown239:4, + voxlevel:4; + u8 unknown23A:3, + voxdelay:5; + u8 unknown23B:5, + tail:3; + u8 unknown23C; + u8 unknown23D:1, + ammrch:7; + u8 unknown23E:5, + vvolume:3; + u8 unknown23F:5, + fmam:3; + u8 unknown240:4, + upmrbank:4; + u8 unknown241:7, + upwork:1; + u8 unknown242:7, + uplink:1; + u8 unknown243:4, + dnmrbank:4; + u8 unknown244:7, + dnwork:1; + u8 unknown245:7, + downlink:1; + u8 unknown246:7, + banklink1:1; + u8 unknown247:7, + banklink2:1; + u8 unknown248:7, + banklink3:1; + u8 unknown249:7, + banklink4:1; + u8 unknown24A:7, + banklink5:1; + u8 unknown24B:7, + banklink6:1; + u8 unknown24C:7, + banklink7:1; + u8 unknown24D:7, + banklink8:1; + u8 unknown24E:7, + banklink9:1; + u8 unknown24F:7, + banklink0:1; + u8 unknown250:6, + noaa:2; + u8 unknown251:5, + noaach:3; + u8 unknown252:6, + part95:2; + u8 unknown253:3, + gmrs:5; + u8 unknown254:5, + murs:3; + u8 unknown255:5, + amsql:3; +} settings; + +#seekto 0x0246; +struct { + u8 unused:7, + bank:1; +} banklink[10]; + +#seekto 0x03E0; +struct { + char line1[6]; + char line2[6]; +} welcome_msg; + +#seekto 0x2000; +struct memory memory[200]; +""" + + +def _echo_write(radio, data): + try: + radio.pipe.write(data) + except Exception, e: + LOG.error("Error writing to radio: %s" % e) + raise errors.RadioError("Unable to write to radio") + + +def _read(radio, length): + try: + data = radio.pipe.read(length) + except Exception, e: + LOG.error("Error reading from radio: %s" % e) + raise errors.RadioError("Unable to read from radio") + + if len(data) != length: + LOG.error("Short read from radio (%i, expected %i)" % + (len(data), length)) + LOG.debug(util.hexprint(data)) + raise errors.RadioError("Short read from radio") + return data + +valid_model = ['TERMN8R', 'OBLTR8R'] + + +def _ident(radio): + radio.pipe.timeout = 1 + _echo_write(radio, "PROGRAM") + response = radio.pipe.read(3) + if response != "QX\x06": + LOG.debug("Response was:\n%s" % util.hexprint(response)) + raise errors.RadioError("Radio did not respond. Check connection.") + _echo_write(radio, "\x02") + response = radio.pipe.read(16) + LOG.debug(util.hexprint(response)) + if radio._file_ident not in response: + LOG.debug("Response was:\n%s" % util.hexprint(response)) + raise errors.RadioError("Unsupported model") + + +def _finish(radio): + endframe = "\x45\x4E\x44" + _echo_write(radio, endframe) + result = radio.pipe.read(1) + if result != "\x06": + LOG.debug("Got:\n%s" % util.hexprint(result)) + raise errors.RadioError("Radio did not finish cleanly") + + +def _checksum(data): + cs = 0 + for byte in data: + cs += ord(byte) + return cs % 256 + + +def _send(radio, cmd, addr, length, data=None): + frame = struct.pack(">cHb", cmd, addr, length) + if data: + frame += data + frame += chr(_checksum(frame[1:])) + frame += "\x06" + _echo_write(radio, frame) + LOG.debug("Sent:\n%s" % util.hexprint(frame)) + if data: + result = radio.pipe.read(1) + if result != "\x06": + LOG.debug("Ack was: %s" % repr(result)) + raise errors.RadioError( + "Radio did not accept block at %04x" % addr) + return + result = _read(radio, length + 6) + LOG.debug("Got:\n%s" % util.hexprint(result)) + header = result[0:4] + data = result[4:-2] + ack = result[-1] + if ack != "\x06": + LOG.debug("Ack was: %s" % repr(ack)) + raise errors.RadioError("Radio NAK'd block at %04x" % addr) + _cmd, _addr, _length = struct.unpack(">cHb", header) + if _addr != addr or _length != _length: + LOG.debug("Expected/Received:") + LOG.debug(" Length: %02x/%02x" % (length, _length)) + LOG.debug(" Addr: %04x/%04x" % (addr, _addr)) + raise errors.RadioError("Radio send unexpected block") + cs = _checksum(result[1:-2]) + if cs != ord(result[-2]): + LOG.debug("Calculated: %02x" % cs) + LOG.debug("Actual: %02x" % ord(result[-2])) + raise errors.RadioError("Block at 0x%04x failed checksum" % addr) + return data + + +def _download(radio): + _ident(radio) + + memobj = None + + data = "" + for start, end in radio._ranges: + for addr in range(start, end, 0x10): + block = _send(radio, 'R', addr, 0x10) + data += block + + status = chirp_common.Status() + status.cur = len(data) + status.max = end + status.msg = "Cloning from radio" + radio.status_fn(status) + + _finish(radio) + + return memmap.MemoryMap(data) + + +def _upload(radio): + _ident(radio) + + for start, end in radio._ranges: + for addr in range(start, end, 0x10): + if addr < 0x0100: + continue + block = radio._mmap[addr:addr + 0x10] + _send(radio, 'W', addr, len(block), block) + + status = chirp_common.Status() + status.cur = addr + status.max = end + status.msg = "Cloning to radio" + radio.status_fn(status) + + _finish(radio) + + +APO = ['Off', '30 Minutes', '1 Hour', '2 Hours'] +BACKLIGHT = ['Off', 'On', 'Auto'] +BCLO = ['Off', 'Repeater', 'Busy'] +CHARSET = chirp_common.CHARSET_ASCII +COLOR = ['Blue', 'Orange', 'Purple'] +DISPLAY = ['Frequency', 'N/A', 'Name'] +DUPLEXES = ['', 'N/A', '-', '+', 'split', 'off'] +GMRS = ['GMRS %s' % x for x in range(1, 8)] + \ + ['GMRS %s' % x for x in range(15, 23)] + \ + ['GMRS Repeater %s' % x for x in range(15, 23)] +MAIN = ['Up', 'Down'] +MODES = ['FM', 'NFM'] +MONI = ['Squelch Off Momentarily', 'Squelch Off'] +MRBANK = ['Bank %s' % x for x in range(1, 10)] + ['Bank 0'] +MURS = ['MURS %s' % x for x in range(1, 6)] +NOAA = ['Weather Off', 'Weather On', 'Weather Alerts'] +NOAACH = ['WX %s' % x for x in range(1, 8)] +PART95 = ['Normal(Part 90)', 'GMRS(Part 95A)', 'MURS(Part 95J)'] +PAUSE = ['%s Seconds (TO)' % x for x in range(5, 20, 5)] + ['2 Seconds (CO)'] +PFKEYT = ['Off', 'VOLT', 'CALL', 'FHSS', 'SUB PTT', 'ALARM', 'MONI'] +PFKEYO = ['Off', 'VOLT', 'CALL', 'SUB PTT', 'ALARM', 'MONI'] +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5), + chirp_common.PowerLevel("Mid", watts=2), + chirp_common.PowerLevel("Low", watts=1)] +SAVE = ['Off', '1:2', '1:3', '1:5', '1:8', 'Auto'] +SQUELCH = ['%s' % x for x in range(0, 10)] +STOP = ['%s Seconds' % x for x in range(0, 4)] + ['Manual'] +TAIL = ['Off', '120 Degree', '180 Degree', '240 Degree'] +TBST = ['Off', '1750 Hz', '2100 Hz', '1000 Hz', '1450 Hz'] +TMODES = ['', 'Tone', 'DTCS', ''] +TONES = [62.5] + list(chirp_common.TONES) +TOT = ['Off'] + ['%s Seconds' % x for x in range(10, 280, 10)] +VDISPLAY = ['Frequency/Channel', 'Battery Voltage', 'Off'] +VFOMR = ["VFO", "MR"] +VOXLEVEL = ['Off'] + ['%s' % x for x in range(1, 11)] +VOXDELAY = ['%.1f Seconds' % (0.1 * x) for x in range(5, 31)] +WORKMODE = ["Channel", "Bank"] + + +@directory.register +class AnyToneTERMN8RRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """AnyTone TERMN-8R""" + VENDOR = "AnyTone" + MODEL = "TERMN-8R" + BAUD_RATE = 9600 + _file_ident = "TERMN8R" + + # May try to mirror the OEM behavior later + _ranges = [ + (0x0000, 0x8000), + ] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("The Anytone driver is currently experimental. " + "There are no known issues with it, but you should " + "proceed with caution.") + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_cross = True + rf.has_tuning_step = False + rf.has_rx_dtcs = True + rf.valid_skips = ["", "S"] + rf.valid_modes = MODES + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES + rf.valid_bands = [(136000000, 174000000), + (400000000, 520000000)] + rf.valid_characters = CHARSET + rf.valid_name_length = 6 + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = DUPLEXES + rf.can_odd_split = True + rf.memory_bounds = (0, 199) + return rf + + def sync_in(self): + self._mmap = _download(self) + self.process_mmap() + + def sync_out(self): + _upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def _get_dcs_index(self, _mem, which): + base = getattr(_mem, '%scode' % which) + extra = getattr(_mem, '%sdcsextra' % which) + return (int(extra) << 8) | int(base) + + def _set_dcs_index(self, _mem, which, index): + base = getattr(_mem, '%scode' % which) + extra = getattr(_mem, '%sdcsextra' % which) + base.set_value(index & 0xFF) + extra.set_value(index >> 8) + + def get_memory(self, number): + bitpos = (1 << (number % 8)) + bytepos = (number / 8) + + _mem = self._memobj.memory[number] + _skp = self._memobj.skip_flags[bytepos] + _usd = self._memobj.used_flags[bytepos] + + mem = chirp_common.Memory() + mem.number = number + + if _usd & bitpos: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 100 + mem.offset = int(_mem.offset) * 100 + mem.name = self.filter_name(str(_mem.name).rstrip()) + mem.duplex = DUPLEXES[_mem.duplex] + mem.mode = MODES[_mem.channel_width] + + if _mem.tx_off == True: + mem.duplex = "off" + mem.offset = 0 + + rxtone = txtone = None + rxmode = TMODES[_mem.rxtmode] + txmode = TMODES[_mem.txtmode] + if txmode == "Tone": + txtone = TONES[_mem.txtone] + elif txmode == "DTCS": + txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, + 'tx')] + if rxmode == "Tone": + rxtone = TONES[_mem.rxtone] + elif rxmode == "DTCS": + rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem, + 'rx')] + + rxpol = _mem.rxinv and "R" or "N" + txpol = _mem.txinv and "R" or "N" + + chirp_common.split_tone_decode(mem, + (txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) + + if _skp & bitpos: + mem.skip = "S" + + mem.power = POWER_LEVELS[_mem.power] + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueList(BCLO, + BCLO[_mem.bcl])) + mem.extra.append(rs) + + rs = RadioSetting("squelch", "Squelch", + RadioSettingValueList(SQUELCH, + SQUELCH[_mem.squelch])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + bitpos = (1 << (mem.number % 8)) + bytepos = (mem.number / 8) + + _mem = self._memobj.memory[mem.number] + _skp = self._memobj.skip_flags[bytepos] + _usd = self._memobj.used_flags[bytepos] + + if mem.empty: + _usd |= bitpos + _skp |= bitpos + _mem.set_raw("\xFF" * 32) + return + _usd &= ~bitpos + + if _mem.get_raw() == ("\xFF" * 32): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 32) + _mem.squelch = 3 + + _mem.freq = mem.freq / 100 + + if mem.duplex == "off": + _mem.duplex = DUPLEXES.index("") + _mem.offset = 0 + _mem.tx_off = True + elif mem.duplex == "split": + diff = mem.offset - mem.freq + _mem.duplex = DUPLEXES.index("-") if diff < 0 \ + else DUPLEXES.index("+") + _mem.offset = abs(diff) / 100 + else: + _mem.offset = mem.offset / 100 + _mem.duplex = DUPLEXES.index(mem.duplex) + + _mem.name = mem.name.ljust(6) + + try: + _mem.channel_width = MODES.index(mem.mode) + except ValueError: + _mem.channel_width = 0 + + ((txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) + + _mem.txtmode = TMODES.index(txmode) + _mem.rxtmode = TMODES.index(rxmode) + if txmode == "Tone": + _mem.txtone = TONES.index(txtone) + elif txmode == "DTCS": + self._set_dcs_index(_mem, 'tx', + chirp_common.ALL_DTCS_CODES.index(txtone)) + if rxmode == "Tone": + _mem.sqlmode = 1 + _mem.rxtone = TONES.index(rxtone) + elif rxmode == "DTCS": + _mem.sqlmode = 1 + self._set_dcs_index(_mem, 'rx', + chirp_common.ALL_DTCS_CODES.index(rxtone)) + else: + _mem.sqlmode = 0 + + _mem.txinv = txpol == "R" + _mem.rxinv = rxpol == "R" + + if mem.skip: + _skp |= bitpos + else: + _skp &= ~bitpos + + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _msg = self._memobj.welcome_msg + _oem = self._memobj.oem_info + _settings = self._memobj.settings + cfg_grp = RadioSettingGroup("cfg_grp", "Function Setup") + oem_grp = RadioSettingGroup("oem_grp", "OEM Info") + + group = RadioSettings(cfg_grp, + oem_grp) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + # + # Function Setup + # + + rs = RadioSetting("welcome_msg.line1", "Welcome Message 1", + RadioSettingValueString( + 0, 6, _filter(_msg.line1))) + cfg_grp.append(rs) + + rs = RadioSetting("welcome_msg.line2", "Welcome Message 2", + RadioSettingValueString( + 0, 6, _filter(_msg.line2))) + cfg_grp.append(rs) + + rs = RadioSetting("display", "Display Mode", + RadioSettingValueList(DISPLAY, + DISPLAY[_settings.display])) + cfg_grp.append(rs) + + rs = RadioSetting("upvfomr", "Up VFO/MR", + RadioSettingValueList(VFOMR, + VFOMR[_settings.upvfomr])) + cfg_grp.append(rs) + + rs = RadioSetting("dnvfomr", "Down VFO/MR", + RadioSettingValueList(VFOMR, + VFOMR[_settings.dnvfomr])) + cfg_grp.append(rs) + + rs = RadioSetting("upwork", "Up Work Mode", + RadioSettingValueList(WORKMODE, + WORKMODE[_settings.upwork])) + cfg_grp.append(rs) + + rs = RadioSetting("upmrbank", "Up MR Bank", + RadioSettingValueList(MRBANK, + MRBANK[_settings.upmrbank])) + cfg_grp.append(rs) + + rs = RadioSetting("upmrch", "Up MR Channel", + RadioSettingValueInteger(0, 200, _settings.upmrch)) + cfg_grp.append(rs) + + rs = RadioSetting("dnwork", "Down Work Mode", + RadioSettingValueList(WORKMODE, + WORKMODE[_settings.dnwork])) + cfg_grp.append(rs) + + rs = RadioSetting("dnmrbank", "Down MR Bank", + RadioSettingValueList(MRBANK, + MRBANK[_settings.dnmrbank])) + cfg_grp.append(rs) + + rs = RadioSetting("dnmrch", "Down MR Channel", + RadioSettingValueInteger(0, 200, _settings.dnmrch)) + cfg_grp.append(rs) + + rs = RadioSetting("main", "Main", + RadioSettingValueList(MAIN, + MAIN[_settings.main])) + cfg_grp.append(rs) + + rs = RadioSetting("pause", "Scan Pause Time", + RadioSettingValueList(PAUSE, + PAUSE[_settings.pause])) + cfg_grp.append(rs) + + rs = RadioSetting("stop", "Function Keys Stop Time", + RadioSettingValueList(STOP, + STOP[_settings.stop])) + cfg_grp.append(rs) + + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList(BACKLIGHT, + BACKLIGHT[_settings.backlight])) + cfg_grp.append(rs) + + rs = RadioSetting("color", "Backlight Color", + RadioSettingValueList(COLOR, + COLOR[_settings.color])) + cfg_grp.append(rs) + + rs = RadioSetting("vdisplay", "Vice-Machine Display", + RadioSettingValueList(VDISPLAY, + VDISPLAY[_settings.vdisplay])) + cfg_grp.append(rs) + + rs = RadioSetting("voxlevel", "Vox Level", + RadioSettingValueList(VOXLEVEL, + VOXLEVEL[_settings.voxlevel])) + cfg_grp.append(rs) + + rs = RadioSetting("voxdelay", "Vox Delay", + RadioSettingValueList(VOXDELAY, + VOXDELAY[_settings.voxdelay])) + cfg_grp.append(rs) + + rs = RadioSetting("tot", "Time Out Timer", + RadioSettingValueList(TOT, + TOT[_settings.tot])) + cfg_grp.append(rs) + + rs = RadioSetting("tbst", "Tone Burst", + RadioSettingValueList(TBST, + TBST[_settings.tbst])) + cfg_grp.append(rs) + + rs = RadioSetting("monikey", "MONI Key Function", + RadioSettingValueList(MONI, + MONI[_settings.monikey])) + cfg_grp.append(rs) + + if self.MODEL == "TERMN-8R": + rs = RadioSetting("pf1key", "PF1 Key Function", + RadioSettingValueList(PFKEYT, + PFKEYT[_settings.pf1key])) + cfg_grp.append(rs) + + rs = RadioSetting("pf2key", "PF2 Key Function", + RadioSettingValueList(PFKEYT, + PFKEYT[_settings.pf2key])) + cfg_grp.append(rs) + + if self.MODEL == "OBLTR-8R": + rs = RadioSetting("pf1key", "PF1 Key Function", + RadioSettingValueList(PFKEYO, + PFKEYO[_settings.pf1key])) + cfg_grp.append(rs) + + rs = RadioSetting("fmam", "PF2 Key Function", + RadioSettingValueList(PFKEYO, + PFKEYO[_settings.fmam])) + cfg_grp.append(rs) + + rs = RadioSetting("apo", "Automatic Power Off", + RadioSettingValueList(APO, + APO[_settings.apo])) + cfg_grp.append(rs) + + rs = RadioSetting("save", "Power Save", + RadioSettingValueList(SAVE, + SAVE[_settings.save])) + cfg_grp.append(rs) + + rs = RadioSetting("tail", "Tail Eliminator Type", + RadioSettingValueList(TAIL, + TAIL[_settings.tail])) + cfg_grp.append(rs) + + rs = RadioSetting("fmvfomr", "FM VFO/MR", + RadioSettingValueList(VFOMR, + VFOMR[_settings.fmvfomr])) + cfg_grp.append(rs) + + rs = RadioSetting("fmmrch", "FM MR Channel", + RadioSettingValueInteger(0, 100, _settings.fmmrch)) + cfg_grp.append(rs) + + rs = RadioSetting("noaa", "NOAA", + RadioSettingValueList(NOAA, + NOAA[_settings.noaa])) + cfg_grp.append(rs) + + rs = RadioSetting("noaach", "NOAA Channel", + RadioSettingValueList(NOAACH, + NOAACH[_settings.noaach])) + cfg_grp.append(rs) + + rs = RadioSetting("part95", "PART95", + RadioSettingValueList(PART95, + PART95[_settings.part95])) + cfg_grp.append(rs) + + rs = RadioSetting("gmrs", "GMRS", + RadioSettingValueList(GMRS, + GMRS[_settings.gmrs])) + cfg_grp.append(rs) + + rs = RadioSetting("murs", "MURS", + RadioSettingValueList(MURS, + MURS[_settings.murs])) + cfg_grp.append(rs) + + for i in range(0, 9): + val = self._memobj.banklink[i].bank + rs = RadioSetting("banklink/%i.bank" % i, + "Bank Link %i" % (i + 1), + RadioSettingValueBoolean(val)) + cfg_grp.append(rs) + + val = self._memobj.banklink[9].bank + rs = RadioSetting("banklink/9.bank", "Bank Link 0", + RadioSettingValueBoolean(val)) + cfg_grp.append(rs) + + rs = RadioSetting("allband", "All Band", + RadioSettingValueBoolean(_settings.allband)) + cfg_grp.append(rs) + + rs = RadioSetting("alarmoff", "Alarm Function Off", + RadioSettingValueBoolean(_settings.alarmoff)) + cfg_grp.append(rs) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + cfg_grp.append(rs) + + rs = RadioSetting("radio", "Radio", + RadioSettingValueBoolean(_settings.radio)) + cfg_grp.append(rs) + + if self.MODEL == "TERMN-8R": + rs = RadioSetting("keylock", "Keylock", + RadioSettingValueBoolean(_settings.keylock)) + cfg_grp.append(rs) + + rs = RadioSetting("fastscan", "Fast Scan", + RadioSettingValueBoolean(_settings.fastscan)) + cfg_grp.append(rs) + + if self.MODEL == "OBLTR-8R": + # "pf2key" is used for OBLTR-8R "keylock" + rs = RadioSetting("pf2key", "Keylock", + RadioSettingValueBoolean(_settings.pf2key)) + cfg_grp.append(rs) + + rs = RadioSetting("fastscano", "Fast Scan", + RadioSettingValueBoolean(_settings.fastscano)) + cfg_grp.append(rs) + + rs = RadioSetting("uplink", "Up Bank Link Select", + RadioSettingValueBoolean(_settings.uplink)) + cfg_grp.append(rs) + + rs = RadioSetting("downlink", "Down Bank Link Select", + RadioSettingValueBoolean(_settings.downlink)) + cfg_grp.append(rs) + + # + # OEM info + # + + val = RadioSettingValueString(0, 7, _filter(_oem.model)) + val.set_mutable(False) + rs = RadioSetting("oem_info.model", "Model", val) + oem_grp.append(rs) + + val = RadioSettingValueString(0, 9, _filter(_oem.date)) + val.set_mutable(False) + rs = RadioSetting("oem_info.date", "Date", val) + oem_grp.append(rs) + + val = RadioSettingValueString(0, 16, _filter(_oem.dealer)) + val.set_mutable(False) + rs = RadioSetting("oem_info.dealer", "Dealer Code", val) + oem_grp.append(rs) + + val = RadioSettingValueString(0, 9, _filter(_oem.stockdate)) + val.set_mutable(False) + rs = RadioSetting("oem_info.stockdate", "Stock Date", val) + oem_grp.append(rs) + + val = RadioSettingValueString(0, 9, _filter(_oem.selldate)) + val.set_mutable(False) + rs = RadioSetting("oem_info.selldate", "Sell Date", val) + oem_grp.append(rs) + + return group + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + return cls._file_ident in filedata[0x10:0x20] + +@directory.register +class AnyToneOBLTR8RRadio(AnyToneTERMN8RRadio): + """AnyTone OBLTR-8R""" + VENDOR = "AnyTone" + MODEL = "OBLTR-8R" + _file_ident = "OBLTR8R" diff --git a/chirp/drivers/ap510.py b/chirp/drivers/ap510.py new file mode 100644 index 0000000..36df4c0 --- /dev/null +++ b/chirp/drivers/ap510.py @@ -0,0 +1,807 @@ +# Copyright 2014 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +from time import sleep +import logging + +from chirp import chirp_common, directory, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + InvalidValueError, RadioSettings + +LOG = logging.getLogger(__name__) + + +def chunks(s, t): + """ Yield chunks of s in sizes defined in t.""" + i = 0 + for n in t: + yield s[i:i+n] + i += n + + +def encode_base100(v): + return (v / 100 << 8) + (v % 100) + + +def decode_base100(u16): + return 100 * (u16 >> 8 & 0xff) + (u16 & 0xff) + + +def drain(pipe): + """Chew up any data waiting on @pipe""" + for x in xrange(3): + buf = pipe.read(4096) + if not buf: + return + raise errors.RadioError('Your pipes are clogged.') + + +def enter_setup(pipe): + """Put AP510 in configuration mode.""" + for x in xrange(30): + if x % 2: + pipe.write("@SETUP") + else: + pipe.write("\r\nSETUP\r\n") + s = pipe.read(64) + if s and "\r\nSETUP" in s: + return True + elif s and "SETUP" in s: + return False + raise errors.RadioError('Radio did not respond.') + + +def download(radio): + status = chirp_common.Status() + drain(radio.pipe) + + status.msg = " Power on AP510 now, waiting " + radio.status_fn(status) + new = enter_setup(radio.pipe) + + status.cur = 1 + status.max = 5 + status.msg = "Downloading" + radio.status_fn(status) + if new: + radio.pipe.write("\r\nDISP\r\n") + else: + radio.pipe.write("@DISP") + buf = "" + + for status.cur in xrange(status.cur, status.max): + buf += radio.pipe.read(1024) + if buf.endswith("\r\n"): + status.cur = status.max + radio.status_fn(status) + break + radio.status_fn(status) + else: + raise errors.RadioError("Incomplete data received.") + + LOG.debug("%04i P7H", self._memobj[self.ATTR_MAP['smartbeacon']])) + )) + + def set_smartbeacon(self, d): + self._memobj[self.ATTR_MAP['smartbeacon']] = \ + struct.pack(">7H", + encode_base100(d['lowspeed']), + encode_base100(d['slowrate']), + encode_base100(d['highspeed']), + encode_base100(d['fastrate']), + encode_base100(d['turnslope']), + encode_base100(d['turnangle']), + encode_base100(d['turntime']), + ) + + +class AP510Memory20141215(AP510Memory): + """Compatible with firmware version 20141215""" + ATTR_MAP = dict(AP510Memory.ATTR_MAP.items() + { + 'tx_volume': '21', # 1-6 + 'rx_volume': '22', # 1-9 + 'tx_power': '23', # 1: 1 watt, 0: 0.5 watt + 'tx_serial_ui_out': '24', + 'path1': '25', + 'path2': '26', + 'path3': '27', # like "WIDE1 1" else "0" + 'multiple': '28', + 'auto_on': '29', + }.items()) + + def get_multiple(self): + return dict(zip( + ( + 'mice_message', # conveniently matches APRS spec Mic-E messages + 'voltage', # voltage in comment + 'temperature', # temperature in comment + 'tfx', # not sure what the TF/X toggle does + 'squelch', # squelch level 0-8 (0 = disabled) + 'blueled', # 0: squelch LED on GPS lock + # 1: light LED on GPS lock + 'telemetry', # 1: enable + 'telemetry_every', # two-digit int + 'timeslot_enable', # 1: enable Is this implemented in firmware? + 'timeslot', # int 00-59 + 'dcd', # 0: Blue LED displays squelch, + # 1: Blue LED displays software DCD + 'tf_card' # 0: KML, 1: WPL + ), map(int, chunks(self._memobj[self.ATTR_MAP['multiple']], + (1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1))) + )) + + def set_multiple(self, d): + self._memobj[self.ATTR_MAP['multiple']] = "%(mice_message)1d" \ + "%(voltage)1d" \ + "%(temperature)1d" \ + "%(tfx)1d" \ + "%(squelch)1d" \ + "%(blueled)1d" \ + "%(telemetry)1d" \ + "%(telemetry_every)02d" \ + "%(timeslot_enable)1d" \ + "%(timeslot)02d" \ + "%(dcd)1d" \ + "%(tf_card)1d" % d + + def get_smartbeacon(self): + # raw: 18=0100300060010240028005 + # chunks: 18=010 0300 060 010 240 028 005 + return dict(zip(( + 'lowspeed', + 'slowrate', + 'highspeed', + 'fastrate', + 'turnslope', + 'turnangle', + 'turntime', + ), map(int, chunks( + self._memobj[self.ATTR_MAP['smartbeacon']], + (3, 4, 3, 3, 3, 3, 3))) + )) + + def set_smartbeacon(self, d): + self._memobj[self.ATTR_MAP['smartbeacon']] = "%(lowspeed)03d" \ + "%(slowrate)04d" \ + "%(highspeed)03d" \ + "%(fastrate)03d" \ + "%(turnslope)03d" \ + "%(turnangle)03d" \ + "%(turntime)03d" % d + + +PTT_DELAY = ['60 ms', '120 ms', '180 ms', '300 ms', '480 ms', + '600 ms', '1000 ms'] +OUTPUT = ['KISS', 'Waypoint out', 'UI out'] +PATH = [ + '(None)', + 'WIDE1-1', + 'WIDE1-1,WIDE2-1', + 'WIDE1-1,WIDE2-2', + 'TEMP1-1', + 'TEMP1-1,WIDE 2-1', + 'WIDE2-1', +] +TABLE = "/\#&0>AW^_acnsuvz" +SYMBOL = "".join(map(chr, range(ord("!"), ord("~")+1))) +BEACON = ['manual', 'auto', 'auto + manual', 'smart', 'smart + manual'] +ALIAS = ['WIDE1-N', 'WIDE2-N', 'WIDE1-N + WIDE2-N'] +CHARSET = "".join(map(chr, range(0, 256))) +MICE_MESSAGE = ['Emergency', 'Priority', 'Special', 'Committed', 'Returning', + 'In Service', 'En Route', 'Off Duty'] +TF_CARD = ['WPL', 'KML'] +POWER_LEVELS = [chirp_common.PowerLevel("0.5 watt", watts=0.50), + chirp_common.PowerLevel("1 watt", watts=1.00)] + +RP_IMMUTABLE = ["number", "skip", "bank", "extd_number", "name", "rtone", + "ctone", "dtcs", "tmode", "dtcs_polarity", "skip", "duplex", + "offset", "mode", "tuning_step", "bank_index"] + + +class AP510Radio(chirp_common.CloneModeRadio): + """Sainsonic AP510""" + BAUD_RATE = 9600 + VENDOR = "Sainsonic" + MODEL = "AP510" + + _model = "AVRT5" + mem_upper_limit = 0 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.valid_modes = ["FM"] + rf.valid_tmodes = [""] + rf.valid_characters = "" + rf.valid_duplexes = [""] + rf.valid_name_length = 0 + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = [] + rf.valid_tuning_steps = [] + rf.has_bank = False + rf.has_ctone = False + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_mode = False + rf.has_name = False + rf.has_offset = False + rf.has_tuning_step = False + rf.valid_bands = [(136000000, 174000000)] + rf.memory_bounds = (0, 0) + return rf + + def sync_in(self): + try: + data = download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + # _mmap isn't a Chirp MemoryMap, but since AP510Memory implements + # get_packed(), the standard Chirp save feature works. + if data.startswith('\r\n00=%s 20141215' % self._model): + self._mmap = AP510Memory20141215(data) + else: + self._mmap = AP510Memory(data) + + def process_mmap(self): + self._mmap.process_data() + + def sync_out(self): + try: + upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def load_mmap(self, filename): + """Load the radio's memory map from @filename""" + mapfile = file(filename, "rb") + data = mapfile.read() + if data.startswith('\r\n00=%s 20141215' % self._model): + self._mmap = AP510Memory20141215(data) + else: + self._mmap = AP510Memory(data) + mapfile.close() + + def get_raw_memory(self, number): + return self._mmap.get_packed() + + def get_memory(self, number): + if number != 0: + raise errors.InvalidMemoryLocation("AP510 has only one slot") + + mem = chirp_common.Memory() + mem.number = 0 + mem.freq = float(self._mmap.freq) * 1000000 + mem.name = "TX/RX" + mem.mode = "FM" + mem.offset = 0.0 + try: + mem.power = POWER_LEVELS[int(self._mmap.tx_power)] + except NotImplementedError: + mem.power = POWER_LEVELS[1] + mem.immutable = RP_IMMUTABLE + + return mem + + def set_memory(self, mem): + if mem.number != 0: + raise errors.InvalidMemoryLocation("AP510 has only one slot") + + self._mmap.freq = "%8.4f" % (mem.freq / 1000000.0) + if mem.power: + try: + self._mmap.tx_power = str(POWER_LEVELS.index(mem.power)) + except NotImplementedError: + pass + + def get_settings(self): + china = RadioSettingGroup("china", "China Map Fix") + smartbeacon = RadioSettingGroup("smartbeacon", "Smartbeacon") + + aprs = RadioSettingGroup("aprs", "APRS", china, smartbeacon) + digipeat = RadioSettingGroup("digipeat", "Digipeat") + system = RadioSettingGroup("system", "System") + settings = RadioSettings(aprs, digipeat, system) + + aprs.append(RadioSetting("callsign", "Callsign", + RadioSettingValueString(0, 6, self._mmap.callsign[:6]))) + aprs.append(RadioSetting("ssid", "SSID", RadioSettingValueInteger( + 0, 15, ord(self._mmap.callsign[6]) - 0x30))) + pttdelay = PTT_DELAY[int(self._mmap.pttdelay) - 1] + aprs.append(RadioSetting("pttdelay", "PTT Delay", + RadioSettingValueList(PTT_DELAY, pttdelay))) + output = OUTPUT[int(self._mmap.output) - 1] + aprs.append(RadioSetting("output", "Output", + RadioSettingValueList(OUTPUT, output))) + aprs.append(RadioSetting("mice", "Mic-E", + RadioSettingValueBoolean(strbool(self._mmap.mice)))) + try: + mice_msg = MICE_MESSAGE[int(self._mmap.multiple['mice_message'])] + aprs.append(RadioSetting("mice_message", "Mic-E Message", + RadioSettingValueList(MICE_MESSAGE, mice_msg))) + except NotImplementedError: + pass + try: + aprs.append(RadioSetting("path1", "Path 1", + RadioSettingValueString(0, 6, self._mmap.path1[:6], + autopad=True, + charset=CHARSET))) + ssid1 = ord(self._mmap.path1[6]) - 0x30 + aprs.append(RadioSetting("ssid1", "SSID 1", + RadioSettingValueInteger(0, 7, ssid1))) + aprs.append(RadioSetting("path2", "Path 2", + RadioSettingValueString(0, 6, self._mmap.path2[:6], + autopad=True, + charset=CHARSET))) + ssid2 = ord(self._mmap.path2[6]) - 0x30 + aprs.append(RadioSetting("ssid2", "SSID 2", + RadioSettingValueInteger(0, 7, ssid2))) + aprs.append(RadioSetting("path3", "Path 3", + RadioSettingValueString(0, 6, self._mmap.path3[:6], + autopad=True, + charset=CHARSET))) + ssid3 = ord(self._mmap.path3[6]) - 0x30 + aprs.append(RadioSetting("ssid3", "SSID 3", + RadioSettingValueInteger(0, 7, ssid3))) + except NotImplementedError: + aprs.append(RadioSetting("path", "Path", + RadioSettingValueList(PATH, + PATH[int(self._mmap.path)]))) + aprs.append(RadioSetting("table", "Table or Overlay", + RadioSettingValueList(TABLE, self._mmap.symbol[1]))) + aprs.append(RadioSetting("symbol", "Symbol", + RadioSettingValueList(SYMBOL, self._mmap.symbol[0]))) + aprs.append(RadioSetting("beacon", "Beacon Mode", + RadioSettingValueList(BEACON, + BEACON[int(self._mmap.beacon) - 1]))) + aprs.append(RadioSetting("rate", "Beacon Rate (seconds)", + RadioSettingValueInteger(10, 9999, self._mmap.rate))) + aprs.append(RadioSetting("comment", "Comment", + RadioSettingValueString(0, 34, self._mmap.comment, + autopad=False, charset=CHARSET))) + try: + voltage = self._mmap.multiple['voltage'] + aprs.append(RadioSetting("voltage", "Voltage in comment", + RadioSettingValueBoolean(voltage))) + temperature = self._mmap.multiple['temperature'] + aprs.append(RadioSetting("temperature", "Temperature in comment", + RadioSettingValueBoolean(temperature))) + except NotImplementedError: + pass + aprs.append(RadioSetting("status", "Status", RadioSettingValueString( + 0, 34, self._mmap.status, autopad=False, charset=CHARSET))) + try: + telemetry = self._mmap.multiple['telemetry'] + aprs.append(RadioSetting("telemetry", "Telemetry", + RadioSettingValueBoolean(telemetry))) + telemetry_every = self._mmap.multiple['telemetry_every'] + aprs.append(RadioSetting("telemetry_every", "Telemetry every", + RadioSettingValueInteger(1, 99, telemetry_every))) + timeslot_enable = self._mmap.multiple['telemetry'] + aprs.append(RadioSetting("timeslot_enable", "Timeslot", + RadioSettingValueBoolean(timeslot_enable))) + timeslot = self._mmap.multiple['timeslot'] + aprs.append(RadioSetting("timeslot", "Timeslot (second of minute)", + RadioSettingValueInteger(0, 59, timeslot))) + except NotImplementedError: + pass + + fields = [ + ("chinamapfix", "China map fix", + RadioSettingValueBoolean(strbool(self._mmap.chinamapfix[0]))), + ("chinalat", "Lat", + RadioSettingValueInteger( + -45, 45, ord(self._mmap.chinamapfix[2]) - 80)), + ("chinalon", "Lon", + RadioSettingValueInteger( + -45, 45, ord(self._mmap.chinamapfix[1]) - 80)), + ] + for field in fields: + china.append(RadioSetting(*field)) + + try: + # Sometimes when digipeat is disabled, alias is 0xFF + alias = ALIAS[int(self._mmap.digipeat[1]) - 1] + except ValueError: + alias = ALIAS[0] + fields = [ + ("digipeat", "Digipeat", + RadioSettingValueBoolean(strbool(self._mmap.digipeat[0]))), + ("alias", "Digipeat Alias", + RadioSettingValueList( + ALIAS, alias)), + ("virtualgps", "Static Position", + RadioSettingValueBoolean(strbool(self._mmap.virtualgps[0]))), + ("btext", "Static Position BTEXT", RadioSettingValueString( + 0, 27, self._mmap.virtualgps[1:], autopad=False, + charset=CHARSET)), + ] + for field in fields: + digipeat.append(RadioSetting(*field)) + + sb = self._mmap.smartbeacon + fields = [ + ("lowspeed", "Low Speed"), + ("highspeed", "High Speed"), + ("slowrate", "Slow Rate (seconds)"), + ("fastrate", "Fast Rate (seconds)"), + ("turnslope", "Turn Slope"), + ("turnangle", "Turn Angle"), + ("turntime", "Turn Time (seconds)"), + ] + for field in fields: + smartbeacon.append(RadioSetting( + field[0], field[1], + RadioSettingValueInteger(0, 9999, sb[field[0]]) + )) + + system.append(RadioSetting("version", "Version (read-only)", + RadioSettingValueString(0, 14, self._mmap.version))) + system.append(RadioSetting("autooff", "Auto off (after 90 minutes)", + RadioSettingValueBoolean(strbool(self._mmap.autooff)))) + system.append(RadioSetting("beep", "Beep on transmit", + RadioSettingValueBoolean(strbool(self._mmap.beep)))) + system.append(RadioSetting("highaltitude", "High Altitude", + RadioSettingValueBoolean( + strbool(self._mmap.highaltitude)))) + system.append(RadioSetting("busywait", + "Wait for clear channel before transmit", + RadioSettingValueBoolean( + strbool(self._mmap.busywait)))) + try: + system.append(RadioSetting("tx_volume", "Transmit volume", + RadioSettingValueList( + map(str, range(1, 7)), self._mmap.tx_volume))) + system.append(RadioSetting("rx_volume", "Receive volume", + RadioSettingValueList( + map(str, range(1, 10)), self._mmap.rx_volume))) + system.append(RadioSetting("squelch", "Squelch", + RadioSettingValueList( + map(str, range(0, 9)), + str(self._mmap.multiple['squelch'])))) + system.append(RadioSetting("tx_serial_ui_out", "Tx serial UI out", + RadioSettingValueBoolean( + strbool(self._mmap.tx_serial_ui_out)))) + system.append(RadioSetting("auto_on", "Auto-on with 5V input", + RadioSettingValueBoolean( + strbool(self._mmap.auto_on[0])))) + system.append(RadioSetting( + "auto_on_delay", + "Auto-off delay after 5V lost (seconds)", + RadioSettingValueInteger( + 0, 9999, int(self._mmap.auto_on[1:])) + )) + system.append(RadioSetting("tfx", "TF/X", + RadioSettingValueBoolean( + self._mmap.multiple['tfx']))) + system.append(RadioSetting("blueled", "Light blue LED on GPS lock", + RadioSettingValueBoolean( + self._mmap.multiple['blueled']))) + system.append(RadioSetting("dcd", "Blue LED shows software DCD", + RadioSettingValueBoolean( + self._mmap.multiple['dcd']))) + system.append(RadioSetting("tf_card", "TF card format", + RadioSettingValueList( + TF_CARD, + TF_CARD[int(self._mmap.multiple['tf_card'])]))) + except NotImplementedError: + pass + + return settings + + def set_settings(self, settings): + for setting in settings: + if not isinstance(setting, RadioSetting): + self.set_settings(setting) + continue + if not setting.changed(): + continue + try: + name = setting.get_name() + if name == "callsign": + self.set_callsign(callsign=setting.value) + elif name == "ssid": + self.set_callsign(ssid=int(setting.value)) + elif name == "pttdelay": + self._mmap.pttdelay = PTT_DELAY.index( + str(setting.value)) + 1 + elif name == "output": + self._mmap.output = OUTPUT.index(str(setting.value)) + 1 + elif name in ('mice', 'autooff', 'beep', 'highaltitude', + 'busywait', 'tx_serial_ui_out'): + setattr(self._mmap, name, boolstr(setting.value)) + elif name == "mice_message": + multiple = self._mmap.multiple + multiple['mice_message'] = MICE_MESSAGE.index( + str(setting.value)) + self._mmap.multiple = multiple + elif name == "path": + self._mmap.path = PATH.index(str(setting.value)) + elif name == "path1": + self._mmap.path1 = "%s%s" % ( + setting.value, self._mmap.path1[6]) + elif name == "ssid1": + self._mmap.path1 = "%s%s" % ( + self._mmap.path1[:6], setting.value) + elif name == "path2": + self._mmap.path2 = "%s%s" % ( + setting.value, self._mmap.path2[6]) + elif name == "ssid2": + self._mmap.path2 = "%s%s" % ( + self._mmap.path2[:6], setting.value) + elif name == "path3": + self._mmap.path3 = "%s%s" % ( + setting.value, self._mmap.path3[6]) + elif name == "ssid3": + self._mmap.path3 = "%s%s" % ( + self._mmap.path3[:6], setting.value) + elif name == "table": + self.set_symbol(table=setting.value) + elif name == "symbol": + self.set_symbol(symbol=setting.value) + elif name == "beacon": + self._mmap.beacon = BEACON.index(str(setting.value)) + 1 + elif name == "rate": + self._mmap.rate = "%04d" % setting.value + elif name == "comment": + self._mmap.comment = str(setting.value) + elif name == "voltage": + multiple = self._mmap.multiple + multiple['voltage'] = int(setting.value) + self._mmap.multiple = multiple + elif name == "temperature": + multiple = self._mmap.multiple + multiple['temperature'] = int(setting.value) + self._mmap.multiple = multiple + elif name == "status": + self._mmap.status = str(setting.value) + elif name in ("telemetry", "telemetry_every", + "timeslot_enable", "timeslot", + "tfx", "blueled", "dcd"): + multiple = self._mmap.multiple + multiple[name] = int(setting.value) + self._mmap.multiple = multiple + elif name == "chinamapfix": + self.set_chinamapfix(enable=setting.value) + elif name == "chinalat": + self.set_chinamapfix(lat=int(setting.value)) + elif name == "chinalon": + self.set_chinamapfix(lon=int(setting.value)) + elif name == "digipeat": + self.set_digipeat(enable=setting.value) + elif name == "alias": + self.set_digipeat( + alias=str(ALIAS.index(str(setting.value)) + 1)) + elif name == "virtualgps": + self.set_virtualgps(enable=setting.value) + elif name == "btext": + self.set_virtualgps(btext=str(setting.value)) + elif name == "lowspeed": + self.set_smartbeacon(lowspeed=int(setting.value)) + elif name == "highspeed": + self.set_smartbeacon(highspeed=int(setting.value)) + elif name == "slowrate": + self.set_smartbeacon(slowrate=int(setting.value)) + elif name == "fastrate": + self.set_smartbeacon(fastrate=int(setting.value)) + elif name == "turnslope": + self.set_smartbeacon(turnslope=int(setting.value)) + elif name == "turnangle": + self.set_smartbeacon(turnangle=int(setting.value)) + elif name == "turntime": + self.set_smartbeacon(turntime=int(setting.value)) + elif name in ("tx_volume", "rx_volume", "squelch"): + setattr(self._mmap, name, "%1d" % setting.value) + elif name == "auto_on": + self._mmap.auto_on = "%s%05d" % ( + bool(setting.value) and '1' or 'i', + int(self._mmap.auto_on[1:])) + elif name == "auto_on_delay": + self._mmap.auto_on = "%s%05d" % ( + self._mmap.auto_on[0], setting.value) + elif name == "tf_card": + multiple = self._mmap.multiple + multiple['tf_card'] = TF_CARD.index(str(setting.value)) + self._mmap.multiple = multiple + except: + LOG.debug(setting.get_name()) + raise + + def set_callsign(self, callsign=None, ssid=None): + if callsign is None: + callsign = self._mmap.callsign[:6] + if ssid is None: + ssid = ord(self._mmap.callsign[6]) - 0x30 + self._mmap.callsign = str(callsign) + chr(ssid + 0x30) + + def set_symbol(self, table=None, symbol=None): + if table is None: + table = self._mmap.symbol[1] + if symbol is None: + symbol = self._mmap.symbol[0] + self._mmap.symbol = str(symbol) + str(table) + + def set_chinamapfix(self, enable=None, lat=None, lon=None): + if enable is None: + enable = strbool(self._mmap.chinamapfix[0]) + if lat is None: + lat = ord(self._mmap.chinamapfix[2]) - 80 + if lon is None: + lon = ord(self._mmap.chinamapfix[1]) - 80 + self._mmapchinamapfix = boolstr(enable) + chr(lon + 80) + chr(lat + 80) + + def set_digipeat(self, enable=None, alias=None): + if enable is None: + enable = strbool(self._mmap.digipeat[0]) + if alias is None: + alias = self._mmap.digipeat[1] + self._mmap.digipeat = boolstr(enable) + alias + + def set_virtualgps(self, enable=None, btext=None): + if enable is None: + enable = strbool(self._mmap.virtualgps[0]) + if btext is None: + btext = self._mmap.virtualgps[1:] + self._mmap.virtualgps = boolstr(enable) + btext + + def set_smartbeacon(self, **kwargs): + sb = self._mmap.smartbeacon + sb.update(kwargs) + if sb['lowspeed'] > sb['highspeed']: + raise InvalidValueError("Low speed must be less than high speed") + if sb['slowrate'] < sb['fastrate']: + raise InvalidValueError("Slow rate must be greater than fast rate") + self._mmap.smartbeacon = sb + + @classmethod + def match_model(cls, filedata, filename): + return filedata.startswith('\r\n00=' + cls._model) diff --git a/chirp/drivers/baofeng_common.py b/chirp/drivers/baofeng_common.py new file mode 100644 index 0000000..9a6581c --- /dev/null +++ b/chirp/drivers/baofeng_common.py @@ -0,0 +1,641 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""common functions for Baofeng (or similar) handheld radios""" + +import time +import struct +import logging +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList + +LOG = logging.getLogger(__name__) + +STIMEOUT = 1.5 + + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + LOG.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + msg = "Error reading data from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">BHB", ord(cmd), addr, length) + # add the data if set + if len(data) != 0: + frame += data + # return the data + return frame + + +def _recv(radio, addr, length): + """Get data from the radio """ + # read 4 bytes of header + hdr = _rawrecv(radio, 4) + + # read data + data = _rawrecv(radio, length) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(hdr + data)) + + c, a, l = struct.unpack(">BHB", hdr) + if a != addr or l != length or c != ord("X"): + LOG.error("Invalid answer for block 0x%04x:" % addr) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l)) + raise errors.RadioError("Unknown response from the radio") + + return data + + +def _get_radio_firmware_version(radio): + msg = struct.pack(">BHB", ord("S"), radio._fw_ver_start, + radio._recv_block_size) + radio.pipe.write(msg) + block = _recv(radio, radio._fw_ver_start, radio._recv_block_size) + _rawsend(radio, "\x06") + time.sleep(0.05) + version = block[0:16] + return version + + +def _image_ident_from_data(data, start, stop): + return data[start:stop] + + +def _get_image_firmware_version(radio): + return _image_ident_from_data(radio.get_mmap(), radio._fw_ver_start, + radio._fw_ver_start + 0x10) + + +def _do_ident(radio, magic): + """Put the radio in PROGRAM mode""" + # set the serial discipline + radio.pipe.baudrate = 9600 + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # flush input buffer + _clean_buffer(radio) + + # send request to enter program mode + _rawsend(radio, magic) + + ack = _rawrecv(radio, 1) + if ack != "\x06": + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond") + + _rawsend(radio, "\x02") + + # Ok, get the response + ident = _rawrecv(radio, radio._magic_response_length) + + # check if response is OK + if not ident.startswith("\xaa") or not ident.endswith("\xdd"): + # bad response + msg = "Unexpected response, got this:" + msg += util.hexprint(ident) + LOG.debug(msg) + raise errors.RadioError("Unexpected response from radio.") + + # DEBUG + LOG.info("Valid response, got this:") + LOG.debug(util.hexprint(ident)) + + _rawsend(radio, "\x06") + ack = _rawrecv(radio, 1) + if ack != "\x06": + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio refused clone") + + return ident + + +def _ident_radio(radio): + for magic in radio._magic: + error = None + try: + data = _do_ident(radio, magic) + return data + except errors.RadioError, e: + print e + error = e + time.sleep(2) + if error: + raise error + raise errors.RadioError("Radio did not respond") + + +def _download(radio): + """Get the memory map""" + # put radio in program mode + ident = _ident_radio(radio) + + # identify radio + radio_ident = _get_radio_firmware_version(radio) + LOG.info("Radio firmware version:") + LOG.debug(util.hexprint(radio_ident)) + + if radio_ident == "\xFF" * 16: + ident += radio.MODEL.ljust(8) + elif radio.MODEL in ("GMRS-V1", "MURS-V1"): + # check if radio_ident is OK + if not radio_ident[:7] in radio._fileid: + msg = "Incorrect model ID, got this:\n\n" + msg += util.hexprint(radio_ident) + LOG.debug(msg) + raise errors.RadioError("Incorrect 'Model' selected.") + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._mem_size / radio._recv_block_size + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, radio._mem_size, radio._recv_block_size): + frame = _make_frame("S", addr, radio._recv_block_size) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # sending the read request + _rawsend(radio, frame) + + if radio._ack_block: + ack = _rawrecv(radio, 1) + if ack != "\x06": + raise errors.RadioError( + "Radio refused to send block 0x%04x" % addr) + + # now we read + d = _recv(radio, addr, radio._recv_block_size) + + _rawsend(radio, "\x06") + time.sleep(0.05) + + # aggregate the data + data += d + + # UI Update + status.cur = addr / radio._recv_block_size + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data += ident + + return data + + +def _upload(radio): + """Upload procedure""" + # put radio in program mode + _ident_radio(radio) + + # identify radio + radio_ident = _get_radio_firmware_version(radio) + LOG.info("Radio firmware version:") + LOG.debug(util.hexprint(radio_ident)) + # identify image + image_ident = _get_image_firmware_version(radio) + LOG.info("Image firmware version:") + LOG.debug(util.hexprint(image_ident)) + + if radio.MODEL in ("GMRS-V1", "MURS-V1"): + # check if radio_ident is OK + if radio_ident != image_ident: + msg = "Incorrect model ID, got this:\n\n" + msg += util.hexprint(radio_ident) + LOG.debug(msg) + raise errors.RadioError("Image not supported by radio") + + if radio_ident != "0xFF" * 16 and image_ident == radio_ident: + _ranges = radio._ranges + else: + _ranges = [(0x0000, 0x0DF0), + (0x0E00, 0x1800)] + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._mem_size / radio._send_block_size + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # the fun start here + for start, end in _ranges: + for addr in range(start, end, radio._send_block_size): + # sending the data + data = radio.get_mmap()[addr:addr + radio._send_block_size] + + frame = _make_frame("X", addr, radio._send_block_size, data) + + _rawsend(radio, frame) + time.sleep(0.05) + + # receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + # UI Update + status.cur = addr / radio._send_block_size + status.msg = "Cloning to radio..." + radio.status_fn(status) + + +def _split(rf, f1, f2): + """Returns False if the two freqs are in the same band (no split) + or True otherwise""" + + # determine if the two freqs are in the same band + for low, high in rf.valid_bands: + if f1 >= low and f1 <= high and \ + f2 >= low and f2 <= high: + # if the two freqs are on the same Band this is not a split + return False + + # if you get here is because the freq pairs are split + return True + +class BaofengCommonHT(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """Baofeng HT Sytle Radios""" + VENDOR = "Baofeng" + MODEL = "" + IDENT = "" + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + _upload(self) + except errors.RadioError: + raise + except Exception, e: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def get_features(self): + """Get the radio's features""" + + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_name_length = self.LENGTH_NAME + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = self.SKIP_VALUES + rf.valid_dtcs_codes = self.DTCS_CODES + rf.memory_bounds = (0, 127) + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_bands = self.VALID_BANDS + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] + + return rf + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.txfreq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + if self._is_txinh(_mem): + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + else: + # TX freq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset != 0: + if _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10): + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + elif offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + for char in _nam.name: + if str(char) == "\xFF": + char = " " # The OEM software may have 0xFF mid-name + mem.name += str(char) + mem.name = mem.name.rstrip() + + dtcs_pol = ["N", "N"] + + if _mem.txtone in [0, 0xFFFF]: + txmode = "" + elif _mem.txtone >= 0x0258: + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + elif _mem.txtone <= 0x0258: + txmode = "DTCS" + if _mem.txtone > 0x69: + index = _mem.txtone - 0x6A + dtcs_pol[0] = "R" + else: + index = _mem.txtone - 1 + mem.dtcs = self.DTCS_CODES[index] + else: + LOG.warn("Bug: txtone is %04x" % _mem.txtone) + + if _mem.rxtone in [0, 0xFFFF]: + rxmode = "" + elif _mem.rxtone >= 0x0258: + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + elif _mem.rxtone <= 0x0258: + rxmode = "DTCS" + if _mem.rxtone >= 0x6A: + index = _mem.rxtone - 0x6A + dtcs_pol[1] = "R" + else: + index = _mem.rxtone - 1 + mem.rx_dtcs = self.DTCS_CODES[index] + else: + LOG.warn("Bug: rxtone is %04x" % _mem.rxtone) + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + if not _mem.scan: + mem.skip = "S" + + levels = self.POWER_LEVELS + try: + mem.power = levels[_mem.lowpower] + except IndexError: + LOG.error("Radio reported invalid power level %s (in %s)" % + (_mem.power, levels)) + mem.power = levels[0] + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(self.PTTID_LIST, + self.PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("scode", "S-CODE", + RadioSettingValueList(self.SCODE_LIST, + self.SCODE_LIST[_mem.scode])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _nam = self._memobj.names[mem.number] + + if mem.empty: + _mem.set_raw("\xff" * 16) + _nam.set_raw("\xff" * 16) + return + + _mem.set_raw("\x00" * 16) + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _nam.name[i] = mem.name[i] + except IndexError: + _nam.name[i] = "\xFF" + + rxmode = txmode = "" + if mem.tmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + _mem.rxtone = 0 + elif mem.tmode == "TSQL": + _mem.txtone = int(mem.ctone * 10) + _mem.rxtone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + rxmode = txmode = "DTCS" + _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + _mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1 + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + elif txmode == "DTCS": + _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + else: + _mem.txtone = 0 + if rxmode == "Tone": + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCS": + _mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1 + else: + _mem.rxtone = 0 + else: + _mem.rxtone = 0 + _mem.txtone = 0 + + if txmode == "DTCS" and mem.dtcs_polarity[0] == "R": + _mem.txtone += 0x69 + if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R": + _mem.rxtone += 0x69 + + _mem.scan = mem.skip != "S" + _mem.wide = mem.mode == "FM" + + if mem.power: + _mem.lowpower = self.POWER_LEVELS.index(mem.power) + else: + _mem.lowpower = 0 + + # extra settings + if len(mem.extra) > 0: + # there are setting, parse + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + else: + # there are no extra settings, load defaults + _mem.bcl = 0 + _mem.pttid = 0 + _mem.scode = 0 + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + for element in settings: + try: + val = element.value + if self._memobj.fm_presets <= 108.0 * 10 - 650: + value = int(val.get_value() * 10 - 650) + else: + value = int(val.get_value() * 10) + LOG.debug("Setting fm_presets = %s" % (value)) + self._memobj.fm_presets = value + except Exception, e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/baofeng_uv3r.py b/chirp/drivers/baofeng_uv3r.py new file mode 100644 index 0000000..a68bc60 --- /dev/null +++ b/chirp/drivers/baofeng_uv3r.py @@ -0,0 +1,653 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Baofeng UV3r radio management module""" + +import time +import os +import logging + +from wouxun_common import do_download, do_upload +from chirp import util, chirp_common, bitwise, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings + +LOG = logging.getLogger(__name__) + + +def _uv3r_prep(radio): + radio.pipe.write("\x05PROGRAM") + ack = radio.pipe.read(1) + if ack != "\x06": + raise errors.RadioError("Radio did not ACK first command") + + radio.pipe.write("\x02") + ident = radio.pipe.read(8) + if len(ident) != 8: + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio did not send identification") + + radio.pipe.write("\x06") + if radio.pipe.read(1) != "\x06": + raise errors.RadioError("Radio did not ACK ident") + + +def uv3r_prep(radio): + """Do the UV3R identification dance""" + for _i in range(0, 10): + try: + return _uv3r_prep(radio) + except errors.RadioError, e: + time.sleep(1) + + raise e + + +def uv3r_download(radio): + """Talk to a UV3R and do a download""" + try: + uv3r_prep(radio) + return do_download(radio, 0x0000, 0x0E40, 0x0010) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + +def uv3r_upload(radio): + """Talk to a UV3R and do an upload""" + try: + uv3r_prep(radio) + return do_upload(radio, 0x0000, 0x0E40, 0x0010) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + +UV3R_MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rx_freq[4]; + u8 rxtone; + lbcd offset[4]; + u8 txtone; + u8 ishighpower:1, + iswide:1, + dtcsinvt:1, + unknown1:1, + dtcsinvr:1, + unknown2:1, + duplex:2; + u8 unknown; + lbcd tx_freq[4]; +} tx_memory[99]; + +#seekto 0x0780; +struct { + lbcd lower_vhf[2]; + lbcd upper_vhf[2]; + lbcd lower_uhf[2]; + lbcd upper_uhf[2]; +} limits; + +struct vfosettings { + lbcd freq[4]; + u8 rxtone; + u8 unknown1; + lbcd offset[3]; + u8 txtone; + u8 power:1, + bandwidth:1, + unknown2:4, + duplex:2; + u8 step; + u8 unknown3[4]; +}; + +#seekto 0x0790; +struct { + struct vfosettings uhf; + struct vfosettings vhf; +} vfo; + +#seekto 0x07C2; +struct { + u8 squelch; + u8 vox; + u8 timeout; + u8 save:1, + unknown_1:1, + dw:1, + ste:1, + beep:1, + unknown_2:1, + bclo:1, + ch_flag:1; + u8 backlight:2, + relaym:1, + scanm:1, + pri:1, + unknown_3:3; + u8 unknown_4[3]; + u8 pri_ch; +} settings; + +#seekto 0x07E0; +u16 fm_presets[16]; + +#seekto 0x0810; +struct { + lbcd rx_freq[4]; + u8 rxtone; + lbcd offset[4]; + u8 txtone; + u8 ishighpower:1, + iswide:1, + dtcsinvt:1, + unknown1:1, + dtcsinvr:1, + unknown2:1, + duplex:2; + u8 unknown; + lbcd tx_freq[4]; +} rx_memory[99]; + +#seekto 0x1008; +struct { + u8 unknown[8]; + u8 name[6]; + u8 pad[2]; +} names[128]; +""" + +STEPS = [5.0, 6.25, 10.0, 12.5, 20.0, 25.0] +STEP_LIST = [str(x) for x in STEPS] +BACKLIGHT_LIST = ["Off", "Key", "Continuous"] +TIMEOUT_LIST = ["Off"] + ["%s sec" % x for x in range(30, 210, 30)] +SCANM_LIST = ["TO", "CO"] +PRI_CH_LIST = ["Off"] + ["%s" % x for x in range(1, 100)] +CH_FLAG_LIST = ["Freq Mode", "Channel Mode"] +POWER_LIST = ["Low", "High"] +BANDWIDTH_LIST = ["Narrow", "Wide"] +DUPLEX_LIST = ["Off", "-", "+"] +STE_LIST = ["On", "Off"] + +UV3R_DUPLEX = ["", "-", "+", ""] +UV3R_POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00), + chirp_common.PowerLevel("Low", watts=0.50)] +UV3R_DTCS_POL = ["NN", "NR", "RN", "RR"] + + +@directory.register +class UV3RRadio(chirp_common.CloneModeRadio): + """Baofeng UV-3R""" + VENDOR = "Baofeng" + MODEL = "UV-3R" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = UV3R_POWER_LEVELS + rf.valid_bands = [(136000000, 235000000), (400000000, 529000000)] + rf.valid_skips = [] + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS"] + rf.valid_tuning_steps = STEPS + rf.has_ctone = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_bank = False + rf.has_name = False + rf.can_odd_split = True + rf.memory_bounds = (1, 99) + return rf + + def sync_in(self): + self._mmap = uv3r_download(self) + self.process_mmap() + + def sync_out(self): + uv3r_upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(UV3R_MEM_FORMAT, self._mmap) + + def get_memory(self, number): + _mem = self._memobj.rx_memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + mem.offset = int(_mem.offset) * 10 + mem.duplex = UV3R_DUPLEX[_mem.duplex] + if mem.offset > 60000000: + if mem.duplex == "+": + mem.offset = mem.freq + mem.offset + elif mem.duplex == "-": + mem.offset = mem.freq - mem.offset + mem.duplex = "split" + mem.power = UV3R_POWER_LEVELS[1 - _mem.ishighpower] + if not _mem.iswide: + mem.mode = "NFM" + + dtcspol = (int(_mem.dtcsinvt) << 1) + _mem.dtcsinvr + mem.dtcs_polarity = UV3R_DTCS_POL[dtcspol] + + if _mem.txtone in [0, 0xFF]: + txmode = "" + elif _mem.txtone < 0x33: + mem.rtone = chirp_common.TONES[_mem.txtone - 1] + txmode = "Tone" + elif _mem.txtone >= 0x33: + tcode = chirp_common.DTCS_CODES[_mem.txtone - 0x33] + mem.dtcs = tcode + txmode = "DTCS" + else: + LOG.warn("Bug: tx_mode is %02x" % _mem.txtone) + + if _mem.rxtone in [0, 0xFF]: + rxmode = "" + elif _mem.rxtone < 0x33: + mem.ctone = chirp_common.TONES[_mem.rxtone - 1] + rxmode = "Tone" + elif _mem.rxtone >= 0x33: + rcode = chirp_common.DTCS_CODES[_mem.rxtone - 0x33] + mem.dtcs = rcode + rxmode = "DTCS" + else: + LOG.warn("Bug: rx_mode is %02x" % _mem.rxtone) + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS": + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + return mem + + def _set_tone(self, _mem, which, value, mode): + if mode == "Tone": + val = chirp_common.TONES.index(value) + 1 + elif mode == "DTCS": + val = chirp_common.DTCS_CODES.index(value) + 0x33 + elif mode == "": + val = 0 + else: + raise errors.RadioError("Internal error: tmode %s" % mode) + + setattr(_mem, which, val) + + def _set_memory(self, mem, _mem): + if mem.empty: + _mem.set_raw("\xff" * 16) + return + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "split": + diff = mem.freq - mem.offset + _mem.offset = abs(diff) / 10 + _mem.duplex = UV3R_DUPLEX.index(diff < 0 and "+" or "-") + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + else: + _mem.offset = mem.offset / 10 + _mem.duplex = UV3R_DUPLEX.index(mem.duplex) + _mem.tx_freq = (mem.freq + mem.offset) / 10 + + _mem.ishighpower = mem.power == UV3R_POWER_LEVELS[0] + _mem.iswide = mem.mode == "FM" + + _mem.dtcsinvt = mem.dtcs_polarity[0] == "R" + _mem.dtcsinvr = mem.dtcs_polarity[1] == "R" + + rxtone = txtone = 0 + rxmode = txmode = "" + + if mem.tmode == "DTCS": + rxmode = txmode = "DTCS" + rxtone = txtone = mem.dtcs + elif mem.tmode and mem.tmode != "Cross": + rxtone = txtone = mem.tmode == "Tone" and mem.rtone or mem.ctone + txmode = "Tone" + rxmode = mem.tmode == "TSQL" and "Tone" or "" + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + + if txmode == "DTCS": + txtone = mem.dtcs + elif txmode == "Tone": + txtone = mem.rtone + + if rxmode == "DTCS": + rxtone = mem.dtcs + elif rxmode == "Tone": + rxtone = mem.ctone + + self._set_tone(_mem, "txtone", txtone, txmode) + self._set_tone(_mem, "rxtone", rxtone, rxmode) + + def set_memory(self, mem): + _tmem = self._memobj.tx_memory[mem.number - 1] + _rmem = self._memobj.rx_memory[mem.number - 1] + + self._set_memory(mem, _tmem) + self._set_memory(mem, _rmem) + + def get_settings(self): + _settings = self._memobj.settings + _vfo = self._memobj.vfo + basic = RadioSettingGroup("basic", "Basic Settings") + group = RadioSettings(basic) + + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("backlight", "LCD Back Light", + RadioSettingValueList( + BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings.backlight])) + basic.append(rs) + + rs = RadioSetting("beep", "Keypad Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Level (0=OFF)", + RadioSettingValueInteger(0, 9, _settings.vox)) + basic.append(rs) + + rs = RadioSetting("dw", "Dual Watch", + RadioSettingValueBoolean(_settings.dw)) + basic.append(rs) + + rs = RadioSetting("ste", "Squelch Tail Eliminate", + RadioSettingValueList( + STE_LIST, STE_LIST[_settings.ste])) + basic.append(rs) + + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueBoolean(_settings.save)) + basic.append(rs) + + rs = RadioSetting("timeout", "Time Out Timer", + RadioSettingValueList( + TIMEOUT_LIST, TIMEOUT_LIST[_settings.timeout])) + basic.append(rs) + + rs = RadioSetting("scanm", "Scan Mode", + RadioSettingValueList( + SCANM_LIST, SCANM_LIST[_settings.scanm])) + basic.append(rs) + + rs = RadioSetting("relaym", "Repeater Sound Response", + RadioSettingValueBoolean(_settings.relaym)) + basic.append(rs) + + rs = RadioSetting("bclo", "Busy Channel Lock Out", + RadioSettingValueBoolean(_settings.bclo)) + basic.append(rs) + + rs = RadioSetting("pri", "Priority Channel Scanning", + RadioSettingValueBoolean(_settings.pri)) + basic.append(rs) + + rs = RadioSetting("pri_ch", "Priority Channel", + RadioSettingValueList( + PRI_CH_LIST, PRI_CH_LIST[_settings.pri_ch])) + basic.append(rs) + + rs = RadioSetting("ch_flag", "Display Mode", + RadioSettingValueList( + CH_FLAG_LIST, CH_FLAG_LIST[_settings.ch_flag])) + basic.append(rs) + + _limit = int(self._memobj.limits.lower_vhf) / 10 + if _limit < 115 or _limit > 239: + _limit = 144 + rs = RadioSetting("limits.lower_vhf", "VHF Lower Limit (115-239 MHz)", + RadioSettingValueInteger(115, 235, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.lower_vhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + _limit = int(self._memobj.limits.upper_vhf) / 10 + if _limit < 115 or _limit > 239: + _limit = 146 + rs = RadioSetting("limits.upper_vhf", "VHF Upper Limit (115-239 MHz)", + RadioSettingValueInteger(115, 235, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.upper_vhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + _limit = int(self._memobj.limits.lower_uhf) / 10 + if _limit < 200 or _limit > 529: + _limit = 420 + rs = RadioSetting("limits.lower_uhf", "UHF Lower Limit (200-529 MHz)", + RadioSettingValueInteger(200, 529, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.lower_uhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + _limit = int(self._memobj.limits.upper_uhf) / 10 + if _limit < 200 or _limit > 529: + _limit = 450 + rs = RadioSetting("limits.upper_uhf", "UHF Upper Limit (200-529 MHz)", + RadioSettingValueInteger(200, 529, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.upper_uhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + vfo_preset = RadioSettingGroup("vfo_preset", "VFO Presets") + group.append(vfo_preset) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + real_freq = bytes + return chirp_common.format_freq(real_freq * 10) + + def apply_vhf_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + obj.vhf.freq = value + + val = RadioSettingValueString( + 0, 10, convert_bytes_to_freq(int(_vfo.vhf.freq))) + rs = RadioSetting("vfo.vhf.freq", + "VHF RX Frequency (115.00000-236.00000)", val) + rs.set_apply_callback(apply_vhf_freq, _vfo) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.vhf.duplex", "Shift Direction", + RadioSettingValueList( + DUPLEX_LIST, DUPLEX_LIST[_vfo.vhf.duplex])) + vfo_preset.append(rs) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + real_offset = bytes + return chirp_common.format_freq(real_offset * 10000) + + def apply_vhf_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10000 + obj.vhf.offset = value + + val = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(int(_vfo.vhf.offset))) + rs = RadioSetting("vfo.vhf.offset", "Offset (0.00-37.995)", val) + rs.set_apply_callback(apply_vhf_offset, _vfo) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.vhf.power", "Power Level", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfo.vhf.power])) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.vhf.bandwidth", "Bandwidth", + RadioSettingValueList( + BANDWIDTH_LIST, + BANDWIDTH_LIST[_vfo.vhf.bandwidth])) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.vhf.step", "Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfo.vhf.step])) + vfo_preset.append(rs) + + def apply_uhf_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + obj.uhf.freq = value + + val = RadioSettingValueString( + 0, 10, convert_bytes_to_freq(int(_vfo.uhf.freq))) + rs = RadioSetting("vfo.uhf.freq", + "UHF RX Frequency (200.00000-529.00000)", val) + rs.set_apply_callback(apply_uhf_freq, _vfo) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.uhf.duplex", "Shift Direction", + RadioSettingValueList( + DUPLEX_LIST, DUPLEX_LIST[_vfo.uhf.duplex])) + vfo_preset.append(rs) + + def apply_uhf_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10000 + obj.uhf.offset = value + + val = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(int(_vfo.uhf.offset))) + rs = RadioSetting("vfo.uhf.offset", "Offset (0.00-69.995)", val) + rs.set_apply_callback(apply_uhf_offset, _vfo) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.uhf.power", "Power Level", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfo.uhf.power])) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.uhf.bandwidth", "Bandwidth", + RadioSettingValueList( + BANDWIDTH_LIST, + BANDWIDTH_LIST[_vfo.uhf.bandwidth])) + vfo_preset.append(rs) + + rs = RadioSetting("vfo.uhf.step", "Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfo.uhf.step])) + vfo_preset.append(rs) + + fm_preset = RadioSettingGroup("fm_preset", "FM Radio Presets") + group.append(fm_preset) + + for i in range(0, 16): + if self._memobj.fm_presets[i] < 0x01AF: + used = True + preset = self._memobj.fm_presets[i] / 10.0 + 65 + else: + used = False + preset = 65 + rs = RadioSetting("fm_presets_%1i" % i, "FM Preset %i" % (i + 1), + RadioSettingValueBoolean(used), + RadioSettingValueFloat(65, 108, preset, 0.1, 1)) + fm_preset.append(rs) + + return group + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + for element in settings: + try: + index = (int(element.get_name().split("_")[-1])) + val = element.value + if val[0].get_value(): + value = int(val[1].get_value() * 10 - 650) + else: + value = 0x01AF + LOG.debug("Setting fm_presets[%1i] = %s" % (index, value)) + setting = self._memobj.fm_presets + setting[index] = value + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == 3648 + + def get_raw_memory(self, number): + _rmem = self._memobj.tx_memory[number - 1] + _tmem = self._memobj.rx_memory[number - 1] + return repr(_rmem) + repr(_tmem) diff --git a/chirp/drivers/baofeng_wp970i.py b/chirp/drivers/baofeng_wp970i.py new file mode 100644 index 0000000..6feda61 --- /dev/null +++ b/chirp/drivers/baofeng_wp970i.py @@ -0,0 +1,912 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +from chirp.drivers import baofeng_common +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, \ + InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +# #### MAGICS ######################################################### + +# Baofeng WP970I magic string +MSTRING_WP970I = "\x50\xBB\xFF\x20\x14\x04\x13" + + +DTMF_CHARS = "0123456789 *#ABCD" +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] + +LIST_AB = ["A", "B"] +LIST_ALMOD = ["Site", "Tone", "Code"] +LIST_BANDWIDTH = ["Wide", "Narrow"] +LIST_COLOR = ["Off", "Blue", "Orange", "Purple"] +LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"] +LIST_MODE = ["Channel", "Name", "Frequency"] +LIST_OFF1TO9 = ["Off"] + list("123456789") +LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"] +LIST_OFFAB = ["Off"] + LIST_AB +LIST_RESUME = ["TO", "CO", "SE"] +LIST_PONMSG = ["Full", "Message"] +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_SCODE = ["%s" % x for x in range(1, 16)] +LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SHIFTD = ["Off", "+", "-"] +LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)] +LIST_STEP = [str(x) for x in STEPS] +LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)] +LIST_TXPOWER = ["High", "Mid", "Low"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_WORKMODE = ["Frequency", "Channel"] + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + if len(data) > 0x2008: + rid = data[0x2008:0x2010] + return rid.startswith(cls.MODEL) + elif len(data) == 0x2008: + rid = data[0x1EF0:0x1EF7] + return rid in cls._fileid + else: + return False + + +class WP970I(baofeng_common.BaofengCommonHT): + """Baofeng WP970I""" + VENDOR = "Baofeng" + MODEL = "WP970I" + + _fileid = [] + _magic = [MSTRING_WP970I, ] + _magic_response_length = 8 + _fw_ver_start = 0x1EF0 + _recv_block_size = 0x40 + _mem_size = 0x2000 + _ack_block = True + + _ranges = [(0x0000, 0x0DF0), + (0x0E00, 0x1800), + (0x1EE0, 0x1EF0), + (0x1F60, 0x1F70), + (0x1F80, 0x1F90), + (0x1FC0, 0x1FD0)] + _send_block_size = 0x10 + + MODES = ["NFM", "FM"] + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "!@#$%^&*()+-=[]:\";'<>?,./" + LENGTH_NAME = 6 + SKIP_VALUES = ["", "S"] + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Med", watts=3.00), + chirp_common.PowerLevel("Low", watts=1.00)] + _vhf_range = (130000000, 180000000) + _uhf_range = (400000000, 521000000) + VALID_BANDS = [_vhf_range, + _uhf_range] + PTTID_LIST = LIST_PTTID + SCODE_LIST = LIST_SCODE + + MEM_FORMAT = """ + #seekto 0x0000; + struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unused1:3, + isuhf:1, + scode:4; + u8 unknown1:7, + txtoneicon:1; + u8 mailicon:3, + unknown2:3, + lowpower:2; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; + } memory[128]; + + #seekto 0x0B00; + struct { + u8 code[5]; + u8 unused[11]; + } pttid[15]; + + #seekto 0x0CAA; + struct { + u8 code[5]; + u8 unused1:6, + aniid:2; + u8 unknown[2]; + u8 dtmfon; + u8 dtmfoff; + } ani; + + #seekto 0x0E20; + struct { + u8 squelch; + u8 step; + u8 unknown1; + u8 save; + u8 vox; + u8 unknown2; + u8 abr; + u8 tdr; + u8 beep; + u8 timeout; + u8 unknown3[4]; + u8 voice; + u8 unknown4; + u8 dtmfst; + u8 unknown5; + u8 unknown12:6, + screv:2; + u8 pttid; + u8 pttlt; + u8 mdfa; + u8 mdfb; + u8 bcl; + u8 autolk; + u8 sftd; + u8 unknown6[3]; + u8 wtled; + u8 rxled; + u8 txled; + u8 almod; + u8 band; + u8 tdrab; + u8 ste; + u8 rpste; + u8 rptrl; + u8 ponmsg; + u8 roger; + u8 rogerrx; + u8 tdrch; + u8 displayab:1, + unknown1:2, + fmradio:1, + alarm:1, + unknown2:1, + reset:1, + menu:1; + u8 unknown1:6, + singleptt:1, + vfomrlock:1; + u8 workmode; + u8 keylock; + } settings; + + #seekto 0x0E76; + struct { + u8 unused1:1, + mrcha:7; + u8 unused2:1, + mrchb:7; + } wmchannel; + + struct vfo { + u8 unknown0[8]; + u8 freq[8]; + u8 offset[6]; + ul16 rxtone; + ul16 txtone; + u8 unused1:7, + band:1; + u8 unknown3; + u8 unused2:2, + sftd:2, + scode:4; + u8 unknown4; + u8 unused3:1 + step:3, + unused4:4; + u8 unused5:1, + widenarr:1, + unused6:4, + txpower3:2; + }; + + #seekto 0x0F00; + struct { + struct vfo a; + struct vfo b; + } vfo; + + #seekto 0x0F4E; + u16 fm_presets; + + #seekto 0x1000; + struct { + char name[7]; + u8 unknown1[9]; + } names[128]; + + #seekto 0x1ED0; + struct { + char line1[7]; + char line2[7]; + } sixpoweron_msg; + + #seekto 0x1EE0; + struct { + char line1[7]; + char line2[7]; + } poweron_msg; + + #seekto 0x1EF0; + struct { + char line1[7]; + char line2[7]; + } firmware_msg; + + struct squelch { + u8 sql0; + u8 sql1; + u8 sql2; + u8 sql3; + u8 sql4; + u8 sql5; + u8 sql6; + u8 sql7; + u8 sql8; + u8 sql9; + }; + + #seekto 0x1F60; + struct { + struct squelch vhf; + u8 unknown1[6]; + u8 unknown2[16]; + struct squelch uhf; + } squelch; + + struct limit { + u8 enable; + bbcd lower[2]; + bbcd upper[2]; + }; + + #seekto 0x1FC0; + struct { + struct limit vhf; + struct limit uhf; + } limits; + + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = baofeng_common.BaofengCommonHT.get_features(self) + rf.valid_tuning_steps = STEPS + return rf + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + fm_preset = RadioSettingGroup("fm_preset", "FM Preset") + dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings") + service = RadioSettingGroup("service", "Service Settings") + top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe, + service) + + # Basic settings + if _mem.settings.squelch > 0x09: + val = 0x00 + else: + val = _mem.settings.squelch + rs = RadioSetting("settings.squelch", "Squelch", + RadioSettingValueList( + LIST_OFF1TO9, LIST_OFF1TO9[val])) + basic.append(rs) + + if _mem.settings.save > 0x04: + val = 0x00 + else: + val = _mem.settings.save + rs = RadioSetting("settings.save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, LIST_SAVE[val])) + basic.append(rs) + + if _mem.settings.vox > 0x0A: + val = 0x00 + else: + val = _mem.settings.vox + rs = RadioSetting("settings.vox", "Vox", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + if _mem.settings.abr > 0x0A: + val = 0x00 + else: + val = _mem.settings.abr + rs = RadioSetting("settings.abr", "Backlight Timeout", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + rs = RadioSetting("settings.tdr", "Dual Watch", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(rs) + + rs = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(rs) + + if _mem.settings.timeout > 0x27: + val = 0x03 + else: + val = _mem.settings.timeout + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + LIST_TIMEOUT, LIST_TIMEOUT[val])) + basic.append(rs) + + if _mem.settings.voice > 0x02: + val = 0x01 + else: + val = _mem.settings.voice + rs = RadioSetting("settings.voice", "Voice Prompt", + RadioSettingValueList( + LIST_VOICE, LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("settings.dtmfst", "DTMF Sidetone", + RadioSettingValueList(LIST_DTMFST, LIST_DTMFST[ + _mem.settings.dtmfst])) + basic.append(rs) + + if _mem.settings.screv > 0x02: + val = 0x01 + else: + val = _mem.settings.screv + rs = RadioSetting("settings.screv", "Scan Resume", + RadioSettingValueList( + LIST_RESUME, LIST_RESUME[val])) + basic.append(rs) + + rs = RadioSetting("settings.pttid", "When to send PTT ID", + RadioSettingValueList(LIST_PTTID, LIST_PTTID[ + _mem.settings.pttid])) + basic.append(rs) + + if _mem.settings.pttlt > 0x1E: + val = 0x05 + else: + val = _mem.settings.pttlt + rs = RadioSetting("pttlt", "PTT ID Delay", + RadioSettingValueInteger(0, 50, val)) + basic.append(rs) + + rs = RadioSetting("settings.mdfa", "Display Mode (A)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("settings.mdfb", "Display Mode (B)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("settings.autolk", "Automatic Key Lock", + RadioSettingValueBoolean(_mem.settings.autolk)) + basic.append(rs) + + rs = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.wtled])) + basic.append(rs) + + rs = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.rxled])) + basic.append(rs) + + rs = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.txled])) + basic.append(rs) + + val = _mem.settings.almod + rs = RadioSetting("settings.almod", "Alarm Mode", + RadioSettingValueList( + LIST_ALMOD, LIST_ALMOD[val])) + basic.append(rs) + + if _mem.settings.tdrab > 0x02: + val = 0x00 + else: + val = _mem.settings.tdrab + rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority", + RadioSettingValueList( + LIST_OFFAB, LIST_OFFAB[val])) + basic.append(rs) + + rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)", + RadioSettingValueBoolean(_mem.settings.ste)) + basic.append(rs) + + if _mem.settings.rpste > 0x0A: + val = 0x00 + else: + val = _mem.settings.rpste + rs = RadioSetting("settings.rpste", + "Squelch Tail Eliminate (repeater)", + RadioSettingValueList( + LIST_RPSTE, LIST_RPSTE[val])) + basic.append(rs) + + if _mem.settings.rptrl > 0x0A: + val = 0x00 + else: + val = _mem.settings.rptrl + rs = RadioSetting("settings.rptrl", "STE Repeater Delay", + RadioSettingValueList( + LIST_STEDELAY, LIST_STEDELAY[val])) + basic.append(rs) + + rs = RadioSetting("settings.ponmsg", "Power-On Message", + RadioSettingValueList(LIST_PONMSG, LIST_PONMSG[ + _mem.settings.ponmsg])) + basic.append(rs) + + rs = RadioSetting("settings.roger", "Roger Beep", + RadioSettingValueBoolean(_mem.settings.roger)) + basic.append(rs) + + # Advanced settings + rs = RadioSetting("settings.reset", "RESET Menu", + RadioSettingValueBoolean(_mem.settings.reset)) + advanced.append(rs) + + rs = RadioSetting("settings.menu", "All Menus", + RadioSettingValueBoolean(_mem.settings.menu)) + advanced.append(rs) + + rs = RadioSetting("settings.fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_mem.settings.fmradio)) + advanced.append(rs) + + rs = RadioSetting("settings.alarm", "Alarm Sound", + RadioSettingValueBoolean(_mem.settings.alarm)) + advanced.append(rs) + + # Other settings + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = _mem.firmware_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val) + other.append(rs) + + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val) + other.append(rs) + + _msg = _mem.sixpoweron_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val) + other.append(rs) + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val) + other.append(rs) + + _msg = _mem.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + lower = 130 + upper = 179 + rs = RadioSetting("limits.vhf.lower", "VHF Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf.lower)) + other.append(rs) + + rs = RadioSetting("limits.vhf.upper", "VHF Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf.upper)) + other.append(rs) + + lower = 400 + upper = 520 + rs = RadioSetting("limits.uhf.lower", "UHF Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.uhf.lower)) + other.append(rs) + + rs = RadioSetting("limits.uhf.upper", "UHF Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.uhf.upper)) + other.append(rs) + + # Work mode settings + rs = RadioSetting("settings.displayab", "Display", + RadioSettingValueList( + LIST_AB, LIST_AB[_mem.settings.displayab])) + work.append(rs) + + rs = RadioSetting("settings.workmode", "VFO/MR Mode", + RadioSettingValueList( + LIST_WORKMODE, + LIST_WORKMODE[_mem.settings.workmode])) + work.append(rs) + + rs = RadioSetting("settings.keylock", "Keypad Lock", + RadioSettingValueBoolean(_mem.settings.keylock)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrcha)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrchb)) + work.append(rs) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + value = chirp_common.parse_freq(value) + msg = ("Can't be less than %i.0000") + if value > 99000000 and value < 130 * 1000000: + raise InvalidValueError(msg % (130)) + msg = ("Can't be between %i.9975-%i.0000") + if (179 + 1) * 1000000 <= value and value < 400 * 1000000: + raise InvalidValueError(msg % (179, 400)) + msg = ("Can't be greater than %i.9975") + if value > 99000000 and value > (520 + 1) * 1000000: + raise InvalidValueError(msg % (520)) + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.a.freq)) + val1a.set_validate_callback(my_validate) + rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a) + rs.set_apply_callback(apply_freq, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.b.freq)) + val1b.set_validate_callback(my_validate) + rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b) + rs.set_apply_callback(apply_freq, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.sftd", "VFO A Shift", + RadioSettingValueList( + LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.a.sftd])) + work.append(rs) + + rs = RadioSetting("vfo.b.sftd", "VFO B Shift", + RadioSettingValueList( + LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.b.sftd])) + work.append(rs) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + for byte in bytes: + real_offset = (real_offset * 10) + byte + return chirp_common.format_freq(real_offset * 1000) + + def apply_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 1000 + for i in range(5, -1, -1): + obj.offset[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_mem.vfo.a.offset)) + rs = RadioSetting("vfo.a.offset", + "VFO A Offset", val1a) + rs.set_apply_callback(apply_offset, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_mem.vfo.b.offset)) + rs = RadioSetting("vfo.b.offset", + "VFO B Offset", val1b) + rs.set_apply_callback(apply_offset, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.txpower3", "VFO A Power", + RadioSettingValueList( + LIST_TXPOWER, + LIST_TXPOWER[_mem.vfo.a.txpower3])) + work.append(rs) + + rs = RadioSetting("vfo.b.txpower3", "VFO B Power", + RadioSettingValueList( + LIST_TXPOWER, + LIST_TXPOWER[_mem.vfo.b.txpower3])) + work.append(rs) + + rs = RadioSetting("vfo.a.widenarr", "VFO A Bandwidth", + RadioSettingValueList( + LIST_BANDWIDTH, + LIST_BANDWIDTH[_mem.vfo.a.widenarr])) + work.append(rs) + + rs = RadioSetting("vfo.b.widenarr", "VFO B Bandwidth", + RadioSettingValueList( + LIST_BANDWIDTH, + LIST_BANDWIDTH[_mem.vfo.b.widenarr])) + work.append(rs) + + rs = RadioSetting("vfo.a.scode", "VFO A S-CODE", + RadioSettingValueList( + LIST_SCODE, + LIST_SCODE[_mem.vfo.a.scode])) + work.append(rs) + + rs = RadioSetting("vfo.b.scode", "VFO B S-CODE", + RadioSettingValueList( + LIST_SCODE, + LIST_SCODE[_mem.vfo.b.scode])) + work.append(rs) + + rs = RadioSetting("vfo.a.step", "VFO A Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.a.step])) + work.append(rs) + rs = RadioSetting("vfo.b.step", "VFO B Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.b.step])) + work.append(rs) + + # broadcast FM settings + _fm_presets = self._memobj.fm_presets + if _fm_presets <= 108.0 * 10 - 650: + preset = _fm_presets / 10.0 + 65 + elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10: + preset = _fm_presets / 10.0 + else: + preset = 76.0 + rs = RadioSetting("fm_presets", "FM Preset(MHz)", + RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)) + fm_preset.append(rs) + + # DTMF settings + def apply_code(setting, obj, length): + code = [] + for j in range(0, length): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + pttid = RadioSetting("pttid/%i.code" % i, + "Signal Code %i" % (i + 1), val) + pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5) + dtmfe.append(pttid) + + if _mem.ani.dtmfon > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + if _mem.ani.dtmfoff > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.code", "ANI Code", val) + rs.set_apply_callback(apply_code, self._memobj.ani, 5) + dtmfe.append(rs) + + rs = RadioSetting("ani.aniid", "When to send ANI ID", + RadioSettingValueList(LIST_PTTID, + LIST_PTTID[_mem.ani.aniid])) + dtmfe.append(rs) + + # Service settings + for band in ["vhf", "uhf"]: + for index in range(0, 10): + key = "squelch.%s.sql%i" % (band, index) + if band == "vhf": + _obj = self._memobj.squelch.vhf + elif band == "uhf": + _obj = self._memobj.squelch.uhf + val = RadioSettingValueInteger(0, 123, + getattr( + _obj, "sql%i" % (index))) + if index == 0: + val.set_mutable(False) + name = "%s Squelch %i" % (band.upper(), index) + rs = RadioSetting(key, name, val) + service.append(rs) + + return top + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) in [0x2008, 0x2010]: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +class RH5XAlias(chirp_common.Alias): + VENDOR = "Rugged" + MODEL = "RH5X" + + +class UV82IIIAlias(chirp_common.Alias): + VENDOR = "Baofeng" + MODEL = "UV-82III" + + +@directory.register +class BFA58(WP970I): + """Baofeng BF-A58""" + VENDOR = "Baofeng" + MODEL = "BF-A58" + ALIASES = [RH5XAlias] + + _fileid = ["BFT515 ", "BFT517 "] + + +@directory.register +class UV82WP(WP970I): + """Baofeng UV82-WP""" + VENDOR = "Baofeng" + MODEL = "UV-82WP" + + +@directory.register +class GT3WP(WP970I): + """Baofeng GT-3WP""" + VENDOR = "Baofeng" + MODEL = "GT-3WP" + + +@directory.register +class RT6(WP970I): + """Retevis RT6""" + VENDOR = "Retevis" + MODEL = "RT6" + + +@directory.register +class BFA58S(WP970I): + VENDOR = "Baofeng" + MODEL = "BF-A58S" + ALIASES = [UV82IIIAlias] + + def get_features(self): + rf = WP970I.get_features(self) + rf.valid_bands = [self._vhf_range, + (200000000, 260000000), + self._uhf_range] + return rf + + +@directory.register +class UV9R(WP970I): + """Baofeng UV-9R""" + VENDOR = "Baofeng" + MODEL = "UV-9R" + LENGTH_NAME = 7 diff --git a/chirp/drivers/bf-t1.py b/chirp/drivers/bf-t1.py new file mode 100644 index 0000000..c5b4e04 --- /dev/null +++ b/chirp/drivers/bf-t1.py @@ -0,0 +1,917 @@ +# Copyright 2017 Pavel Milanes, CO7WT, +# +# This driver is a community effort as I don't have the radio on my hands, so +# I was only the director of the orchestra, without the players this may never +# came true, so special thanks to the following hams for their contribution: +# - Henk van der Laan, PA3CQN +# - Setting Discovery. +# - Special channels for RELAY and EMERGENCY. +# - Harold Hankins +# - Memory limits, testing & bug hunting. +# - Dmitry Milkov +# - Testing & bug hunting. +# - Many others participants in the issue page on Chirp's site. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from time import sleep +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings +from textwrap import dedent + +import struct +import logging + +LOG = logging.getLogger(__name__) + +# A note about the memmory in these radios +# +# The '9100' OEM software only manipulates the lower 0x0180 bytes on read/write +# operations as we know, the file generated by the OEM software IS NOT an exact +# eeprom image, it's a crude text file with a pseudo csv format +# +# Later investigations by Harold Hankins found that the eeprom extend up to 2k +# consistent with a hardware chip K24C16 a 2k x 8 bit serial eeprom + +MEM_SIZE = 0x0800 # 2048 bytes +WRITE_SIZE = 0x0180 # 384 bytes +BLOCK_SIZE = 0x10 +ACK_CMD = "\x06" +MODES = ["NFM", "FM"] +SKIP_VALUES = ["S", ""] +TONES = chirp_common.TONES +DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +# Special channels +SPECIALS = { + "EMG": -2, + "RLY": -1 + } + +# Settings vars +TOT_LIST = ["Off"] + ["%s" % x for x in range(30, 210, 30)] +SCAN_TYPE_LIST = ["Time", "Carrier", "Search"] +LANGUAGE_LIST = ["Off", "English", "Chinese"] +TIMER_LIST = ["Off"] + ["%s h" % (x * 0.5) for x in range(1, 17)] +FM_RANGE_LIST = ["76-108", "65-76"] +RELAY_MODE_LIST = ["Off", "RX sync", "TX sync"] +BACKLIGHT_LIST = ["Off", "Key", "On"] +POWER_LIST = ["0.5 Watt", "1.0 Watt"] + +# This is a general serial timeout for all serial read functions. +# Practice has show that about 0.07 sec will be enough to cover all radios. +STIMEOUT = 0.07 + +# this var controls the verbosity in the debug and by default it's low (False) +# make it True and you will to get a very verbose debug.log +debug = False + +# #### ID strings ##################################################### + +# BF-T1 handheld +BFT1_magic = "\x05PROGRAM" +BFT1_ident = " BF9100S" + + +def _clean_buffer(radio): + """Cleaning the read serial buffer, hard timeout to survive an infinite + data stream""" + + dump = "1" + datacount = 0 + + try: + while len(dump) > 0: + dump = radio.pipe.read(100) + datacount += len(dump) + # hard limit to survive a infinite serial data stream + # 5 times bigger than a normal rx block (20 bytes) + if datacount > 101: + seriale = "Please check your serial port selection." + raise errors.RadioError(seriale) + + except Exception: + raise errors.RadioError("Unknown error cleaning the serial buffer") + + +def _rawrecv(radio, amount=0): + """Raw read from the radio device""" + + # var to hold the data to return + data = "" + + try: + if amount == 0: + data = radio.pipe.read() + else: + data = radio.pipe.read(amount) + + # DEBUG + if debug is True: + LOG.debug("<== (%d) bytes:\n\n%s" % + (len(data), util.hexprint(data))) + + # fail if no data is received + if len(data) == 0: + raise errors.RadioError("No data received from radio") + + except: + raise errors.RadioError("Error reading data from radio") + + return data + + +def _send(radio, data): + """Send data to the radio device""" + + try: + radio.pipe.write(data) + + # DEBUG + if debug is True: + LOG.debug("==> (%d) bytes:\n\n%s" % + (len(data), util.hexprint(data))) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, data=""): + """Pack the info in the header format""" + frame = struct.pack(">BHB", ord(cmd), addr, BLOCK_SIZE) + + # add the data if set + if len(data) != 0: + frame += data + + return frame + + +def _recv(radio, addr): + """Get data from the radio""" + + # Get the full 20 bytes at a time + # 4 bytes header + 16 bytes of data (BLOCK_SIZE) + + # get the whole block + block = _rawrecv(radio, BLOCK_SIZE + 4) + + # short answer + if len(block) < (BLOCK_SIZE + 4): + raise errors.RadioError("Wrong block length (short) at 0x%04x" % addr) + + # long answer + if len(block) > (BLOCK_SIZE + 4): + raise errors.RadioError("Wrong block length (long) at 0x%04x" % addr) + + # header validation + c, a, l = struct.unpack(">cHB", block[0:4]) + if c != "W" or a != addr or l != BLOCK_SIZE: + LOG.debug("Invalid header for block 0x%04x:" % addr) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l)) + raise errors.RadioError("Invalid header for block 0x%04x:" % addr) + + # return the data, 16 bytes of payload + return block[4:] + + +def _start_clone_mode(radio, status): + """Put the radio in clone mode, 3 tries""" + + # cleaning the serial buffer + _clean_buffer(radio) + + # prep the data to show in the UI + status.cur = 0 + status.msg = "Identifying the radio..." + status.max = 3 + radio.status_fn(status) + + try: + for a in range(0, status.max): + # Update the UI + status.cur = a + 1 + radio.status_fn(status) + + # send the magic word + _send(radio, radio._magic) + + # Now you get a x06 of ACK if all goes well + ack = _rawrecv(radio, 1) + + if ack == ACK_CMD: + # DEBUG + LOG.info("Magic ACK received") + status.cur = status.max + radio.status_fn(status) + + return True + + return False + + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error sending Magic to radio:\n%s" % e) + + +def _do_ident(radio, status): + """Put the radio in PROGRAM mode & identify it""" + # set the serial discipline (default) + radio.pipe.baudrate = 9600 + radio.pipe.parity = "N" + radio.pipe.bytesize = 8 + radio.pipe.stopbits = 1 + radio.pipe.timeout = STIMEOUT + + # open the radio into program mode + if _start_clone_mode(radio, status) is False: + raise errors.RadioError("Radio did not enter clone mode, wrong model?") + + # Ok, poke it to get the ident string + _send(radio, "\x02") + ident = _rawrecv(radio, len(radio._id)) + + # basic check for the ident + if len(ident) != len(radio._id): + raise errors.RadioError("Radio send a odd identification block.") + + # check if ident is OK + if ident != radio._id: + LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident)) + raise errors.RadioError("Radio identification failed.") + + # handshake + _send(radio, ACK_CMD) + ack = _rawrecv(radio, 1) + + # checking handshake + if len(ack) == 1 and ack == ACK_CMD: + # DEBUG + LOG.info("ID ACK received") + else: + LOG.debug("Radio handshake failed.") + raise errors.RadioError("Radio handshake failed.") + + # DEBUG + LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL)) + + return True + + +def _download(radio): + """Get the memory map""" + + # UI progress + status = chirp_common.Status() + + # put radio in program mode and identify it + _do_ident(radio, status) + + # reset the progress bar in the UI + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + status.cur = 0 + radio.status_fn(status) + + # cleaning the serial buffer + _clean_buffer(radio) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # sending the read request + _send(radio, _make_frame("R", addr)) + + # read + d = _recv(radio, addr) + + # aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + # close comms with the radio + _send(radio, "\x62") + # DEBUG + LOG.info("Close comms cmd sent, radio must reboot now.") + + return data + + +def _upload(radio): + """Upload procedure, we only upload to the radio the Writable space""" + + # UI progress + status = chirp_common.Status() + + # put radio in program mode and identify it + _do_ident(radio, status) + + # get the data to upload to radio + data = radio.get_mmap() + + # Reset the UI progress + status.max = WRITE_SIZE / BLOCK_SIZE + status.cur = 0 + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # cleaning the serial buffer + _clean_buffer(radio) + + # the fun start here, we use WRITE_SIZE instead of the full MEM_SIZE + for addr in range(0, WRITE_SIZE, BLOCK_SIZE): + # getting the block of data to send + d = data[addr:addr + BLOCK_SIZE] + + # build the frame to send + frame = _make_frame("W", addr, d) + + # send the frame + _send(radio, frame) + + # receiving the response + ack = _rawrecv(radio, 1) + + # basic check + if len(ack) != 1: + raise errors.RadioError("No ACK when writing block 0x%04x" % addr) + + if ack != ACK_CMD: + raise errors.RadioError("Bad ACK writing block 0x%04x:" % addr) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # close comms with the radio + _send(radio, "\x62") + # DEBUG + LOG.info("Close comms cmd sent, radio must reboot now.") + + +def _model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + # a reliable fingerprint: the model name at + rid = data[0x06f8:0x0700] + + if rid == BFT1_ident: + return True + + return False + + +def _decode_ranges(low, high): + """Unpack the data in the ranges zones in the memmap and return + a tuple with the integer corresponding to the Mhz it means""" + return (int(low) * 100000, int(high) * 100000) + + +MEM_FORMAT = """ + +struct channel { + lbcd rxfreq[4]; // rx freq. + u8 rxtone; // x00 = none + // x01 - x32 = index of the analog tones + // x33 - x9b = index of Digital tones + // Digital tone polarity is handled below by + // ttondinv & ttondinv settings + lbcd txoffset[4]; // the difference against RX, direction handled by + // offplus & offminus + u8 txtone; // Idem to rxtone + u8 noskip:1, // if true is included in the scan + wide:1, // 1 = Wide, 0 = Narrow + ttondinv:1, // if true TX tone is Digital & Inverted + unA:1, // + rtondinv:1, // if true RX tone is Digital & Inverted + unB:1, // + offplus:1, // TX = RX + offset + offminus:1; // TX = RX - offset + u8 empty[5]; +}; + +#seekto 0x0000; +struct channel emg; // channel 0 is Emergent CH +#seekto 0x0010; +struct channel channels[20]; // normal 1-20 mem channels + +#seekto 0x0150; // Settings +struct { + lbcd vhfl[2]; // VHF low limit + lbcd vhfh[2]; // VHF high limit + lbcd uhfl[2]; // UHF low limit + lbcd uhfh[2]; // UHF high limit + u8 unk0[8]; + u8 unk1[2]; // start of 0x0160 <======= + u8 squelch; // byte: 0-9 + u8 vox; // byte: 0-9 + u8 timeout; // tot, 0 off, then 30 sec increments up to 180 + u8 batsave:1, // battery save 0 = off, 1 = on + fm_funct:1, // fm-radio 0=off, 1=on ( off disables fm button on set ) + ste:1, // squelch tail 0 = off, 1 = on + blo:1, // busy lockout 0 = off, 1 = on + beep:1, // key beep 0 = off, 1 = on + lock:1, // keylock 0 = ff, = on + backlight:2; // backlight 00 = off, 01 = key, 10 = on + u8 scantype; // scan type 0 = timed, 1 = carrier, 2 = stop + u8 channel; // active channel 1-20, setting it works on upload + u8 fmrange; // fm range 1 = low[65-76](ASIA), 0 = high[76-108](AMERICA) + u8 alarm; // alarm (count down timer) + // d0 - d16 in half hour increments => off, 0.5 - 8.0 h + u8 voice; // voice prompt 0 = off, 1 = english, 2 = chinese + u8 volume; // volume 1-7 as per the radio steps + // set to #FF by original software on upload + // chirp uploads actual value and works. + u16 fm_vfo; // the frequency of the fm receiver. + // resulting frequency is 65 + value * 0.1 MHz + // 0x145 is then 65 + 325*0.1 = 97.5 MHz + u8 relaym; // relay mode, d0 = off, d2 = re-tx, d1 = re-rx + // still a mystery on how it works + u8 tx_pwr; // tx pwr 0 = low (0.5W), 1 = high(1.0W) +} settings; + +#seekto 0x0170; // Relay CH +struct channel rly; + +""" + + +@directory.register +class BFT1(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): + """Baofeng BT-F1 radio & possibly alike radios""" + VENDOR = "Baofeng" + MODEL = "BF-T1" + _vhf_range = (130000000, 174000000) + _uhf_range = (400000000, 520000000) + _upper = 20 + _magic = BFT1_magic + _id = BFT1_ident + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental.\n' + '\n' + 'Please keep a copy of your memories with the original software ' + 'if you treasure them, this driver is new and may contain' + ' bugs.\n' + '\n' + '"Emergent CH" & "Relay CH" are implemented via special channels,' + 'be sure to click on the button on the interface to access them.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + + """)) + rp.pre_upload = _(dedent("""\ + Follow these instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + + """)) + return rp + + def get_features(self): + """Get the radio's features""" + + rf = chirp_common.RadioFeatures() + rf.valid_special_chans = SPECIALS.keys() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = False + rf.has_offset = True + rf.has_mode = True + rf.valid_modes = MODES + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = SKIP_VALUES + rf.valid_dtcs_codes = DTCS + rf.memory_bounds = (1, self._upper) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + + # normal dual bands + rf.valid_bands = [self._vhf_range, self._uhf_range] + + return rf + + def process_mmap(self): + """Process the mem map into the mem object""" + + # Get it + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + # set the band limits as the memmap + settings = self._memobj.settings + self._vhf_range = _decode_ranges(settings.vhfl, settings.vhfh) + self._uhf_range = _decode_ranges(settings.uhfl, settings.uhfh) + + def sync_in(self): + """Download from radio""" + data = _download(self) + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + + try: + _upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error: %s" % e) + + def _decode_tone(self, val, inv): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + + if val == 0: + return '', None, None + elif val < 51: # analog tone + return 'Tone', TONES[val - 1], None + elif val > 50: # digital tone + pol = "N" + # polarity? + if inv == 1: + pol = "R" + + return 'DTCS', DTCS[val - 51], pol + + def _encode_tone(self, memtone, meminv, mode, tone, pol): + """Parse the tone data to encode from UI to mem""" + + if mode == '' or mode is None: + memtone.set_value(0) + meminv.set_value(0) + elif mode == 'Tone': + # caching errors for analog tones. + try: + memtone.set_value(TONES.index(tone) + 1) + meminv.set_value(0) + except: + msg = "TCSS Tone '%d' is not supported" % tone + LOG.error(msg) + raise errors.RadioError(msg) + + elif mode == 'DTCS': + # caching errors for digital tones. + try: + memtone.set_value(DTCS.index(tone) + 51) + if pol == "R": + meminv.set_value(True) + else: + meminv.set_value(False) + except: + msg = "Digital Tone '%d' is not supported" % tone + LOG.error(msg) + raise errors.RadioError(msg) + else: + msg = "Internal error: invalid mode '%s'" % mode + LOG.error(msg) + raise errors.InvalidDataError(msg) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def _get_special(self, number): + if isinstance(number, str): + return (getattr(self._memobj, number.lower())) + elif number < 0: + for k, v in SPECIALS.items(): + if number == v: + return (getattr(self._memobj, k.lower())) + else: + return self._memobj.channels[number-1] + + def get_memory(self, number): + """Get the mem representation from the radio image""" + _mem = self._get_special(number) + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Check if special or normal + if isinstance(number, str): + mem.number = SPECIALS[number] + mem.extd_number = number + else: + mem.number = number + + if _mem.get_raw()[0] == "\xFF": + mem.empty = True + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + + # TX freq (Stored as a difference) + mem.offset = int(_mem.txoffset) * 10 + mem.duplex = "" + + # must work out the polarity + if mem.offset != 0: + if _mem.offminus == 1: + mem.duplex = "-" + # tx below RX + + if _mem.offplus == 1: + # tx above RX + mem.duplex = "+" + + # split RX/TX in different bands + if mem.offset > 71000000: + mem.duplex = "split" + + # show the actual value in the offset, depending on the shift + if _mem.offminus == 1: + mem.offset = mem.freq - mem.offset + if _mem.offplus == 1: + mem.offset = mem.freq + mem.offset + + # wide/narrow + mem.mode = MODES[int(_mem.wide)] + + # skip + mem.skip = SKIP_VALUES[_mem.noskip] + + # tone data + rxtone = txtone = None + txtone = self._decode_tone(_mem.txtone, _mem.ttondinv) + rxtone = self._decode_tone(_mem.rxtone, _mem.rtondinv) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI""" + # get the eprom representation of this channel + _mem = self._get_special(mem.number) + + # if empty memmory + if mem.empty: + # the channel itself + _mem.set_raw("\xFF" * 16) + # return it + return mem + + # frequency + _mem.rxfreq = mem.freq / 10 + + # duplex/ offset Offset is an absolute value + _mem.txoffset = mem.offset / 10 + + # must work out the polarity + if mem.duplex == "": + _mem.offplus = 0 + _mem.offminus = 0 + elif mem.duplex == "+": + _mem.offplus = 1 + _mem.offminus = 0 + elif mem.duplex == "-": + _mem.offplus = 0 + _mem.offminus = 1 + elif mem.duplex == "split": + if mem.freq > mem.offset: + _mem.offplus = 0 + _mem.offminus = 1 + _mem.txoffset = (mem.freq - mem.offset) / 10 + else: + _mem.offplus = 1 + _mem.offminus = 0 + _mem.txoffset = (mem.offset - mem.freq) / 10 + + # wide/narrow + _mem.wide = MODES.index(mem.mode) + + # skip + _mem.noskip = SKIP_VALUES.index(mem.skip) + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.txtone, _mem.ttondinv, txmode, txtone, txpol) + self._encode_tone(_mem.rxtone, _mem.rtondinv, rxmode, rxtone, rxpol) + + return mem + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + fm = RadioSettingGroup("fm", "FM Radio") + adv = RadioSettingGroup("adv", "Advanced Settings") + group = RadioSettings(basic, fm, adv) + + # ## Basic Settings + rs = RadioSetting("tx_pwr", "TX Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_settings.tx_pwr])) + basic.append(rs) + + rs = RadioSetting("channel", "Active Channel", + RadioSettingValueInteger(1, 20, _settings.channel)) + basic.append(rs) + + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Level", + RadioSettingValueInteger(0, 9, _settings.vox)) + basic.append(rs) + + # volume validation, as the OEM software set 0xFF on write + _volume = _settings.volume + if _volume > 7: + _volume = 7 + rs = RadioSetting("volume", "Volume Level", + RadioSettingValueInteger(0, 7, _volume)) + basic.append(rs) + + rs = RadioSetting("scantype", "Scan Type", + RadioSettingValueList(SCAN_TYPE_LIST, SCAN_TYPE_LIST[ + _settings.scantype])) + basic.append(rs) + + rs = RadioSetting("timeout", "Time Out Timer (seconds)", + RadioSettingValueList( + TOT_LIST, TOT_LIST[_settings.timeout])) + basic.append(rs) + + rs = RadioSetting("voice", "Voice Prompt", + RadioSettingValueList( + LANGUAGE_LIST, LANGUAGE_LIST[_settings.voice])) + basic.append(rs) + + rs = RadioSetting("alarm", "Alarm Time", + RadioSettingValueList( + TIMER_LIST, TIMER_LIST[_settings.alarm])) + basic.append(rs) + + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList( + BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings.backlight])) + basic.append(rs) + + rs = RadioSetting("blo", "Busy Lockout", + RadioSettingValueBoolean(_settings.blo)) + basic.append(rs) + + rs = RadioSetting("ste", "Squelch Tail Eliminate", + RadioSettingValueBoolean(_settings.ste)) + basic.append(rs) + + rs = RadioSetting("batsave", "Battery Save", + RadioSettingValueBoolean(_settings.batsave)) + basic.append(rs) + + rs = RadioSetting("lock", "Key Lock", + RadioSettingValueBoolean(_settings.lock)) + basic.append(rs) + + rs = RadioSetting("beep", "Key Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + # ## FM Settings + rs = RadioSetting("fm_funct", "FM Function", + RadioSettingValueBoolean(_settings.fm_funct)) + fm.append(rs) + + rs = RadioSetting("fmrange", "FM Range", + RadioSettingValueList( + FM_RANGE_LIST, FM_RANGE_LIST[_settings.fmrange])) + fm.append(rs) + + # callbacks for the FM VFO + def apply_fm_freq(setting, obj): + setattr(obj, setting.get_name(), int(setting.value. + get_value() * 10) - 650) + + _fm_vfo = int(_settings.fm_vfo) * 0.1 + 65 + rs = RadioSetting("fm_vfo", "FM Station", + RadioSettingValueFloat(65, 108, _fm_vfo)) + rs.set_apply_callback(apply_fm_freq, _settings) + fm.append(rs) + + # ## Advanced + def apply_limit(setting, obj): + setattr(obj, setting.get_name(), int(setting.value) * 10) + + rs = RadioSetting("vhfl", "VHF Low Limit", + RadioSettingValueInteger(130, 174, int( + _settings.vhfl) / 10)) + rs.set_apply_callback(apply_limit, _settings) + adv.append(rs) + + rs = RadioSetting("vhfh", "VHF High Limit", + RadioSettingValueInteger(130, 174, int( + _settings.vhfh) / 10)) + rs.set_apply_callback(apply_limit, _settings) + adv.append(rs) + + rs = RadioSetting("uhfl", "UHF Low Limit", + RadioSettingValueInteger(400, 520, int( + _settings.uhfl) / 10)) + rs.set_apply_callback(apply_limit, _settings) + adv.append(rs) + + rs = RadioSetting("uhfh", "UHF High Limit", + RadioSettingValueInteger(400, 520, int( + _settings.uhfh) / 10)) + rs.set_apply_callback(apply_limit, _settings) + adv.append(rs) + + rs = RadioSetting("relaym", "Relay Mode", + RadioSettingValueList(RELAY_MODE_LIST, + RELAY_MODE_LIST[_settings.relaym])) + adv.append(rs) + + return group + + def set_settings(self, uisettings): + _settings = self._memobj.settings + + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + + try: + name = element.get_name() + value = element.value + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + obj = getattr(_settings, name) + setattr(_settings, name, value) + + LOG.debug("Setting %s: %s" % (name, value)) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # DEBUG + if debug is True: + LOG.debug("BF-T1 matched!") + + # testing the firmware model fingerprint + match_model = _model_match(cls, filedata) + + return match_size and match_model diff --git a/chirp/drivers/bj9900.py b/chirp/drivers/bj9900.py new file mode 100644 index 0000000..ef92d64 --- /dev/null +++ b/chirp/drivers/bj9900.py @@ -0,0 +1,407 @@ +# +# Copyright 2015 Marco Filippi IZ3GME +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Baojie BJ-9900 management module""" + +from chirp import chirp_common, util, memmap, errors, directory, bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +import struct +import time +import logging +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +CMD_ACK = 0x06 + +@directory.register +class BJ9900Radio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """Baojie BJ-9900""" + VENDOR = "Baojie" + MODEL = "BJ-9900" + VARIANT = "" + BAUD_RATE = 115200 + + DUPLEX = ["", "-", "+", "split"] + MODES = ["NFM", "FM"] + TMODES = ["", "Tone", "TSQL", "DTCS", "Cross"] + CROSS_MODES = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + STEPS = [5.0, 6.25, 10.0, 12.5, 25.0] + VALID_BANDS = [(109000000, 136000000), (136000000, 174000000), + (400000000, 470000000)] + + CHARSET = list(chirp_common.CHARSET_ALPHANUMERIC) + CHARSET.remove(" ") + + POWER_LEVELS = [ + chirp_common.PowerLevel("Low", watts=20.00), + chirp_common.PowerLevel("High", watts=40.00)] + + _memsize = 0x18F1 + + # dat file format is + # 2 char per byte hex string + # on CR LF terminated lines of 96 char + # plus an empty line at the end + _datsize = (_memsize * 2) / 96 * 98 + 2 + + # block are read in same order as original sw eventhough they are not + # in physical order + _blocks = [ + (0x400, 0x1BFF, 0x30), + (0x300, 0x32F, 0x30), + (0x380, 0x3AF, 0x30), + (0x200, 0x22F, 0x30), + (0x240, 0x26F, 0x30), + (0x270, 0x2A0, 0x31), + ] + + MEM_FORMAT = """ + #seekto 0x%X; + struct { + u32 rxfreq; + u16 is_rxdigtone:1, + rxdtcs_pol:1, + rxtone:14; + u8 rxdtmf:4, + spmute:4; + u8 unknown1; + u32 txfreq; + u16 is_txdigtone:1, + txdtcs_pol:1, + txtone:14; + u8 txdtmf:4 + pttid:4; + u8 power:1, + wide:1, + compandor:1 + unknown3:5; + u8 namelen; + u8 name[7]; + } memory[128]; + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_upload = rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Remove front head. + 3. Connect data cable to radio, use the same connector where + head was connected to, not the mic connector. + 4. Click OK.""")) + rp.experimental = _( + 'This is experimental support for BJ-9900 ' + 'which is still under development.\n' + 'Please ensure you have a good backup with OEM software.\n' + 'Also please send in bug and enhancement requests!\n' + 'You have been warned. Proceed at your own risk!') + return rp + + def _read(self, addr, blocksize): + # read a single block + msg = struct.pack(">4sHH", "READ", addr, blocksize) + LOG.debug("sending " + util.hexprint(msg)) + self.pipe.write(msg) + block = self.pipe.read(blocksize) + LOG.debug("received " + util.hexprint(block)) + if len(block) != blocksize: + raise Exception("Unable to read block at addr %04X expected" + " %i got %i bytes" % + (addr, blocksize, len(block))) + return block + + def _clone_in(self): + start = time.time() + + data = "" + status = chirp_common.Status() + status.msg = _("Cloning from radio") + status.max = self._memsize + for addr_from, addr_to, blocksize in self._blocks: + for addr in range(addr_from, addr_to, blocksize): + data += self._read(addr, blocksize) + status.cur = len(data) + self.status_fn(status) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + return memmap.MemoryMap(data) + + def _write(self, addr, block): + # write a single block + msg = struct.pack(">4sHH", "WRIE", addr, len(block)) + block + LOG.debug("sending " + util.hexprint(msg)) + self.pipe.write(msg) + data = self.pipe.read(1) + LOG.debug("received " + util.hexprint(data)) + if ord(data) != CMD_ACK: + raise errors.RadioError( + "Radio refused to accept block 0x%04x" % addr) + + def _clone_out(self): + start = time.time() + + status = chirp_common.Status() + status.msg = _("Cloning to radio") + status.max = self._memsize + pos = 0 + for addr_from, addr_to, blocksize in self._blocks: + for addr in range(addr_from, addr_to, blocksize): + self._write(addr, self._mmap[pos:(pos + blocksize)]) + pos += blocksize + status.cur = pos + self.status_fn(status) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + def sync_in(self): + try: + self._mmap = self._clone_in() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + self._clone_out() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + if len(self._mmap) == self._datsize: + self._mmap = memmap.MemoryMap([ + chr(int(self._mmap.get(i, 2), 16)) + for i in range(0, self._datsize, 2) + if self._mmap.get(i, 2) != "\r\n" + ]) + try: + self._memobj = bitwise.parse( + self.MEM_FORMAT % self._memstart, self._mmap) + except AttributeError: + # main variant have no _memstart attribute + return + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_dtcs_polarity = True + rf.has_nostep_tuning = False + rf.valid_modes = list(self.MODES) + rf.valid_tmodes = list(self.TMODES) + rf.valid_cross_modes = list(self.CROSS_MODES) + rf.valid_duplexes = list(self.DUPLEX) + rf.has_tuning_step = False + # rf.valid_tuning_steps = list(self.STEPS) + rf.valid_bands = self.VALID_BANDS + rf.valid_skips = [""] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_characters = "".join(self.CHARSET) + rf.valid_name_length = 7 + rf.memory_bounds = (1, 128) + rf.can_odd_split = True + rf.has_settings = False + rf.has_cross = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_sub_devices = self.VARIANT == "" + + return rf + + def get_sub_devices(self): + return [BJ9900RadioLeft(self._mmap), BJ9900RadioRight(self._mmap)] + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xff" * (_mem.size() / 8)) # clean up + _mem.namelen = 0 + return + + _mem.rxfreq = mem.freq / 10 + if mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.namelen = len(mem.name) + for i in range(_mem.namelen): + _mem.name[i] = ord(mem.name[i]) + + rxmode = "" + txmode = "" + + if mem.tmode == "Tone": + txmode = "Tone" + elif mem.tmode == "TSQL": + rxmode = "Tone" + txmode = "TSQL" + elif mem.tmode == "DTCS": + rxmode = "DTCSSQL" + txmode = "DTCS" + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + + if rxmode == "": + _mem.rxdtcs_pol = 1 + _mem.is_rxdigtone = 1 + _mem.rxtone = 0x3FFF + elif rxmode == "Tone": + _mem.rxdtcs_pol = 0 + _mem.is_rxdigtone = 0 + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCSSQL": + _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 + _mem.is_rxdigtone = 1 + _mem.rxtone = mem.dtcs + elif rxmode == "DTCS": + _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 + _mem.is_rxdigtone = 1 + _mem.rxtone = mem.rx_dtcs + + if txmode == "": + _mem.txdtcs_pol = 1 + _mem.is_txdigtone = 1 + _mem.txtone = 0x3FFF + elif txmode == "Tone": + _mem.txdtcs_pol = 0 + _mem.is_txdigtone = 0 + _mem.txtone = int(mem.rtone * 10) + elif txmode == "TSQL": + _mem.txdtcs_pol = 0 + _mem.is_txdigtone = 0 + _mem.txtone = int(mem.ctone * 10) + elif txmode == "DTCS": + _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0 + _mem.is_txdigtone = 1 + _mem.txtone = mem.dtcs + + if (mem.power): + _mem.power = self.POWER_LEVELS.index(mem.power) + _mem.wide = self.MODES.index(mem.mode) + + # not supported yet + _mem.compandor = 0 + _mem.pttid = 0 + _mem.txdtmf = 0 + _mem.rxdtmf = 0 + _mem.spmute = 0 + + # set to mimic radio behaviour + _mem.unknown3 = 0 + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + if int(_mem.rxfreq) == int(_mem.txfreq) or _mem.txfreq == 0xFFFFFFFF: + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _mem.name[:_mem.namelen]: + mem.name += chr(char) + + dtcs_pol = ["N", "N"] + + if _mem.rxtone == 0x3FFF: + rxmode = "" + elif _mem.is_rxdigtone == 0: + # ctcss + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + else: + # digital + rxmode = "DTCS" + mem.rx_dtcs = int(_mem.rxtone & 0x3FFF) + if _mem.rxdtcs_pol == 1: + dtcs_pol[1] = "R" + + if _mem.txtone == 0x3FFF: + txmode = "" + elif _mem.is_txdigtone == 0: + # ctcss + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + else: + # digital + txmode = "DTCS" + mem.dtcs = int(_mem.txtone & 0x3FFF) + if _mem.txdtcs_pol == 1: + dtcs_pol[0] = "R" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + mem.power = self.POWER_LEVELS[_mem.power] + mem.mode = self.MODES[_mem.wide] + + return mem + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize or \ + (len(filedata) == cls._datsize and filedata[-4:] == "\r\n\r\n") + +class BJ9900RadioLeft(BJ9900Radio): + """Baojie BJ-9900 Left VFO subdevice""" + VARIANT = "Left" + _memstart = 0x0 + + +class BJ9900RadioRight(BJ9900Radio): + """Baojie BJ-9900 Right VFO subdevice""" + VARIANT = "Right" + _memstart = 0xC00 diff --git a/chirp/drivers/bjuv55.py b/chirp/drivers/bjuv55.py new file mode 100644 index 0000000..36cb98e --- /dev/null +++ b/chirp/drivers/bjuv55.py @@ -0,0 +1,652 @@ +# Copyright 2013 Jens Jensen AF5MI +# Based on work by Jim Unroe, Dan Smith, et al. +# Special thanks to Mats SM0BTP for equipment donation. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import time +import os +import logging + +from chirp.drivers import uv5r +from chirp import chirp_common, errors, util, directory, memmap +from chirp import bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings +from textwrap import dedent + +LOG = logging.getLogger(__name__) + + +BJUV55_MODEL = "\x50\xBB\xDD\x55\x63\x98\x4D" + +COLOR_LIST = ["Off", "Blue", "Red", "Pink"] + +STEPS = list(uv5r.STEPS) +STEPS.remove(2.5) +STEP_LIST = [str(x) for x in STEPS] + +MEM_FORMAT = """ +#seekto 0x0008; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unused1:3, + isuhf:1, + scode:4; + u8 unknown1:7, + txtoneicon:1; + u8 mailicon:3, + unknown2:4, + lowpower:1; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; +} memory[128]; + +#seekto 0x0B08; +struct { + u8 code[5]; + u8 unused[11]; +} pttid[15]; + +#seekto 0x0C88; +struct { + u8 inspection[5]; + u8 monitor[5]; + u8 alarmcode[5]; + u8 unknown1; + u8 stun[5]; + u8 kill[5]; + u8 revive[5]; + u8 unknown2; + u8 master_control_id[5]; + u8 vice_control_id[5]; + u8 code[5]; + u8 unused1:6, + aniid:2; + u8 unknown[2]; + u8 dtmfon; + u8 dtmfoff; +} ani; + +#seekto 0x0E28; +struct { + u8 squelch; + u8 step; + u8 tdrab; + u8 tdr; + u8 vox; + u8 timeout; + u8 unk2[6]; + u8 abr; + u8 beep; + u8 ani; + u8 unknown3[2]; + u8 voice; + u8 ring_time; + u8 dtmfst; + u8 unknown5; + u8 unknown12:6, + screv:2; + u8 pttid; + u8 pttlt; + u8 mdfa; + u8 mdfb; + u8 bcl; + u8 autolk; + u8 sftd; + u8 unknown6[3]; + u8 wtled; + u8 rxled; + u8 txled; + u8 unknown7[5]; + u8 save; + u8 unknown8; + u8 displayab:1, + unknown1:2, + fmradio:1, + alarm:1, + unknown2:1, + reset:1, + menu:1; + u8 vfomrlock; + u8 workmode; + u8 keylock; + u8 workmode_channel; + u8 password[6]; + u8 unknown10[11]; +} settings; + +#seekto 0x0E7E; +struct { + u8 mrcha; + u8 mrchb; +} wmchannel; + +#seekto 0x0F10; +struct { + u8 freq[8]; + u8 unknown1; + u8 offset[4]; + u8 unknown2; + ul16 rxtone; + ul16 txtone; + u8 unused1:7, + band:1; + u8 unknown3; + u8 unused2:2, + sftd:2, + scode:4; + u8 unknown4; + u8 unused3:1 + step:3, + unused4:4; + u8 txpower:1, + widenarr:1, + unknown5:6; +} vfoa; + +#seekto 0x0F30; +struct { + u8 freq[8]; + u8 unknown1; + u8 offset[4]; + u8 unknown2; + ul16 rxtone; + ul16 txtone; + u8 unused1:7, + band:1; + u8 unknown3; + u8 unused2:2, + sftd:2, + scode:4; + u8 unknown4; + u8 unused3:1 + step:3, + unused4:4; + u8 txpower:1, + widenarr:1, + unknown5:6; +} vfob; + +#seekto 0x0F57; +u8 fm_preset; + +#seekto 0x1008; +struct { + char name[6]; + u8 unknown2[10]; +} names[128]; + +#seekto 0x%04X; +struct { + char line1[7]; + char line2[7]; +} poweron_msg; + +#seekto 0x1838; +struct { + char line1[7]; + char line2[7]; +} firmware_msg; + +#seekto 0x1849; +u8 power_vhf_hi[14]; // 136-174 MHz, 3 MHz divisions +u8 power_uhf_hi[14]; // 400-470 MHz, 5 MHz divisions +#seekto 0x1889; +u8 power_vhf_lo[14]; +u8 power_uhf_lo[14]; + +struct limit { + u8 enable; + bbcd lower[2]; + bbcd upper[2]; +}; + +#seekto 0x1908; +struct { + struct limit vhf; + u8 unk11[11]; + struct limit uhf; +} limits; + +""" + + +@directory.register +class BaojieBJUV55Radio(uv5r.BaofengUV5R): + VENDOR = "Baojie" + MODEL = "BJ-UV55" + _basetype = ["BJ55"] + _idents = [BJUV55_MODEL] + _mem_params = (0x1928 # poweron_msg offset + ) + _fw_ver_file_start = 0x1938 + _fw_ver_file_stop = 0x193E + + def get_features(self): + rf = super(BaojieBJUV55Radio, self).get_features() + rf.valid_name_length = 6 + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap) + + def set_memory(self, mem): + super(BaojieBJUV55Radio, self).set_memory(mem) + _mem = self._memobj.memory[mem.number] + if (mem.freq - mem.offset) > (400 * 1000000): + _mem.isuhf = True + else: + _mem.isuhf = False + if mem.tmode in ["Tone", "TSQL"]: + _mem.txtoneicon = True + else: + _mem.txtoneicon = False + + def _get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + group = RadioSettings(basic, advanced) + + rs = RadioSetting("squelch", "Carrier Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueInteger(0, 4, _settings.save)) + basic.append(rs) + + rs = RadioSetting("abr", "Backlight", + RadioSettingValueBoolean(_settings.abr)) + basic.append(rs) + + rs = RadioSetting("tdr", "Dual Watch (BDR)", + RadioSettingValueBoolean(_settings.tdr)) + advanced.append(rs) + + rs = RadioSetting("tdrab", "Dual Watch TX Priority", + RadioSettingValueList( + uv5r.TDRAB_LIST, + uv5r.TDRAB_LIST[_settings.tdrab])) + advanced.append(rs) + + rs = RadioSetting("alarm", "Alarm", + RadioSettingValueBoolean(_settings.alarm)) + advanced.append(rs) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + rs = RadioSetting("timeout", "Timeout Timer", + RadioSettingValueList( + uv5r.TIMEOUT_LIST, + uv5r.TIMEOUT_LIST[_settings.timeout])) + basic.append(rs) + + rs = RadioSetting("screv", "Scan Resume", + RadioSettingValueList( + uv5r.RESUME_LIST, + uv5r.RESUME_LIST[_settings.screv])) + advanced.append(rs) + + rs = RadioSetting("mdfa", "Display Mode (A)", + RadioSettingValueList( + uv5r.MODE_LIST, uv5r.MODE_LIST[_settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("mdfb", "Display Mode (B)", + RadioSettingValueList( + uv5r.MODE_LIST, uv5r.MODE_LIST[_settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueBoolean(_settings.bcl)) + advanced.append(rs) + + rs = RadioSetting("autolk", "Automatic Key Lock", + RadioSettingValueBoolean(_settings.autolk)) + advanced.append(rs) + + rs = RadioSetting("fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_settings.fmradio)) + advanced.append(rs) + + rs = RadioSetting("wtled", "Standby LED Color", + RadioSettingValueList( + COLOR_LIST, COLOR_LIST[_settings.wtled])) + basic.append(rs) + + rs = RadioSetting("rxled", "RX LED Color", + RadioSettingValueList( + COLOR_LIST, COLOR_LIST[_settings.rxled])) + basic.append(rs) + + rs = RadioSetting("txled", "TX LED Color", + RadioSettingValueList( + COLOR_LIST, COLOR_LIST[_settings.txled])) + basic.append(rs) + + rs = RadioSetting("reset", "RESET Menu", + RadioSettingValueBoolean(_settings.reset)) + advanced.append(rs) + + rs = RadioSetting("menu", "All Menus", + RadioSettingValueBoolean(_settings.menu)) + advanced.append(rs) + + if len(self._mmap.get_packed()) == 0x1808: + # Old image, without aux block + return group + + other = RadioSettingGroup("other", "Other Settings") + group.append(other) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = self._memobj.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString(0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString(0, 7, _filter(_msg.line2))) + other.append(rs) + + limit = "limits" + vhf_limit = getattr(self._memobj, limit).vhf + rs = RadioSetting("%s.vhf.lower" % limit, "VHF Lower Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, vhf_limit.lower)) + other.append(rs) + + rs = RadioSetting("%s.vhf.upper" % limit, "VHF Upper Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, vhf_limit.upper)) + other.append(rs) + + rs = RadioSetting("%s.vhf.enable" % limit, "VHF TX Enabled", + RadioSettingValueBoolean(vhf_limit.enable)) + other.append(rs) + + uhf_limit = getattr(self._memobj, limit).uhf + rs = RadioSetting("%s.uhf.lower" % limit, "UHF Lower Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, uhf_limit.lower)) + other.append(rs) + rs = RadioSetting("%s.uhf.upper" % limit, "UHF Upper Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, uhf_limit.upper)) + other.append(rs) + rs = RadioSetting("%s.uhf.enable" % limit, "UHF TX Enabled", + RadioSettingValueBoolean(uhf_limit.enable)) + other.append(rs) + + workmode = RadioSettingGroup("workmode", "Work Mode Settings") + group.append(workmode) + + options = ["A", "B"] + rs = RadioSetting("displayab", "Display Selected", + RadioSettingValueList( + options, options[_settings.displayab])) + workmode.append(rs) + + options = ["Frequency", "Channel"] + rs = RadioSetting("workmode", "VFO/MR Mode", + RadioSettingValueList( + options, options[_settings.workmode])) + workmode.append(rs) + + rs = RadioSetting("keylock", "Keypad Lock", + RadioSettingValueBoolean(_settings.keylock)) + workmode.append(rs) + + _mrcna = self._memobj.wmchannel.mrcha + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(0, 127, _mrcna)) + workmode.append(rs) + + _mrcnb = self._memobj.wmchannel.mrchb + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(0, 127, _mrcnb)) + workmode.append(rs) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + value = chirp_common.parse_freq(value) + if 17400000 <= value and value < 40000000: + raise InvalidValueError("Can't be between 174.00000-400.00000") + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + obj.band = value >= 40000000 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString( + 0, 10, convert_bytes_to_freq(self._memobj.vfoa.freq)) + val1a.set_validate_callback(my_validate) + rs = RadioSetting("vfoa.freq", "VFO A Frequency", val1a) + rs.set_apply_callback(apply_freq, self._memobj.vfoa) + workmode.append(rs) + + val1b = RadioSettingValueString( + 0, 10, convert_bytes_to_freq(self._memobj.vfob.freq)) + val1b.set_validate_callback(my_validate) + rs = RadioSetting("vfob.freq", "VFO B Frequency", val1b) + rs.set_apply_callback(apply_freq, self._memobj.vfob) + workmode.append(rs) + + options = ["Off", "+", "-"] + rs = RadioSetting("vfoa.sftd", "VFO A Shift", + RadioSettingValueList( + options, options[self._memobj.vfoa.sftd])) + workmode.append(rs) + + rs = RadioSetting("vfob.sftd", "VFO B Shift", + RadioSettingValueList( + options, options[self._memobj.vfob.sftd])) + workmode.append(rs) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + for byte in bytes: + real_offset = (real_offset * 10) + byte + return chirp_common.format_freq(real_offset * 10000) + + def apply_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10000 + for i in range(3, -1, -1): + obj.offset[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(self._memobj.vfoa.offset)) + rs = RadioSetting("vfoa.offset", "VFO A Offset (0.00-69.95)", val1a) + rs.set_apply_callback(apply_offset, self._memobj.vfoa) + workmode.append(rs) + + val1b = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(self._memobj.vfob.offset)) + rs = RadioSetting("vfob.offset", "VFO B Offset (0.00-69.95)", val1b) + rs.set_apply_callback(apply_offset, self._memobj.vfob) + workmode.append(rs) + + options = ["High", "Low"] + rs = RadioSetting("vfoa.txpower", "VFO A Power", + RadioSettingValueList( + options, options[self._memobj.vfoa.txpower])) + workmode.append(rs) + + rs = RadioSetting("vfob.txpower", "VFO B Power", + RadioSettingValueList( + options, options[self._memobj.vfob.txpower])) + workmode.append(rs) + + options = ["Wide", "Narrow"] + rs = RadioSetting("vfoa.widenarr", "VFO A Bandwidth", + RadioSettingValueList( + options, options[self._memobj.vfoa.widenarr])) + workmode.append(rs) + + rs = RadioSetting("vfob.widenarr", "VFO B Bandwidth", + RadioSettingValueList( + options, options[self._memobj.vfob.widenarr])) + workmode.append(rs) + + options = ["%s" % x for x in range(1, 16)] + rs = RadioSetting("vfoa.scode", "VFO A PTT-ID", + RadioSettingValueList( + options, options[self._memobj.vfoa.scode])) + workmode.append(rs) + + rs = RadioSetting("vfob.scode", "VFO B PTT-ID", + RadioSettingValueList( + options, options[self._memobj.vfob.scode])) + workmode.append(rs) + + rs = RadioSetting("vfoa.step", "VFO A Tuning Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[self._memobj.vfoa.step])) + workmode.append(rs) + rs = RadioSetting("vfob.step", "VFO B Tuning Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[self._memobj.vfob.step])) + workmode.append(rs) + + fm_preset = RadioSettingGroup("fm_preset", "FM Radio Preset") + group.append(fm_preset) + + preset = self._memobj.fm_preset / 10.0 + 87 + rs = RadioSetting("fm_preset", "FM Preset(MHz)", + RadioSettingValueFloat(87, 107.5, preset, 0.1, 1)) + fm_preset.append(rs) + + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + group.append(dtmf) + dtmfchars = "0123456789 *#ABCD" + + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("pttid/%i.code" % i, + "PTT ID Code %i" % (i + 1), val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 5): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, self._memobj.pttid[i]) + dtmf.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("ani.code", "ANI Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 5): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmf.append(rs) + + options = ["Off", "BOT", "EOT", "Both"] + rs = RadioSetting("ani.aniid", "ANI ID", + RadioSettingValueList( + options, options[self._memobj.ani.aniid])) + dtmf.append(rs) + + _codeobj = self._memobj.ani.alarmcode + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("ani.alarmcode", "Alarm Code", val) + + def apply_code(setting, obj): + alarmcode = [] + for j in range(5): + try: + alarmcode.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + alarmcode.append(0xFF) + obj.alarmcode = alarmcode + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmf.append(rs) + + rs = RadioSetting("dtmfst", "DTMF Sidetone", + RadioSettingValueList( + uv5r.DTMFST_LIST, + uv5r.DTMFST_LIST[_settings.dtmfst])) + dtmf.append(rs) + + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList( + uv5r.DTMFSPEED_LIST, + uv5r.DTMFSPEED_LIST[self._memobj.ani.dtmfon])) + dtmf.append(rs) + + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList( + uv5r.DTMFSPEED_LIST, + uv5r.DTMFSPEED_LIST[self._memobj.ani.dtmfoff])) + dtmf.append(rs) + + return group + + def _set_fm_preset(self, settings): + for element in settings: + try: + val = element.value + value = int(val.get_value() * 10 - 870) + LOG.debug("Setting fm_preset = %s" % (value)) + self._memobj.fm_preset = value + except Exception, e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/boblov_x3plus.py b/chirp/drivers/boblov_x3plus.py new file mode 100644 index 0000000..dd861e8 --- /dev/null +++ b/chirp/drivers/boblov_x3plus.py @@ -0,0 +1,572 @@ +""" +Radio driver for the Boblov X3 Plus Motorcycle Helmet Radio +""" +# Copyright 2018 Robert C Jennings +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import struct +import time + +from datetime import datetime +from textwrap import dedent + +from chirp import ( + bitwise, + chirp_common, + directory, + errors, + memmap, + util, +) +from chirp.settings import ( + RadioSetting, + RadioSettingGroup, + RadioSettings, + RadioSettingValueBoolean, + RadioSettingValueInteger, + RadioSettingValueList, +) + +LOG = logging.getLogger(__name__) + + +@directory.register +class BoblovX3Plus(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """Boblov X3 Plus motorcycle/cycling helmet radio""" + + VENDOR = 'Boblov' + MODEL = 'X3Plus' + BAUD_RATE = 9600 + CHANNELS = 16 + + MEM_FORMAT = """ + #seekto 0x0010; + struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown1:1, + compander:1, + scramble:1, + skip:1, + highpower:1, + narrow:1, + unknown2:1, + bcl:1; + u8 unknown3[3]; + } memory[16]; + #seekto 0x03C0; + struct { + u8 unknown1:4, + voiceprompt:2, + batterysaver:1, + beep:1; + u8 squelchlevel; + u8 unknown2; + u8 timeouttimer; + u8 voxlevel; + u8 unknown3; + u8 unknown4; + u8 voxdelay; + } settings; + """ + + # Radio command data + CMD_ACK = '\x06' + CMD_IDENTIFY = '\x02' + CMD_PROGRAM_ENTER = '.VKOGRAM' + CMD_PROGRAM_EXIT = '\x62' # 'b' + CMD_READ = 'R' + CMD_WRITE = 'W' + + BLOCK_SIZE = 0x08 + + VOICE_LIST = ['Off', 'Chinese', 'English'] + TIMEOUTTIMER_LIST = ['Off', '30 seconds', '60 seconds', '90 seconds', + '120 seconds', '150 seconds', '180 seconds', + '210 seconds', '240 seconds', '270 seconds', + '300 seconds'] + VOXLEVEL_LIST = ['Off', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + VOXDELAY_LIST = ['1 seconds', '2 seconds', + '3 seconds', '4 seconds', '5 seconds'] + X3P_POWER_LEVELS = [chirp_common.PowerLevel('Low', watts=0.5), + chirp_common.PowerLevel('High', watts=2.00)] + + _memsize = 0x03F0 + _ranges = [ + (0x0000, 0x03F0), + ] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = _(dedent("""\ + The X3Plus driver is currently experimental. + + There are no known issues but you should proceed with caution. + + Please save an unedited copy of your first successful + download to a CHIRP Radio Images (*.img) file. + """)) + return rp + + @classmethod + def match_model(cls, filedata, filename): + """Given contents of a stored file (@filedata), return True if + this radio driver handles the represented model""" + + if len(filedata) != cls._memsize: + LOG.debug('Boblov_x3plus: match_model: size mismatch') + return False + + LOG.debug('Boblov_x3plus: match_model: size matches') + + if 'P310' in filedata[0x03D0:0x03D8]: + LOG.debug('Boblov_x3plus: match_model: radio ID matches') + return True + + LOG.debug('Boblov_x3plus: match_model: no radio ID match') + return False + + def get_features(self): + """Return a RadioFeatures object for this radio""" + + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.valid_modes = ['NFM', 'FM'] # 12.5 KHz, 25 kHz. + rf.valid_power_levels = self.X3P_POWER_LEVELS + rf.valid_skips = ['', 'S'] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_duplexes = ['', '-', '+', 'split', 'off'] + rf.can_odd_split = True + rf.has_rx_dtcs = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_cross_modes = [ + 'Tone->Tone', + 'DTCS->', + '->DTCS', + 'Tone->DTCS', + 'DTCS->Tone', + '->Tone', + 'DTCS->DTCS'] + rf.has_tuning_step = False + rf.has_bank = False + rf.has_name = False + rf.memory_bounds = (1, self.CHANNELS) + rf.valid_bands = [(400000000, 470000000)] + return rf + + def process_mmap(self): + """Process a newly-loaded or downloaded memory map""" + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def sync_in(self): + "Initiate a radio-to-PC clone operation" + + LOG.debug('Cloning from radio') + status = chirp_common.Status() + status.msg = 'Cloning from radio' + status.cur = 0 + status.max = self._memsize + self.status_fn(status) + + self._enter_programming_mode() + + data = '' + for addr in range(0, self._memsize, self.BLOCK_SIZE): + status.cur = addr + self.BLOCK_SIZE + self.status_fn(status) + + block = self._read_block(addr, self.BLOCK_SIZE) + data += block + + LOG.debug('Address: %04x', addr) + LOG.debug(util.hexprint(block)) + + self._exit_programming_mode() + + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + "Initiate a PC-to-radio clone operation" + + LOG.debug('Upload to radio') + status = chirp_common.Status() + status.msg = 'Uploading to radio' + status.cur = 0 + status.max = self._memsize + self.status_fn(status) + + self._enter_programming_mode() + + for start_addr, end_addr in self._ranges: + for addr in range(start_addr, end_addr, self.BLOCK_SIZE): + status.cur = addr + self.BLOCK_SIZE + self.status_fn(status) + + self._write_block(addr, self.BLOCK_SIZE) + + self._exit_programming_mode() + + def get_raw_memory(self, number): + """Return a raw string describing the memory at @number""" + return repr(self._memobj.memory[number - 1]) + + @staticmethod + def _decode_tone(val): + val = int(val) + if val == 16665: + return '', None, None + elif val >= 12000: + return 'DTCS', val - 12000, 'R' + elif val >= 8000: + return 'DTCS', val - 8000, 'N' + + return 'Tone', val / 10.0, None + + @staticmethod + def _encode_tone(memval, mode, value, pol): + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception('Internal error: invalid mode `%s`' % mode) + + def get_memory(self, number): + """Return a Memory object for the memory at location @number""" + try: + rmem = self._memobj.memory[number - 1] + except KeyError: + raise errors.InvalidMemoryLocation('Unknown channel %s' % number) + + if number < 1 or number > self.CHANNELS: + raise errors.InvalidMemoryLocation( + 'Channel number must be 1 and %s' % self.CHANNELS) + + mem = chirp_common.Memory() + mem.number = number + mem.freq = int(rmem.rxfreq) * 10 + + # A blank (0MHz) or 0xFFFFFFFF frequency is considered empty + if mem.freq == 0 or rmem.rxfreq.get_raw() == '\xFF\xFF\xFF\xFF': + LOG.debug('empty channel %d', number) + mem.freq = 0 + mem.empty = True + return mem + + if rmem.txfreq.get_raw() == '\xFF\xFF\xFF\xFF': + mem.duplex = 'off' + mem.offset = 0 + elif int(rmem.rxfreq) == int(rmem.txfreq): + mem.duplex = '' + mem.offset = 0 + else: + mem.duplex = '-' if int(rmem.rxfreq) > int(rmem.txfreq) else '+' + mem.offset = abs(int(rmem.rxfreq) - int(rmem.txfreq)) * 10 + + mem.mode = 'NFM' if rmem.narrow else 'FM' + mem.skip = 'S' if rmem.skip else '' + mem.power = self.X3P_POWER_LEVELS[rmem.highpower] + + txtone = self._decode_tone(rmem.txtone) + rxtone = self._decode_tone(rmem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.extra = RadioSettingGroup('Extra', 'extra') + mem.extra.append(RadioSetting('bcl', 'Busy Channel Lockout', + RadioSettingValueBoolean( + current=(not rmem.bcl)))) + mem.extra.append(RadioSetting('scramble', 'Scramble', + RadioSettingValueBoolean( + current=(not rmem.scramble)))) + mem.extra.append(RadioSetting('compander', 'Compander', + RadioSettingValueBoolean( + current=(not rmem.compander)))) + + return mem + + def set_memory(self, memory): + """Set the memory object @memory""" + rmem = self._memobj.memory[memory.number - 1] + + if memory.empty: + rmem.set_raw('\xFF' * (rmem.size() // 8)) + return + + rmem.rxfreq = memory.freq / 10 + + set_txtone = True + if memory.duplex == 'off': + for i in range(0, 4): + rmem.txfreq[i].set_raw('\xFF') + # If recieve only then txtone value should be none + self._encode_tone(rmem.txtone, mode='', value=None, pol=None) + set_txtone = False + elif memory.duplex == 'split': + rmem.txfreq = memory.offset / 10 + elif memory.duplex == '+': + rmem.txfreq = (memory.freq + memory.offset) / 10 + elif memory.duplex == '-': + rmem.txfreq = (memory.freq - memory.offset) / 10 + else: + rmem.txfreq = memory.freq / 10 + + txtone, rxtone = chirp_common.split_tone_encode(memory) + if set_txtone: + self._encode_tone(rmem.txtone, *txtone) + self._encode_tone(rmem.rxtone, *rxtone) + + rmem.narrow = 'N' in memory.mode + rmem.skip = memory.skip == 'S' + + for setting in memory.extra: + # NOTE: Only three settings right now, all are inverted + setattr(rmem, setting.get_name(), not int(setting.value)) + + def get_settings(self): + """ + Return a RadioSettings list containing one or more RadioSettingGroup + or RadioSetting objects. These represent general settings that can + be adjusted on the radio. + """ + cur = self._memobj.settings + basic = RadioSettingGroup('basic', 'Basic Settings') + rs = RadioSetting('squelchlevel', 'Squelch level', + RadioSettingValueInteger( + minval=0, maxval=9, + current=cur.squelchlevel)) + basic.append(rs) + rs = RadioSetting('timeouttimer', 'Timeout timer', + RadioSettingValueList( + options=self.TIMEOUTTIMER_LIST, + current=self.TIMEOUTTIMER_LIST[cur.timeouttimer])) + basic.append(rs) + rs = RadioSetting('voiceprompt', 'Voice prompt', + RadioSettingValueList( + options=self.VOICE_LIST, + current=self.VOICE_LIST[cur.voiceprompt])) + basic.append(rs) + rs = RadioSetting('voxlevel', 'Vox level', + RadioSettingValueList( + options=self.VOXLEVEL_LIST, + current=self.VOXLEVEL_LIST[cur.voxlevel])) + basic.append(rs) + rs = RadioSetting('voxdelay', 'VOX delay', + RadioSettingValueList( + options=self.VOXDELAY_LIST, + current=self.VOXDELAY_LIST[cur.voxdelay])) + basic.append(rs) + basic.append(RadioSetting('batterysaver', 'Battery saver', + RadioSettingValueBoolean( + current=cur.batterysaver))) + basic.append(RadioSetting('beep', 'Beep', + RadioSettingValueBoolean( + current=cur.beep))) + return RadioSettings(basic) + + def set_settings(self, settings): + """ + Accepts the top-level RadioSettingGroup returned from + get_settings() and adjusts the values in the radio accordingly. + This function expects the entire RadioSettingGroup hierarchy + returned from get_settings(). + """ + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if '.' in element.get_name(): + bits = element.get_name().split('.') + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug('Using apply callback') + element.run_apply_callback() + else: + LOG.debug('Setting %s = %s', setting, element.value) + setattr(obj, setting, element.value) + except Exception: + LOG.debug(element.get_name()) + raise + + def _write(self, data, timeout=3): + """ + Write data to the serial port and consume the echoed response + + The radio echos the data it is sent before replying. Send the + data to the radio, consume the reply, and ensure that the reply + is the same as the data sent. + """ + + serial = self.pipe + expected = len(data) + resp = b'' + start = datetime.now() + + # LOG.debug('WRITE(%02d): %s', expected, util.hexprint(data).rstrip()) + serial.write(data) + while True: + if not expected: + break + rbytes = serial.read(expected) + resp += rbytes + expected -= len(rbytes) + if (datetime.now() - start).seconds > timeout: + raise errors.RadioError('Timeout while reading from radio') + if resp != data: + raise errors.RadioError('Echoed response did not match sent data') + + def _read(self, length, timeout=3): + """Read data from the serial port""" + + resp = b'' + serial = self.pipe + remaining = length + start = datetime.now() + + if not remaining: + return resp + + while True: + rbytes = serial.read(remaining) + resp += rbytes + remaining -= len(rbytes) + if not remaining: + break + if (datetime.now() - start).seconds > timeout: + raise errors.RadioError('Timeout while reading from radio') + time.sleep(0.1) + + # LOG.debug('READ(%02d): %s', length, util.hexprint(resp).rstrip()) + return resp + + def _read_block(self, block_addr, block_size): + + LOG.debug('Reading block %04x...', block_addr) + cmd = struct.pack('>cHb', self.CMD_READ, block_addr, + block_size) + resp_prefix = self.CMD_WRITE + cmd[1:] + + try: + msg = ('Failed to write command to radio for block ' + 'read at %04x' % block_addr) + self._write(cmd) + + msg = ('Failed to read response from radio for block ' + 'read at %04x' % block_addr) + response = self._read(len(cmd) + block_size) + + if response[:len(cmd)] != resp_prefix: + raise errors.RadioError('Error reading block %04x, ' + 'Command not returned.' % (block_addr)) + + msg = ('Failed to write ACK to radio after block read at ' + '%04x' % block_addr) + self._write(self.CMD_ACK) + + msg = ('Failed to read ACK from radio after block read at ' + '%04x' % block_addr) + ack = self._read(1) + except Exception: + LOG.debug(msg, exc_info=True) + raise errors.RadioError(msg) + + if ack != self.CMD_ACK: + raise errors.RadioError('No ACK reading block ' + '%04x.' % (block_addr)) + + return response[len(cmd):] + + def _write_block(self, block_addr, block_size): + + cmd = struct.pack('>cHb', self.CMD_WRITE, block_addr, block_size) + data = self.get_mmap()[block_addr:block_addr + 8] + + LOG.debug('Writing Data:\n%s%s', + util.hexprint(cmd), util.hexprint(data)) + + try: + self._write(cmd + data) + if self._read(1) != self.CMD_ACK: + raise Exception('No ACK') + except Exception: + msg = 'Failed to send block to radio at %04x' % block_addr + LOG.debug(msg, exc_info=True) + raise errors.RadioError(msg) + + def _enter_programming_mode(self): + + LOG.debug('Entering programming mode') + try: + msg = 'Error communicating with radio entering programming mode.' + self._write(self.CMD_PROGRAM_ENTER) + time.sleep(0.5) + ack = self._read(1) + + if not ack: + raise errors.RadioError('No response from radio') + elif ack != self.CMD_ACK: + raise errors.RadioError('Radio refused to enter ' + 'programming mode') + + msg = 'Error communicating with radio during identification' + self._write(self.CMD_IDENTIFY) + ident = self._read(8) + + if not ident.startswith('SMP558'): + LOG.debug(util.hexprint(ident)) + raise errors.RadioError('Radio returned unknown ID string') + + msg = ('Error communicating with radio while querying ' + 'model identifier') + self._write(self.CMD_ACK) + + msg = 'Error communicating with radio on final handshake' + ack = self._read(1) + + if ack != self.CMD_ACK: + raise errors.RadioError('Radio refused to enter programming ' + 'mode failed on final handshake.') + except Exception: + LOG.debug(msg, exc_info=True) + raise errors.RadioError(msg) + + def _exit_programming_mode(self): + try: + self._write(self.CMD_PROGRAM_EXIT) + except Exception: + msg = 'Radio refused to exit programming mode' + LOG.debug(msg, exc_info=True) + raise errors.RadioError(msg) + LOG.debug('Exited programming mode') diff --git a/chirp/drivers/btech.py b/chirp/drivers/btech.py new file mode 100644 index 0000000..dca1560 --- /dev/null +++ b/chirp/drivers/btech.py @@ -0,0 +1,4195 @@ +# Copyright 2016-2017: +# * Pavel Milanes CO7WT, +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import struct +import time +import logging + +from time import sleep +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +# A note about the memmory in these radios +# +# The real memory of these radios extends to 0x4000 +# On read the factory software only uses up to 0x3200 +# On write it just uploads the contents up to 0x3100 +# +# The mem beyond 0x3200 holds the ID data + +MEM_SIZE = 0x4000 +BLOCK_SIZE = 0x40 +TX_BLOCK_SIZE = 0x10 +ACK_CMD = b"\x06" +MODES = ["FM", "NFM"] +SKIP_VALUES = ["S", ""] +TONES = chirp_common.TONES +DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +# lists related to "extra" settings +PTTID_LIST = ["OFF", "BOT", "EOT", "BOTH"] +PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)] +OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"] +SPMUTE_LIST = ["Tone/DTCS", "Tone/DTCS and Optsig", "Tone/DTCS or Optsig"] + +# lists +LIST_AB = ["A", "B"] +LIST_ABCD = LIST_AB + ["C", "D"] +LIST_ANIL = ["3", "4", "5"] +LIST_APO = ["Off"] + ["%s minutes" % x for x in range(30, 330, 30)] +LIST_COLOR4 = ["Off", "Blue", "Orange", "Purple"] +LIST_COLOR7 = ["White", "Red", "Blue", "Green", "Yellow", "Indego", + "Purple", "Gray"] +LIST_COLOR8 = ["Black"] + LIST_COLOR7 +LIST_DTMFST = ["OFF", "Keyboard", "ANI", "Keyboad + ANI"] +LIST_EMCTP = ["TX alarm sound", "TX ANI", "Both"] +LIST_EMCTPX = ["Off"] + LIST_EMCTP +LIST_LANGUA = ["English", "Chinese"] +LIST_MDF = ["Frequency", "Channel", "Name"] +LIST_OFF1TO9 = ["Off"] + ["%s seconds" % x for x in range(1, 10)] +LIST_OFF1TO10 = ["Off"] + ["%s seconds" % x for x in range(1, 11)] +LIST_OFF1TO50 = ["Off"] + ["%s seconds" % x for x in range(1, 51)] +LIST_PONMSG = ["Full", "Message", "Battery voltage"] +LIST_REPM = ["Off", "Carrier", "CTCSS or DCS", "Tone", "DTMF"] +LIST_REPS = ["1000 Hz", "1450 Hz", "1750 Hz", "2100Hz"] +LIST_RPTDL = ["Off"] + ["%s ms" % x for x in range(1, 10)] +LIST_SCMODE = ["Off", "PTT-SC", "MEM-SC", "PON-SC"] +LIST_SHIFT = ["Off", "+", "-"] +LIST_SKIPTX = ["Off", "Skip 1", "Skip 2"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0] +LIST_STEP = [str(x) for x in STEPS] +LIST_SYNC = ["Off", "AB", "CD", "AB+CD"] +# the first 12 TMR choices common to all color display mobile radios +LIST_TMR12 = ["OFF", "M+A", "M+B", "M+C", "M+D", "M+A+B", "M+A+C", "M+A+D", + "M+B+C", "M+B+D", "M+C+D", "M+A+B+C"] +# the 16 choice list for color display mobile radios that correctly implement +# the full 16 TMR choices +LIST_TMR16 = LIST_TMR12 + ["M+A+B+D", "M+A+C+D", "M+B+C+D", "A+B+C+D"] +# the 15 choice list for color mobile radios that are missing the M+A+B+D +# choice in the TMR menu +LIST_TMR15 = LIST_TMR12 + ["M+A+C+D", "M+B+C+D", "A+B+C+D"] +LIST_TOT = ["%s sec" % x for x in range(15, 615, 15)] +LIST_TXDISP = ["Power", "Mic Volume"] +LIST_TXP = ["High", "Low"] +LIST_TXP3 = ["High", "Mid", "Low"] +LIST_SCREV = ["TO (timeout)", "CO (carrier operated)", "SE (search)"] +LIST_VFOMR = ["Frequency", "Channel"] +LIST_WIDE = ["Wide", "Narrow"] + +# lists related to DTMF, 2TONE and 5TONE settings +LIST_5TONE_STANDARDS = ["CCIR1", "CCIR2", "PCCIR", "ZVEI1", "ZVEI2", "ZVEI3", + "PZVEI", "DZVEI", "PDZVEI", "EEA", "EIA", "EURO", + "CCITT", "NATEL", "MODAT", "none"] +LIST_5TONE_STANDARDS_without_none = ["CCIR1", "CCIR2", "PCCIR", "ZVEI1", + "ZVEI2", "ZVEI3", + "PZVEI", "DZVEI", "PDZVEI", "EEA", "EIA", + "EURO", "CCITT", "NATEL", "MODAT"] +LIST_5TONE_STANDARD_PERIODS = ["20", "30", "40", "50", "60", "70", "80", "90", + "100", "110", "120", "130", "140", "150", "160", + "170", "180", "190", "200"] +LIST_5TONE_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", + "B", "C", "D", "E", "F"] +LIST_5TONE_DELAY = ["%s ms" % x for x in range(0, 1010, 10)] +LIST_5TONE_RESET = ["%s ms" % x for x in range(100, 8100, 100)] +LIST_5TONE_RESET_COLOR = ["%s ms" % x for x in range(100, 20100, 100)] +LIST_DTMF_SPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_DTMF_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", + "C", "D", "#", "*"] +LIST_DTMF_VALUES = [0x0A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0D, 0x0E, 0x0F, 0x00, 0x0C, 0x0B] +LIST_DTMF_SPECIAL_DIGITS = ["*", "#", "A", "B", "C", "D"] +LIST_DTMF_SPECIAL_VALUES = [0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00] +LIST_DTMF_DELAY = ["%s ms" % x for x in range(100, 4100, 100)] +CHARSET_DTMF_DIGITS = "0123456789AaBbCcDd#*" +LIST_2TONE_DEC = ["A-B", "A-C", "A-D", + "B-A", "B-C", "B-D", + "C-A", "C-B", "C-D", + "D-A", "D-B", "D-C"] +LIST_2TONE_RESPONSE = ["None", "Alert", "Transpond", "Alert+Transpond"] + +# This is a general serial timeout for all serial read functions. +# Practice has show that about 0.7 sec will be enough to cover all radios. +STIMEOUT = 0.7 + +# this var controls the verbosity in the debug and by default it's low (False) +# make it True and you will to get a very verbose debug.log +debug = False + +# valid chars on the LCD, Note that " " (space) is stored as "\xFF" +VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" + + +# #### ID strings ##################################################### + +# BTECH UV2501 pre-production units +UV2501pp_fp = b"M2C294" +# BTECH UV2501 pre-production units 2 + and 1st Gen radios +UV2501pp2_fp = b"M29204" +# B-TECH UV-2501 second generation (2G) radios +UV2501G2_fp = b"BTG214" +# B-TECH UV-2501 third generation (3G) radios +UV2501G3_fp = b"BTG324" + +# B-TECH UV-2501+220 pre-production units +UV2501_220pp_fp = b"M3C281" +# extra block read for the 2501+220 pre-production units +# the same for all of this radios so far +UV2501_220pp_id = b" 280528" +# B-TECH UV-2501+220 +UV2501_220_fp = b"M3G201" +# new variant, let's call it Generation 2 +UV2501_220G2_fp = b"BTG211" +# B-TECH UV-2501+220 third generation (3G) +UV2501_220G3_fp = b"BTG311" + +# B-TECH UV-5001 pre-production units + 1st Gen radios +UV5001pp_fp = b"V19204" +# B-TECH UV-5001 alpha units +UV5001alpha_fp = b"V28204" +# B-TECH UV-5001 second generation (2G) radios +UV5001G2_fp = b"BTG214" +# B-TECH UV-5001 second generation (2G2) +UV5001G22_fp = b"V2G204" +# B-TECH UV-5001 third generation (3G) +UV5001G3_fp = b"BTG304" + +# B-TECH UV-25X2 +UV25X2_fp = b"UC2012" + +# B-TECH UV-25X4 +UV25X4_fp = b"UC4014" + +# B-TECH UV-50X2 +UV50X2_fp = b"UC2M12" + +# B-TECH GMRS-50X1 +GMRS50X1_fp = "NC1802" +GMRS50X1_fp1 = "NC1932" + +# special var to know when we found a BTECH Gen 3 +BTECH3 = [UV2501G3_fp, UV2501_220G3_fp, UV5001G3_fp] + + +# WACCOM Mini-8900 +MINI8900_fp = b"M28854" + + +# QYT KT-UV980 +KTUV980_fp = b"H28854" + +# QYT KT8900 +KT8900_fp = b"M29154" +# New generations KT8900 +KT8900_fp1 = b"M2C234" +KT8900_fp2 = b"M2G1F4" +KT8900_fp3 = b"M2G2F4" +KT8900_fp4 = b"M2G304" +KT8900_fp5 = b"M2G314" +# this radio has an extra ID +KT8900_id = b"303688" + +# KT8900R +KT8900R_fp = b"M3G1F4" +# Second Generation +KT8900R_fp1 = b"M3G214" +# another model +KT8900R_fp2 = b"M3C234" +# another model G4? +KT8900R_fp3 = b"M39164" +# another model +KT8900R_fp4 = b"M3G314" +# this radio has an extra ID +KT8900R_id = b"280528" +# another extra ID in dec/2018 +KT8900R_id2 = b"\x05\x58\x3d\xf0\x10" + +# KT7900D (quad band) +KT7900D_fp = b"VC4004" +KT7900D_fp1 = b"VC4284" +KT7900D_fp2 = b"VC4264" + +# QB25 (quad band) - a clone of KT7900D +QB25_fp = b"QB-25" + +# KT8900D (dual band) +KT8900D_fp = b"VC2002" +KT8900D_fp1 = b"VC8632" + +# LUITON LT-588UV +LT588UV_fp = b"V2G1F4" +# Added by rstrickoff gen 2 id +LT588UV_fp1 = b"V2G214" + + +# ### MAGICS +# for the Waccom Mini-8900 +MSTRING_MINI8900 = b"\x55\xA5\xB5\x45\x55\x45\x4d\x02" +# for the B-TECH UV-2501+220 (including pre production ones) +MSTRING_220 = b"\x55\x20\x15\x12\x12\x01\x4d\x02" +# for the QYT KT8900 & R +MSTRING_KT8900 = b"\x55\x20\x15\x09\x16\x45\x4D\x02" +MSTRING_KT8900R = b"\x55\x20\x15\x09\x25\x01\x4D\x02" +# magic string for all other models +MSTRING = b"\x55\x20\x15\x09\x20\x45\x4d\x02" +# for the QYT KT7900D & KT8900D +MSTRING_KT8900D = b"\x55\x20\x16\x08\x01\xFF\xDC\x02" +# for the BTECH UV-25X2 and UV-50X2 +MSTRING_UV25X2 = b"\x55\x20\x16\x12\x28\xFF\xDC\x02" +# for the BTECH UV-25X4 +MSTRING_UV25X4 = "\x55\x20\x16\x11\x18\xFF\xDC\x02" +# for the BTECH GMRS-50X1 +MSTRING_GMRS50X1 = "\x55\x20\x18\x10\x18\xFF\xDC\x02" + + +def _clean_buffer(radio): + """Cleaning the read serial buffer, hard timeout to survive an infinite + data stream""" + + # touching the serial timeout to optimize the flushing + # restored at the end to the default value + radio.pipe.timeout = 0.1 + dump = b"1" + datacount = 0 + + try: + while len(dump) > 0: + dump = radio.pipe.read(100) + datacount += len(dump) + # hard limit to survive a infinite serial data stream + # 5 times bigger than a normal rx block (69 bytes) + if datacount > 345: + seriale = "Please check your serial port selection." + raise errors.RadioError(seriale) + + # restore the default serial timeout + radio.pipe.timeout = STIMEOUT + + except Exception: + raise errors.RadioError("Unknown error cleaning the serial buffer") + + +def _rawrecv(radio, amount): + """Raw read from the radio device, less intensive way""" + + data = b"" + + try: + data = radio.pipe.read(amount) + + # DEBUG + if debug is True: + LOG.debug("<== (%d) bytes:\n\n%s" % + (len(data), util.hexprint(data))) + + # fail if no data is received + if len(data) == 0: + raise errors.RadioError("No data received from radio") + + # notice on the logs if short + if len(data) < amount: + LOG.warn("Short reading %d bytes from the %d requested." % + (len(data), amount)) + + except: + raise errors.RadioError("Error reading data from radio") + + return data + + +def _send(radio, data): + """Send data to the radio device""" + + try: + for byte in data: + radio.pipe.write(bytes([byte])) + # Some OS (mainly Linux ones) are too fast on the serial and + # get the MCU inside the radio stuck in the early stages, this + # hits some models more than others. + # + # To cope with that we introduce a delay on the writes. + # Many option have been tested (delaying only after error occures, + # after short reads, only for linux, ...) + # Finally, a static delay was chosen as simplest of all solutions + # (Michael Wagner, OE4AMW) + # (for details, see issue 3993) + sleep(0.002) + + # DEBUG + if debug is True: + LOG.debug("==> (%d) bytes:\n\n%s" % + (len(data), util.hexprint(data))) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = b"\x06" + struct.pack(">BHB", ord(cmd), addr, length) + # add the data if set + if len(data) != 0: + frame += data + + return frame + + +def _recv(radio, addr): + """Get data from the radio all at once to lower syscalls load""" + + # Get the full 69 bytes at a time to reduce load + # 1 byte ACK + 4 bytes header + 64 bytes of data (BLOCK_SIZE) + + # get the whole block + block = _rawrecv(radio, BLOCK_SIZE + 5) + + # basic check + if len(block) < (BLOCK_SIZE + 5): + raise errors.RadioError("Short read of the block 0x%04x" % addr) + + # checking for the ack + if block[:1] != ACK_CMD: + raise errors.RadioError("Bad ack from radio in block 0x%04x" % addr) + + # header validation + c, a, l = struct.unpack(">BHB", block[1:5]) + if a != addr or l != BLOCK_SIZE or c != ord("X"): + LOG.debug("Invalid header for block 0x%04x" % addr) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l)) + raise errors.RadioError("Invalid header for block 0x%04x:" % addr) + + # return the data + return block[5:] + + +def _start_clone_mode(radio, status): + """Put the radio in clone mode and get the ident string, 3 tries""" + + # cleaning the serial buffer + _clean_buffer(radio) + + # prep the data to show in the UI + status.cur = 0 + status.msg = "Identifying the radio..." + status.max = 3 + radio.status_fn(status) + + try: + for a in range(0, status.max): + # Update the UI + status.cur = a + 1 + radio.status_fn(status) + + # send the magic word + _send(radio, radio._magic) + + # Now you get a x06 of ACK if all goes well + ack = radio.pipe.read(1) + + if ack[:1] == ACK_CMD: + # DEBUG + LOG.info("Magic ACK received") + status.cur = status.max + radio.status_fn(status) + + return True + + return False + + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Error sending Magic to radio:\n%s" % e) + + +def _do_ident(radio, status, upload=False): + """Put the radio in PROGRAM mode & identify it""" + # set the serial discipline + radio.pipe.baudrate = 9600 + radio.pipe.parity = "N" + + # open the radio into program mode + if _start_clone_mode(radio, status) is False: + msg = "Radio did not enter clone mode" + # warning about old versions of QYT KT8900 + if radio.MODEL == "KT8900": + msg += ". You may want to try it as a WACCOM MINI-8900, there is a" + msg += " known variant of this radios that is a clone of it." + raise errors.RadioError(msg) + + # Ok, get the ident string + ident = _rawrecv(radio, 49) + + # basic check for the ident + if len(ident) != 49: + raise errors.RadioError("Radio send a short ident block.") + + # check if ident is OK + itis = False + for fp in radio._fileid: + if fp in ident: + # got it! + itis = True + # checking if we are dealing with a Gen 3 BTECH + if radio.VENDOR == "BTECH" and fp in BTECH3: + radio.btech3 = True + + break + + if itis is False: + LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident)) + raise errors.RadioError("Radio identification failed.") + + # some radios needs a extra read and check for a code on it, this ones + # has the check value in the _id2 var, others simply False + if radio._id2 is not False: + # lower the timeout here as this radios are reseting due to timeout + radio.pipe.timeout = 0.05 + + # query & receive the extra ID + _send(radio, _make_frame("S", 0x3DF0, 16)) + id2 = _rawrecv(radio, 21) + + # WARNING !!!!!! + # different radios send a response with a different amount of data + # it seems that it's padded with \xff, \x20 and some times with \x00 + # we just care about the first 16, our magic string is in there + if len(id2) < 16: + raise errors.RadioError("The extra ID is short, aborting.") + + # ok, the correct string must be in the received data + # the radio._id2 var will be always a list + flag2 = False + for _id2 in radio._id2: + if _id2 in id2: + flag2 = True + + if not flag2: + LOG.debug("Full *BAD* extra ID on the %s is: \n%s" % + (radio.MODEL, util.hexprint(id2))) + raise errors.RadioError("The extra ID is wrong, aborting.") + + # this radios need a extra request/answer here on the upload + # the amount of data received depends of the radio type + # + # also the first block of TX must no have the ACK at the beginning + # see _upload for this. + if upload is True: + # send an ACK + _send(radio, ACK_CMD) + + # the amount of data depend on the radio, so far we have two radios + # reading two bytes with an ACK at the end and just ONE with just + # one byte (QYT KT8900) + # the JT-6188 appears a clone of the last, but reads TWO bytes. + # + # we will read two bytes with a custom timeout to not penalize the + # users for this. + # + # we just check for a response and last byte being a ACK, that is + # the common stone for all radios (3 so far) + ack = _rawrecv(radio, 2) + + # checking + if len(ack) == 0 or ack[-1:] != ACK_CMD: + raise errors.RadioError("Radio didn't ACK the upload") + + # restore the default serial timeout + radio.pipe.timeout = STIMEOUT + + # DEBUG + LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL)) + + return True + + +def _download(radio): + """Get the memory map""" + + # UI progress + status = chirp_common.Status() + + # put radio in program mode and identify it + _do_ident(radio, status) + + # the models that doesn't have the extra ID have to make a dummy read here + if radio._id2 is False: + _send(radio, _make_frame("S", 0, BLOCK_SIZE)) + discard = _rawrecv(radio, BLOCK_SIZE + 5) + + if debug is True: + LOG.info("Dummy first block read done, got this:\n\n %s", + util.hexprint(discard)) + + # reset the progress bar in the UI + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + status.cur = 0 + radio.status_fn(status) + + # cleaning the serial buffer + _clean_buffer(radio) + + data = bytes(b"") + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # sending the read request + _send(radio, _make_frame("S", addr, BLOCK_SIZE)) + + # read + d = _recv(radio, addr) + + # aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + return data + + +def _upload(radio): + """Upload procedure""" + + # The UPLOAD mem is restricted to lower than 0x3100, + # so we will overide that here localy + MEM_SIZE = radio.UPLOAD_MEM_SIZE + + # UI progress + status = chirp_common.Status() + + # put radio in program mode and identify it + _do_ident(radio, status, True) + + # get the data to upload to radio + data = radio.get_mmap().get_byte_compatible() + + # Reset the UI progress + status.max = MEM_SIZE / TX_BLOCK_SIZE + status.cur = 0 + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # the radios that doesn't have the extra ID 'may' do a dummy write, I found + # that leveraging the bad ACK and NOT doing the dummy write is ok, as the + # dummy write is accepted (it actually writes to the mem!) by the radio. + + # cleaning the serial buffer + _clean_buffer(radio) + + # the fun start here + for addr in range(0, MEM_SIZE, TX_BLOCK_SIZE): + # getting the block of data to send + d = data[addr:addr + TX_BLOCK_SIZE] + + # build the frame to send + frame = _make_frame("X", addr, TX_BLOCK_SIZE, d) + + # first block must not send the ACK at the beginning for the + # ones that has the extra id, since this have to do a extra step + if addr == 0 and radio._id2 is not False: + frame = frame[1:] + + # send the frame + _send(radio, frame) + + # receiving the response + ack = _rawrecv(radio, 1) + + # basic check + if len(ack) != 1: + raise errors.RadioError("No ACK when writing block 0x%04x" % addr) + + if ack not in bytes(b"\x06\x05"): + raise errors.RadioError("Bad ACK writing block 0x%04x:" % addr) + + # UI Update + status.cur = addr / TX_BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x3f70:0x3f76] + + if rid in cls._fileid: + return True + + return False + + +def _decode_ranges(low, high): + """Unpack the data in the ranges zones in the memmap and return + a tuple with the integer corresponding to the Mhz it means""" + ilow = int(low[0]) * 100 + int(low[1]) * 10 + int(low[2]) + ihigh = int(high[0]) * 100 + int(high[1]) * 10 + int(high[2]) + ilow *= 1000000 + ihigh *= 1000000 + + return (ilow, ihigh) + + +def _split(rf, f1, f2): + """Returns False if the two freqs are in the same band (no split) + or True otherwise""" + + # determine if the two freqs are in the same band + for low, high in rf.valid_bands: + if f1 >= low and f1 <= high and \ + f2 >= low and f2 <= high: + # if the two freqs are on the same Band this is not a split + return False + + # if you get here is because the freq pairs are split + return True + + +class BTechMobileCommon(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """BTECH's UV-5001 and alike radios""" + VENDOR = "BTECH" + MODEL = "" + IDENT = "" + BANDS = 2 + COLOR_LCD = False + COLOR_LCD2 = False + NAME_LENGTH = 6 + NEEDS_COMPAT_SERIAL = False + UPLOAD_MEM_SIZE = 0X3100 + _power_levels = [chirp_common.PowerLevel("High", watts=25), + chirp_common.PowerLevel("Low", watts=10)] + _vhf_range = (130000000, 180000000) + _220_range = (200000000, 271000000) + _uhf_range = (400000000, 521000000) + _350_range = (350000000, 391000000) + _upper = 199 + _magic = MSTRING + _fileid = None + _id2 = False + btech3 = False + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental.\n' + '\n' + 'Please keep a copy of your memories with the original software ' + 'if you treasure them, this driver is new and may contain' + ' bugs.\n' + '\n' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + + """)) + rp.pre_upload = _(dedent("""\ + Follow these instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + + """)) + return rp + + def get_features(self): + """Get the radio's features""" + + # we will use the following var as global + global POWER_LEVELS + + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = MODES + rf.valid_characters = VALID_CHARS + rf.valid_name_length = self.NAME_LENGTH + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = SKIP_VALUES + rf.valid_dtcs_codes = DTCS + rf.valid_tuning_steps = STEPS + rf.memory_bounds = (0, self._upper) + + # power levels + POWER_LEVELS = self._power_levels + rf.valid_power_levels = POWER_LEVELS + + # normal dual bands + rf.valid_bands = [self._vhf_range, self._uhf_range] + + # 220 band + if self.BANDS == 3 or self.BANDS == 4: + rf.valid_bands.append(self._220_range) + + # 350 band + if self.BANDS == 4: + rf.valid_bands.append(self._350_range) + + return rf + + def sync_in(self): + """Download from radio""" + data = _download(self) + self._mmap = memmap.MemoryMapBytes(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + _upload(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Error: %s" % e) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def _decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + pol = None + + if val in [0, 65535]: + return '', None, None + elif val > 0x0258: + a = val / 10.0 + return 'Tone', a, pol + else: + if val > 0x69: + index = val - 0x6A + pol = "R" + else: + index = val - 1 + pol = "N" + + tone = DTCS[index] + return 'DTCS', tone, pol + + def _encode_tone(self, memval, mode, val, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '' or mode is None: + memval.set_raw("\x00\x00") + elif mode == 'Tone': + memval.set_value(val * 10) + elif mode == 'DTCS': + # detect the index in the DTCS list + try: + index = DTCS.index(val) + if pol == "N": + index += 1 + else: + index += 0x6A + memval.set_value(index) + except: + msg = "Digital Tone '%d' is not supported" % value + LOG.error(msg) + raise errors.RadioError(msg) + else: + msg = "Internal error: invalid mode '%s'" % mode + LOG.error(msg) + raise errors.InvalidDataError(msg) + + def get_memory(self, number): + """Get the mem representation from the radio image""" + _mem = self._memobj.memory[number] + _names = self._memobj.names[number] + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Memory number + mem.number = number + + if _mem.get_raw()[0] == "\xFF": + mem.empty = True + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[4] == "\xFF": + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX freq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset != 0: + if _split(self.get_features(), mem.freq, int( + _mem.txfreq) * 10): + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + elif offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + # name TAG of the channel + mem.name = str(_names.name).rstrip("\xFF").replace("\xFF", " ") + + # power + mem.power = POWER_LEVELS[int(_mem.power)] + + # wide/narrow + mem.mode = MODES[int(_mem.wide)] + + # skip + mem.skip = SKIP_VALUES[_mem.add] + + # tone data + rxtone = txtone = None + txtone = self._decode_tone(_mem.txtone) + rxtone = self._decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra + mem.extra = RadioSettingGroup("extra", "Extra") + + if not self.COLOR_LCD or \ + (self.COLOR_LCD and not self.VENDOR == "BTECH"): + scramble = RadioSetting("scramble", "Scramble", + RadioSettingValueBoolean(bool( + _mem.scramble))) + mem.extra.append(scramble) + + bcl = RadioSetting("bcl", "Busy channel lockout", + RadioSettingValueBoolean(bool(_mem.bcl))) + mem.extra.append(bcl) + + pttid = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_mem.pttid])) + mem.extra.append(pttid) + + # validating scode + scode = _mem.scode if _mem.scode != 15 else 0 + pttidcode = RadioSetting("scode", "PTT ID signal code", + RadioSettingValueList( + PTTIDCODE_LIST, + PTTIDCODE_LIST[scode])) + mem.extra.append(pttidcode) + + optsig = RadioSetting("optsig", "Optional signaling", + RadioSettingValueList( + OPTSIG_LIST, + OPTSIG_LIST[_mem.optsig])) + mem.extra.append(optsig) + + spmute = RadioSetting("spmute", "Speaker mute", + RadioSettingValueList( + SPMUTE_LIST, + SPMUTE_LIST[_mem.spmute])) + mem.extra.append(spmute) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI""" + # get the eprom representation of this channel + _mem = self._memobj.memory[mem.number] + _names = self._memobj.names[mem.number] + + mem_was_empty = False + # same method as used in get_memory for determining if mem is empty + # doing this BEFORE overwriting it with new values ... + if _mem.get_raw()[0] == "\xFF": + LOG.debug("This mem was empty before") + mem_was_empty = True + + # if empty memmory + if mem.empty: + # the channel itself + _mem.set_raw("\xFF" * 16) + # the name tag + _names.set_raw("\xFF" * 16) + return + + if mem_was_empty: + # Zero the whole memory if we're making it unempty for + # the first time + LOG.debug('Zeroing new memory') + _mem.set_raw('\x00' * 16) + + # frequency + _mem.rxfreq = mem.freq / 10 + + # duplex + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + for i in _mem.txfreq: + i.set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + else: + _mem.txfreq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.txtone, txmode, txtone, txpol) + self._encode_tone(_mem.rxtone, rxmode, rxtone, rxpol) + + # name TAG of the channel + if len(mem.name) < self.NAME_LENGTH: + # we must pad to self.NAME_LENGTH chars, " " = "\xFF" + mem.name = str(mem.name).ljust(self.NAME_LENGTH, " ") + _names.name = str(mem.name).replace(" ", "\xFF") + + # power, # default power level is high + _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power) + + # wide/narrow + _mem.wide = MODES.index(mem.mode) + + # scan add property + _mem.add = SKIP_VALUES.index(mem.skip) + + # reseting unknowns, this have to be set by hand + _mem.unknown0 = 0 + _mem.unknown1 = 0 + _mem.unknown2 = 0 + _mem.unknown3 = 0 + _mem.unknown4 = 0 + _mem.unknown5 = 0 + _mem.unknown6 = 0 + + def _zero_settings(): + _mem.spmute = 0 + _mem.optsig = 0 + _mem.scramble = 0 + _mem.bcl = 0 + _mem.pttid = 0 + _mem.scode = 0 + + if self.COLOR_LCD and _mem.scramble: + LOG.info('Resetting scramble bit for BTECH COLOR_LCD variant') + _mem.scramble = 0 + + # extra settings + if len(mem.extra) > 0: + # there are setting, parse + LOG.debug("Extra-Setting supplied. Setting them.") + # Zero them all first so any not provided by model don't + # stay set + _zero_settings() + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + else: + if mem.empty: + LOG.debug("New mem is empty.") + else: + LOG.debug("New mem is NOT empty") + # set extra-settings to default ONLY when apreviously empty or + # deleted memory was edited to prevent errors such as #4121 + if mem_was_empty: + LOG.debug("old mem was empty. Setting default for extras.") + _zero_settings() + + return mem + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + top = RadioSettings(basic, advanced, other, work) + + # Basic + if self.COLOR_LCD: + tmr = RadioSetting("settings.tmr", "Transceiver multi-receive", + RadioSettingValueList( + self.LIST_TMR, + self.LIST_TMR[_mem.settings.tmr])) + basic.append(tmr) + else: + tdr = RadioSetting("settings.tdr", "Transceiver dual receive", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(tdr) + + sql = RadioSetting("settings.sql", "Squelch level", + RadioSettingValueInteger(0, 9, _mem.settings.sql)) + basic.append(sql) + + if self.MODEL == "GMRS-50X1": + autolk = RadioSetting("settings.autolk", "Auto keylock", + RadioSettingValueBoolean( + _mem.settings.autolk)) + basic.append(autolk) + + tot = RadioSetting("settings.tot", "Time out timer", + RadioSettingValueList( + LIST_TOT, + LIST_TOT[_mem.settings.tot])) + basic.append(tot) + + if self.VENDOR == "BTECH" or self.COLOR_LCD: + apo = RadioSetting("settings.apo", "Auto power off timer", + RadioSettingValueList( + LIST_APO, + LIST_APO[_mem.settings.apo])) + basic.append(apo) + else: + toa = RadioSetting("settings.apo", "Time out alert timer", + RadioSettingValueList( + LIST_OFF1TO10, + LIST_OFF1TO10[_mem.settings.apo])) + basic.append(toa) + + abr = RadioSetting("settings.abr", "Backlight timer", + RadioSettingValueList( + LIST_OFF1TO50, + LIST_OFF1TO50[_mem.settings.abr])) + basic.append(abr) + + beep = RadioSetting("settings.beep", "Key beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(beep) + + dtmfst = RadioSetting("settings.dtmfst", "DTMF side tone", + RadioSettingValueList( + LIST_DTMFST, + LIST_DTMFST[_mem.settings.dtmfst])) + basic.append(dtmfst) + + if not self.COLOR_LCD: + prisc = RadioSetting("settings.prisc", "Priority scan", + RadioSettingValueBoolean( + _mem.settings.prisc)) + basic.append(prisc) + + prich = RadioSetting("settings.prich", "Priority channel", + RadioSettingValueInteger(0, self._upper, + _mem.settings.prich)) + basic.append(prich) + + screv = RadioSetting("settings.screv", "Scan resume method", + RadioSettingValueList( + LIST_SCREV, + LIST_SCREV[_mem.settings.screv])) + basic.append(screv) + + pttlt = RadioSetting("settings.pttlt", "PTT transmit delay", + RadioSettingValueInteger(0, 30, + _mem.settings.pttlt)) + basic.append(pttlt) + + if self.VENDOR == "BTECH" and self.COLOR_LCD: + emctp = RadioSetting("settings.emctp", "Alarm mode", + RadioSettingValueList( + LIST_EMCTPX, + LIST_EMCTPX[_mem.settings.emctp])) + basic.append(emctp) + else: + emctp = RadioSetting("settings.emctp", "Alarm mode", + RadioSettingValueList( + LIST_EMCTP, + LIST_EMCTP[_mem.settings.emctp])) + basic.append(emctp) + + emcch = RadioSetting("settings.emcch", "Alarm channel", + RadioSettingValueInteger(0, self._upper, + _mem.settings.emcch)) + basic.append(emcch) + + if self.COLOR_LCD: + if _mem.settings.sigbp > 0x01: + val = 0x00 + else: + val = _mem.settings.sigbp + sigbp = RadioSetting("settings.sigbp", "Signal beep", + RadioSettingValueBoolean(val)) + basic.append(sigbp) + else: + ringt = RadioSetting("settings.ringt", "Ring time", + RadioSettingValueList( + LIST_OFF1TO9, + LIST_OFF1TO9[_mem.settings.ringt])) + basic.append(ringt) + + camdf = RadioSetting("settings.camdf", "Display mode A", + RadioSettingValueList( + LIST_MDF, + LIST_MDF[_mem.settings.camdf])) + basic.append(camdf) + + cbmdf = RadioSetting("settings.cbmdf", "Display mode B", + RadioSettingValueList( + LIST_MDF, + LIST_MDF[_mem.settings.cbmdf])) + basic.append(cbmdf) + + if self.COLOR_LCD: + ccmdf = RadioSetting("settings.ccmdf", "Display mode C", + RadioSettingValueList( + LIST_MDF, + LIST_MDF[_mem.settings.ccmdf])) + basic.append(ccmdf) + + cdmdf = RadioSetting("settings.cdmdf", "Display mode D", + RadioSettingValueList( + LIST_MDF, + LIST_MDF[_mem.settings.cdmdf])) + basic.append(cdmdf) + + langua = RadioSetting("settings.langua", "Language", + RadioSettingValueList( + LIST_LANGUA, + LIST_LANGUA[_mem.settings.langua])) + basic.append(langua) + + if self.VENDOR == "BTECH": + if self.COLOR_LCD: + sync = RadioSetting("settings.sync", "Channel display sync", + RadioSettingValueList( + LIST_SYNC, + LIST_SYNC[_mem.settings.sync])) + basic.append(sync) + else: + sync = RadioSetting("settings.sync", "A/B channel sync", + RadioSettingValueBoolean( + _mem.settings.sync)) + basic.append(sync) + else: + autolk = RadioSetting("settings.sync", "Auto keylock", + RadioSettingValueBoolean( + _mem.settings.sync)) + basic.append(autolk) + + if not self.COLOR_LCD: + ponmsg = RadioSetting("settings.ponmsg", "Power-on message", + RadioSettingValueList( + LIST_PONMSG, + LIST_PONMSG[_mem.settings.ponmsg])) + basic.append(ponmsg) + + if self.COLOR_LCD and not self.COLOR_LCD2: + mainfc = RadioSetting("settings.mainfc", + "Main LCD foreground color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.mainfc])) + basic.append(mainfc) + + mainbc = RadioSetting("settings.mainbc", + "Main LCD background color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.mainbc])) + basic.append(mainbc) + + menufc = RadioSetting("settings.menufc", "Menu foreground color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.menufc])) + basic.append(menufc) + + menubc = RadioSetting("settings.menubc", "Menu background color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.menubc])) + basic.append(menubc) + + stafc = RadioSetting("settings.stafc", + "Top status foreground color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.stafc])) + basic.append(stafc) + + stabc = RadioSetting("settings.stabc", + "Top status background color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.stabc])) + basic.append(stabc) + + sigfc = RadioSetting("settings.sigfc", + "Bottom status foreground color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.sigfc])) + basic.append(sigfc) + + sigbc = RadioSetting("settings.sigbc", + "Bottom status background color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.sigbc])) + basic.append(sigbc) + + rxfc = RadioSetting("settings.rxfc", "Receiving character color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.rxfc])) + basic.append(rxfc) + + txfc = RadioSetting("settings.txfc", + "Transmitting character color", + RadioSettingValueList( + LIST_COLOR8, + LIST_COLOR8[_mem.settings.txfc])) + basic.append(txfc) + + txdisp = RadioSetting("settings.txdisp", + "Transmitting status display", + RadioSettingValueList( + LIST_TXDISP, + LIST_TXDISP[_mem.settings.txdisp])) + basic.append(txdisp) + elif self.COLOR_LCD2: + stfc = RadioSetting("settings.stfc", + "ST-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.stfc])) + basic.append(stfc) + + mffc = RadioSetting("settings.mffc", + "MF-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.mffc])) + basic.append(mffc) + + sfafc = RadioSetting("settings.sfafc", + "SFA-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.sfafc])) + basic.append(sfafc) + + sfbfc = RadioSetting("settings.sfbfc", + "SFB-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.sfbfc])) + basic.append(sfbfc) + + sfcfc = RadioSetting("settings.sfcfc", + "SFC-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.sfcfc])) + basic.append(sfcfc) + + sfdfc = RadioSetting("settings.sfdfc", + "SFD-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.sfdfc])) + basic.append(sfdfc) + + subfc = RadioSetting("settings.subfc", + "SUB-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.subfc])) + basic.append(subfc) + + fmfc = RadioSetting("settings.fmfc", + "FM-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.fmfc])) + basic.append(fmfc) + + sigfc = RadioSetting("settings.sigfc", + "SIG-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.sigfc])) + basic.append(sigfc) + + modfc = RadioSetting("settings.modfc", + "MOD-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.modfc])) + basic.append(modfc) + + menufc = RadioSetting("settings.menufc", + "MENUFC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.menufc])) + basic.append(menufc) + + txfc = RadioSetting("settings.txfc", + "TX-FC", + RadioSettingValueList( + LIST_COLOR7, + LIST_COLOR7[_mem.settings.txfc])) + basic.append(txfc) + + txdisp = RadioSetting("settings.txdisp", + "Transmitting status display", + RadioSettingValueList( + LIST_TXDISP, + LIST_TXDISP[_mem.settings.txdisp])) + basic.append(txdisp) + else: + wtled = RadioSetting("settings.wtled", "Standby backlight Color", + RadioSettingValueList( + LIST_COLOR4, + LIST_COLOR4[_mem.settings.wtled])) + basic.append(wtled) + + rxled = RadioSetting("settings.rxled", "RX backlight Color", + RadioSettingValueList( + LIST_COLOR4, + LIST_COLOR4[_mem.settings.rxled])) + basic.append(rxled) + + txled = RadioSetting("settings.txled", "TX backlight Color", + RadioSettingValueList( + LIST_COLOR4, + LIST_COLOR4[_mem.settings.txled])) + basic.append(txled) + + anil = RadioSetting("settings.anil", "ANI length", + RadioSettingValueList( + LIST_ANIL, + LIST_ANIL[_mem.settings.anil])) + basic.append(anil) + + reps = RadioSetting("settings.reps", "Relay signal (tone burst)", + RadioSettingValueList( + LIST_REPS, + LIST_REPS[_mem.settings.reps])) + basic.append(reps) + + if not self.MODEL == "GMRS-50X1": + repm = RadioSetting("settings.repm", "Relay condition", + RadioSettingValueList( + LIST_REPM, + LIST_REPM[_mem.settings.repm])) + basic.append(repm) + + if self.VENDOR == "BTECH" or self.COLOR_LCD: + if self.COLOR_LCD: + tmrmr = RadioSetting("settings.tmrmr", "TMR return time", + RadioSettingValueList( + LIST_OFF1TO50, + LIST_OFF1TO50[_mem.settings.tmrmr])) + basic.append(tmrmr) + else: + tdrab = RadioSetting("settings.tdrab", "TDR return time", + RadioSettingValueList( + LIST_OFF1TO50, + LIST_OFF1TO50[_mem.settings.tdrab])) + basic.append(tdrab) + + ste = RadioSetting("settings.ste", "Squelch tail eliminate", + RadioSettingValueBoolean(_mem.settings.ste)) + basic.append(ste) + + rpste = RadioSetting("settings.rpste", "Repeater STE", + RadioSettingValueList( + LIST_OFF1TO9, + LIST_OFF1TO9[_mem.settings.rpste])) + basic.append(rpste) + + rptdl = RadioSetting("settings.rptdl", "Repeater STE delay", + RadioSettingValueList( + LIST_RPTDL, + LIST_RPTDL[_mem.settings.rptdl])) + basic.append(rptdl) + + if str(_mem.fingerprint.fp) in BTECH3: + mgain = RadioSetting("settings.mgain", "Mic gain", + RadioSettingValueInteger(0, 120, + _mem.settings.mgain)) + basic.append(mgain) + + if str(_mem.fingerprint.fp) in BTECH3 or self.COLOR_LCD: + dtmfg = RadioSetting("settings.dtmfg", "DTMF gain", + RadioSettingValueInteger(0, 60, + _mem.settings.dtmfg)) + basic.append(dtmfg) + + if self.VENDOR == "BTECH" and self.COLOR_LCD: + mgain = RadioSetting("settings.mgain", "Mic gain", + RadioSettingValueInteger(0, 120, + _mem.settings.mgain)) + basic.append(mgain) + + skiptx = RadioSetting("settings.skiptx", "Skip TX", + RadioSettingValueList( + LIST_SKIPTX, + LIST_SKIPTX[_mem.settings.skiptx])) + basic.append(skiptx) + + scmode = RadioSetting("settings.scmode", "Scan mode", + RadioSettingValueList( + LIST_SCMODE, + LIST_SCMODE[_mem.settings.scmode])) + basic.append(scmode) + + # Advanced + def _filter(name): + filtered = "" + for char in str(name): + if char in VALID_CHARS: + filtered += char + else: + filtered += " " + return filtered + + if self.COLOR_LCD and not self.COLOR_LCD2: + _msg = self._memobj.poweron_msg + line1 = RadioSetting("poweron_msg.line1", + "Power-on message line 1", + RadioSettingValueString(0, 8, _filter( + _msg.line1))) + advanced.append(line1) + line2 = RadioSetting("poweron_msg.line2", + "Power-on message line 2", + RadioSettingValueString(0, 8, _filter( + _msg.line2))) + advanced.append(line2) + line3 = RadioSetting("poweron_msg.line3", + "Power-on message line 3", + RadioSettingValueString(0, 8, _filter( + _msg.line3))) + advanced.append(line3) + line4 = RadioSetting("poweron_msg.line4", + "Power-on message line 4", + RadioSettingValueString(0, 8, _filter( + _msg.line4))) + advanced.append(line4) + line5 = RadioSetting("poweron_msg.line5", + "Power-on message line 5", + RadioSettingValueString(0, 8, _filter( + _msg.line5))) + advanced.append(line5) + line6 = RadioSetting("poweron_msg.line6", + "Power-on message line 6", + RadioSettingValueString(0, 8, _filter( + _msg.line6))) + advanced.append(line6) + line7 = RadioSetting("poweron_msg.line7", + "Power-on message line 7", + RadioSettingValueString(0, 8, _filter( + _msg.line7))) + advanced.append(line7) + line8 = RadioSetting("poweron_msg.line8", "Static message", + RadioSettingValueString(0, 8, _filter( + _msg.line8))) + advanced.append(line8) + elif self.COLOR_LCD2: + _msg = self._memobj.static_msg + line = RadioSetting("static_msg.line", "Static message", + RadioSettingValueString(0, 16, _filter( + _msg.line))) + advanced.append(line) + else: + _msg = self._memobj.poweron_msg + line1 = RadioSetting("poweron_msg.line1", + "Power-on message line 1", + RadioSettingValueString(0, 6, _filter( + _msg.line1))) + advanced.append(line1) + line2 = RadioSetting("poweron_msg.line2", + "Power-on message line 2", + RadioSettingValueString(0, 6, _filter( + _msg.line2))) + advanced.append(line2) + + if self.MODEL in ("UV-2501", "UV-5001"): + vfomren = RadioSetting("settings2.vfomren", "VFO/MR switching", + RadioSettingValueBoolean( + _mem.settings2.vfomren)) + advanced.append(vfomren) + + reseten = RadioSetting("settings2.reseten", "RESET", + RadioSettingValueBoolean( + _mem.settings2.reseten)) + advanced.append(reseten) + + menuen = RadioSetting("settings2.menuen", "Menu", + RadioSettingValueBoolean( + _mem.settings2.menuen)) + advanced.append(menuen) + + # Other + def convert_bytes_to_limit(bytes): + limit = "" + for byte in bytes: + if byte < 10: + limit += chr(byte + 0x30) + else: + break + return limit + + if self.MODEL in ["UV-2501+220", "KT8900R"]: + _ranges = self._memobj.ranges220 + ranges = "ranges220" + else: + _ranges = self._memobj.ranges + ranges = "ranges" + + _limit = convert_bytes_to_limit(_ranges.vhf_low) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + vhf_low = RadioSetting("%s.vhf_low" % ranges, "VHF low", val) + other.append(vhf_low) + + _limit = convert_bytes_to_limit(_ranges.vhf_high) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + vhf_high = RadioSetting("%s.vhf_high" % ranges, "VHF high", val) + other.append(vhf_high) + + if self.BANDS == 3 or self.BANDS == 4: + _limit = convert_bytes_to_limit(_ranges.vhf2_low) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + vhf2_low = RadioSetting("%s.vhf2_low" % ranges, "VHF2 low", val) + other.append(vhf2_low) + + _limit = convert_bytes_to_limit(_ranges.vhf2_high) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + vhf2_high = RadioSetting("%s.vhf2_high" % ranges, "VHF2 high", val) + other.append(vhf2_high) + + _limit = convert_bytes_to_limit(_ranges.uhf_low) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + uhf_low = RadioSetting("%s.uhf_low" % ranges, "UHF low", val) + other.append(uhf_low) + + _limit = convert_bytes_to_limit(_ranges.uhf_high) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + uhf_high = RadioSetting("%s.uhf_high" % ranges, "UHF high", val) + other.append(uhf_high) + + if self.BANDS == 4: + _limit = convert_bytes_to_limit(_ranges.uhf2_low) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + uhf2_low = RadioSetting("%s.uhf2_low" % ranges, "UHF2 low", val) + other.append(uhf2_low) + + _limit = convert_bytes_to_limit(_ranges.uhf2_high) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + uhf2_high = RadioSetting("%s.uhf2_high" % ranges, "UHF2 high", val) + other.append(uhf2_high) + + val = RadioSettingValueString(0, 6, _filter(_mem.fingerprint.fp)) + val.set_mutable(False) + fp = RadioSetting("fingerprint.fp", "Fingerprint", val) + other.append(fp) + + # Work + if self.COLOR_LCD: + dispab = RadioSetting("settings2.dispab", "Display", + RadioSettingValueList( + LIST_ABCD, + LIST_ABCD[_mem.settings2.dispab])) + work.append(dispab) + else: + dispab = RadioSetting("settings2.dispab", "Display", + RadioSettingValueList( + LIST_AB, + LIST_AB[_mem.settings2.dispab])) + work.append(dispab) + + if self.COLOR_LCD: + vfomra = RadioSetting("settings2.vfomra", "VFO/MR A mode", + RadioSettingValueList( + LIST_VFOMR, + LIST_VFOMR[_mem.settings2.vfomra])) + work.append(vfomra) + + vfomrb = RadioSetting("settings2.vfomrb", "VFO/MR B mode", + RadioSettingValueList( + LIST_VFOMR, + LIST_VFOMR[_mem.settings2.vfomrb])) + work.append(vfomrb) + + vfomrc = RadioSetting("settings2.vfomrc", "VFO/MR C mode", + RadioSettingValueList( + LIST_VFOMR, + LIST_VFOMR[_mem.settings2.vfomrc])) + work.append(vfomrc) + + vfomrd = RadioSetting("settings2.vfomrd", "VFO/MR D mode", + RadioSettingValueList( + LIST_VFOMR, + LIST_VFOMR[_mem.settings2.vfomrd])) + work.append(vfomrd) + else: + vfomr = RadioSetting("settings2.vfomr", "VFO/MR mode", + RadioSettingValueList( + LIST_VFOMR, + LIST_VFOMR[_mem.settings2.vfomr])) + work.append(vfomr) + + keylock = RadioSetting("settings2.keylock", "Keypad lock", + RadioSettingValueBoolean( + _mem.settings2.keylock)) + work.append(keylock) + + mrcha = RadioSetting("settings2.mrcha", "MR A channel", + RadioSettingValueInteger(0, self._upper, + _mem.settings2.mrcha)) + work.append(mrcha) + + mrchb = RadioSetting("settings2.mrchb", "MR B channel", + RadioSettingValueInteger(0, self._upper, + _mem.settings2.mrchb)) + work.append(mrchb) + + if self.COLOR_LCD: + mrchc = RadioSetting("settings2.mrchc", "MR C channel", + RadioSettingValueInteger( + 0, self._upper, _mem.settings2.mrchc)) + work.append(mrchc) + + mrchd = RadioSetting("settings2.mrchd", "MR D channel", + RadioSettingValueInteger( + 0, self._upper, _mem.settings2.mrchd)) + work.append(mrchd) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + _vhf_lower = int(convert_bytes_to_limit(_ranges.vhf_low)) + _vhf_upper = int(convert_bytes_to_limit(_ranges.vhf_high)) + _uhf_lower = int(convert_bytes_to_limit(_ranges.uhf_low)) + _uhf_upper = int(convert_bytes_to_limit(_ranges.uhf_high)) + if self.BANDS == 3 or self.BANDS == 4: + _vhf2_lower = int(convert_bytes_to_limit(_ranges.vhf2_low)) + _vhf2_upper = int(convert_bytes_to_limit(_ranges.vhf2_high)) + if self.BANDS == 4: + _uhf2_lower = int(convert_bytes_to_limit(_ranges.uhf2_low)) + _uhf2_upper = int(convert_bytes_to_limit(_ranges.uhf2_high)) + + value = chirp_common.parse_freq(value) + msg = ("Can't be less then %i.0000") + if value > 99000000 and value < _vhf_lower * 1000000: + raise InvalidValueError(msg % (_vhf_lower)) + msg = ("Can't be betweeb %i.9975-%i.0000") + if self.BANDS == 2: + if (_vhf_upper + 1) * 1000000 <= value and \ + value < _uhf_lower * 1000000: + raise InvalidValueError(msg % (_vhf_upper, _uhf_lower)) + if self.BANDS == 3: + if (_vhf_upper + 1) * 1000000 <= value and \ + value < _vhf2_lower * 1000000: + raise InvalidValueError(msg % (_vhf_upper, _vhf2_lower)) + if (_vhf2_upper + 1) * 1000000 <= value and \ + value < _uhf_lower * 1000000: + raise InvalidValueError(msg % (_vhf2_upper, _uhf_lower)) + if self.BANDS == 4: + if (_vhf_upper + 1) * 1000000 <= value and \ + value < _vhf2_lower * 1000000: + raise InvalidValueError(msg % (_vhf_upper, _vhf2_lower)) + if (_vhf2_upper + 1) * 1000000 <= value and \ + value < _uhf2_lower * 1000000: + raise InvalidValueError(msg % (_vhf2_upper, _uhf2_lower)) + if (_uhf2_upper + 1) * 1000000 <= value and \ + value < _uhf_lower * 1000000: + raise InvalidValueError(msg % (_uhf2_upper, _uhf_lower)) + msg = ("Can't be greater then %i.9975") + if value > 99000000 and value >= _uhf_upper * 1000000: + raise InvalidValueError(msg % (_uhf_upper)) + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString(0, 10, convert_bytes_to_freq( + _mem.vfo.a.freq)) + val1a.set_validate_callback(my_validate) + vfoafreq = RadioSetting("vfo.a.freq", "VFO A frequency", val1a) + vfoafreq.set_apply_callback(apply_freq, _mem.vfo.a) + work.append(vfoafreq) + + val1b = RadioSettingValueString(0, 10, convert_bytes_to_freq( + _mem.vfo.b.freq)) + val1b.set_validate_callback(my_validate) + vfobfreq = RadioSetting("vfo.b.freq", "VFO B frequency", val1b) + vfobfreq.set_apply_callback(apply_freq, _mem.vfo.b) + work.append(vfobfreq) + + if self.COLOR_LCD: + val1c = RadioSettingValueString(0, 10, convert_bytes_to_freq( + _mem.vfo.c.freq)) + val1c.set_validate_callback(my_validate) + vfocfreq = RadioSetting("vfo.c.freq", "VFO C frequency", val1c) + vfocfreq.set_apply_callback(apply_freq, _mem.vfo.c) + work.append(vfocfreq) + + val1d = RadioSettingValueString(0, 10, convert_bytes_to_freq( + _mem.vfo.d.freq)) + val1d.set_validate_callback(my_validate) + vfodfreq = RadioSetting("vfo.d.freq", "VFO D frequency", val1d) + vfodfreq.set_apply_callback(apply_freq, _mem.vfo.d) + work.append(vfodfreq) + + if not self.MODEL == "GMRS-50X1": + vfoashiftd = RadioSetting("vfo.a.shiftd", "VFO A shift", + RadioSettingValueList( + LIST_SHIFT, + LIST_SHIFT[_mem.vfo.a.shiftd])) + work.append(vfoashiftd) + + vfobshiftd = RadioSetting("vfo.b.shiftd", "VFO B shift", + RadioSettingValueList( + LIST_SHIFT, + LIST_SHIFT[_mem.vfo.b.shiftd])) + work.append(vfobshiftd) + + if self.COLOR_LCD: + vfocshiftd = RadioSetting("vfo.c.shiftd", "VFO C shift", + RadioSettingValueList( + LIST_SHIFT, + LIST_SHIFT[_mem.vfo.c.shiftd])) + work.append(vfocshiftd) + + vfodshiftd = RadioSetting("vfo.d.shiftd", "VFO D shift", + RadioSettingValueList( + LIST_SHIFT, + LIST_SHIFT[_mem.vfo.d.shiftd])) + work.append(vfodshiftd) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + for byte in bytes: + real_offset = (real_offset * 10) + byte + return chirp_common.format_freq(real_offset * 1000) + + def apply_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 1000 + for i in range(5, -1, -1): + obj.offset[i] = value % 10 + value /= 10 + + if not self.MODEL == "GMRS-50X1": + if self.COLOR_LCD: + val1a = RadioSettingValueString(0, 10, convert_bytes_to_offset( + _mem.vfo.a.offset)) + vfoaoffset = RadioSetting("vfo.a.offset", + "VFO A offset (0.000-999.999)", + val1a) + vfoaoffset.set_apply_callback(apply_offset, _mem.vfo.a) + work.append(vfoaoffset) + + val1b = RadioSettingValueString(0, 10, convert_bytes_to_offset( + _mem.vfo.b.offset)) + vfoboffset = RadioSetting("vfo.b.offset", + "VFO B offset (0.000-999.999)", + val1b) + vfoboffset.set_apply_callback(apply_offset, _mem.vfo.b) + work.append(vfoboffset) + + val1c = RadioSettingValueString(0, 10, convert_bytes_to_offset( + _mem.vfo.c.offset)) + vfocoffset = RadioSetting("vfo.c.offset", + "VFO C offset (0.000-999.999)", + val1c) + vfocoffset.set_apply_callback(apply_offset, _mem.vfo.c) + work.append(vfocoffset) + + val1d = RadioSettingValueString(0, 10, convert_bytes_to_offset( + _mem.vfo.d.offset)) + vfodoffset = RadioSetting("vfo.d.offset", + "VFO D offset (0.000-999.999)", + val1d) + vfodoffset.set_apply_callback(apply_offset, _mem.vfo.d) + work.append(vfodoffset) + else: + val1a = RadioSettingValueString(0, 10, convert_bytes_to_offset( + _mem.vfo.a.offset)) + vfoaoffset = RadioSetting("vfo.a.offset", + "VFO A offset (0.000-99.999)", val1a) + vfoaoffset.set_apply_callback(apply_offset, _mem.vfo.a) + work.append(vfoaoffset) + + val1b = RadioSettingValueString(0, 10, convert_bytes_to_offset( + _mem.vfo.b.offset)) + vfoboffset = RadioSetting("vfo.b.offset", + "VFO B offset (0.000-99.999)", val1b) + vfoboffset.set_apply_callback(apply_offset, _mem.vfo.b) + work.append(vfoboffset) + + if not self.MODEL == "GMRS-50X1": + vfoatxp = RadioSetting("vfo.a.power", "VFO A power", + RadioSettingValueList( + LIST_TXP, + LIST_TXP[_mem.vfo.a.power])) + work.append(vfoatxp) + + vfobtxp = RadioSetting("vfo.b.power", "VFO B power", + RadioSettingValueList( + LIST_TXP, + LIST_TXP[_mem.vfo.b.power])) + work.append(vfobtxp) + + if self.COLOR_LCD: + vfoctxp = RadioSetting("vfo.c.power", "VFO C power", + RadioSettingValueList( + LIST_TXP, + LIST_TXP[_mem.vfo.c.power])) + work.append(vfoctxp) + + vfodtxp = RadioSetting("vfo.d.power", "VFO D power", + RadioSettingValueList( + LIST_TXP, + LIST_TXP[_mem.vfo.d.power])) + work.append(vfodtxp) + + if not self.MODEL == "GMRS-50X1": + vfoawide = RadioSetting("vfo.a.wide", "VFO A bandwidth", + RadioSettingValueList( + LIST_WIDE, + LIST_WIDE[_mem.vfo.a.wide])) + work.append(vfoawide) + + vfobwide = RadioSetting("vfo.b.wide", "VFO B bandwidth", + RadioSettingValueList( + LIST_WIDE, + LIST_WIDE[_mem.vfo.b.wide])) + work.append(vfobwide) + + if self.COLOR_LCD: + vfocwide = RadioSetting("vfo.c.wide", "VFO C bandwidth", + RadioSettingValueList( + LIST_WIDE, + LIST_WIDE[_mem.vfo.c.wide])) + work.append(vfocwide) + + vfodwide = RadioSetting("vfo.d.wide", "VFO D bandwidth", + RadioSettingValueList( + LIST_WIDE, + LIST_WIDE[_mem.vfo.d.wide])) + work.append(vfodwide) + + vfoastep = RadioSetting("vfo.a.step", "VFO A step", + RadioSettingValueList( + LIST_STEP, + LIST_STEP[_mem.vfo.a.step])) + work.append(vfoastep) + + vfobstep = RadioSetting("vfo.b.step", "VFO B step", + RadioSettingValueList( + LIST_STEP, + LIST_STEP[_mem.vfo.b.step])) + work.append(vfobstep) + + if self.COLOR_LCD: + vfocstep = RadioSetting("vfo.c.step", "VFO C step", + RadioSettingValueList( + LIST_STEP, + LIST_STEP[_mem.vfo.c.step])) + work.append(vfocstep) + + vfodstep = RadioSetting("vfo.d.step", "VFO D step", + RadioSettingValueList( + LIST_STEP, + LIST_STEP[_mem.vfo.d.step])) + work.append(vfodstep) + + vfoaoptsig = RadioSetting("vfo.a.optsig", "VFO A optional signal", + RadioSettingValueList( + OPTSIG_LIST, + OPTSIG_LIST[_mem.vfo.a.optsig])) + work.append(vfoaoptsig) + + vfoboptsig = RadioSetting("vfo.b.optsig", "VFO B optional signal", + RadioSettingValueList( + OPTSIG_LIST, + OPTSIG_LIST[_mem.vfo.b.optsig])) + work.append(vfoboptsig) + + if self.COLOR_LCD: + vfocoptsig = RadioSetting("vfo.c.optsig", "VFO C optional signal", + RadioSettingValueList( + OPTSIG_LIST, + OPTSIG_LIST[_mem.vfo.c.optsig])) + work.append(vfocoptsig) + + vfodoptsig = RadioSetting("vfo.d.optsig", "VFO D optional signal", + RadioSettingValueList( + OPTSIG_LIST, + OPTSIG_LIST[_mem.vfo.d.optsig])) + work.append(vfodoptsig) + + vfoaspmute = RadioSetting("vfo.a.spmute", "VFO A speaker mute", + RadioSettingValueList( + SPMUTE_LIST, + SPMUTE_LIST[_mem.vfo.a.spmute])) + work.append(vfoaspmute) + + vfobspmute = RadioSetting("vfo.b.spmute", "VFO B speaker mute", + RadioSettingValueList( + SPMUTE_LIST, + SPMUTE_LIST[_mem.vfo.b.spmute])) + work.append(vfobspmute) + + if self.COLOR_LCD: + vfocspmute = RadioSetting("vfo.c.spmute", "VFO C speaker mute", + RadioSettingValueList( + SPMUTE_LIST, + SPMUTE_LIST[_mem.vfo.c.spmute])) + work.append(vfocspmute) + + vfodspmute = RadioSetting("vfo.d.spmute", "VFO D speaker mute", + RadioSettingValueList( + SPMUTE_LIST, + SPMUTE_LIST[_mem.vfo.d.spmute])) + work.append(vfodspmute) + + if not self.COLOR_LCD or \ + (self.COLOR_LCD and not self.VENDOR == "BTECH"): + vfoascr = RadioSetting("vfo.a.scramble", "VFO A scramble", + RadioSettingValueBoolean( + _mem.vfo.a.scramble)) + work.append(vfoascr) + + vfobscr = RadioSetting("vfo.b.scramble", "VFO B scramble", + RadioSettingValueBoolean( + _mem.vfo.b.scramble)) + work.append(vfobscr) + + if self.COLOR_LCD and not self.VENDOR == "BTECH": + vfocscr = RadioSetting("vfo.c.scramble", "VFO C scramble", + RadioSettingValueBoolean( + _mem.vfo.c.scramble)) + work.append(vfocscr) + + vfodscr = RadioSetting("vfo.d.scramble", "VFO D scramble", + RadioSettingValueBoolean( + _mem.vfo.d.scramble)) + work.append(vfodscr) + + if not self.MODEL == "GMRS-50X1": + vfoascode = RadioSetting("vfo.a.scode", "VFO A PTT-ID", + RadioSettingValueList( + PTTIDCODE_LIST, + PTTIDCODE_LIST[_mem.vfo.a.scode])) + work.append(vfoascode) + + vfobscode = RadioSetting("vfo.b.scode", "VFO B PTT-ID", + RadioSettingValueList( + PTTIDCODE_LIST, + PTTIDCODE_LIST[_mem.vfo.b.scode])) + work.append(vfobscode) + + if self.COLOR_LCD: + vfocscode = RadioSetting("vfo.c.scode", "VFO C PTT-ID", + RadioSettingValueList( + PTTIDCODE_LIST, + PTTIDCODE_LIST[_mem.vfo.c.scode])) + work.append(vfocscode) + + vfodscode = RadioSetting("vfo.d.scode", "VFO D PTT-ID", + RadioSettingValueList( + PTTIDCODE_LIST, + PTTIDCODE_LIST[_mem.vfo.d.scode])) + work.append(vfodscode) + + if not self.MODEL == "GMRS-50X1": + pttid = RadioSetting("settings.pttid", "PTT ID", + RadioSettingValueList( + PTTID_LIST, + PTTID_LIST[_mem.settings.pttid])) + work.append(pttid) + + if not self.COLOR_LCD: + # FM presets + fm_presets = RadioSettingGroup("fm_presets", "FM Presets") + top.append(fm_presets) + + def fm_validate(value): + if value == 0: + return chirp_common.format_freq(value) + if not (87.5 <= value and value <= 108.0): # 87.5-108MHz + msg = ("FM-Preset-Frequency: " + + "Must be between 87.5 and 108 MHz") + raise InvalidValueError(msg) + return value + + def apply_fm_preset_name(setting, obj): + valstring = str(setting.value) + for i in range(0, 6): + if valstring[i] in VALID_CHARS: + obj[i] = valstring[i] + else: + obj[i] = '0xff' + + def apply_fm_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + _presets = self._memobj.fm_radio_preset + i = 1 + for preset in _presets: + line = RadioSetting("fm_presets_" + str(i), + "Station name " + str(i), + RadioSettingValueString(0, 6, _filter( + preset.broadcast_station_name))) + line.set_apply_callback(apply_fm_preset_name, + preset.broadcast_station_name) + + val = RadioSettingValueFloat(0, 108, + convert_bytes_to_freq( + preset.freq)) + fmfreq = RadioSetting("fm_presets_" + str(i) + "_freq", + "Frequency " + str(i), val) + val.set_validate_callback(fm_validate) + fmfreq.set_apply_callback(apply_fm_freq, preset) + fm_presets.append(line) + fm_presets.append(fmfreq) + + i = i + 1 + + # DTMF-Setting + dtmf_enc_settings = RadioSettingGroup("dtmf_enc_settings", + "DTMF Encoding Settings") + dtmf_dec_settings = RadioSettingGroup("dtmf_dec_settings", + "DTMF Decoding Settings") + top.append(dtmf_enc_settings) + top.append(dtmf_dec_settings) + txdisable = RadioSetting("dtmf_settings.txdisable", + "TX-Disable", + RadioSettingValueBoolean( + _mem.dtmf_settings.txdisable)) + dtmf_enc_settings.append(txdisable) + + rxdisable = RadioSetting("dtmf_settings.rxdisable", + "RX-Disable", + RadioSettingValueBoolean( + _mem.dtmf_settings.rxdisable)) + dtmf_enc_settings.append(rxdisable) + + dtmfspeed_on = RadioSetting( + "dtmf_settings.dtmfspeed_on", + "DTMF Speed (On Time)", + RadioSettingValueList(LIST_DTMF_SPEED, + LIST_DTMF_SPEED[ + _mem.dtmf_settings.dtmfspeed_on])) + dtmf_enc_settings.append(dtmfspeed_on) + + dtmfspeed_off = RadioSetting( + "dtmf_settings.dtmfspeed_off", + "DTMF Speed (Off Time)", + RadioSettingValueList(LIST_DTMF_SPEED, + LIST_DTMF_SPEED[ + _mem.dtmf_settings.dtmfspeed_off])) + dtmf_enc_settings.append(dtmfspeed_off) + + def memory2string(dmtf_mem): + dtmf_string = "" + for digit in dmtf_mem: + if digit != 255: + index = LIST_DTMF_VALUES.index(digit) + dtmf_string = dtmf_string + LIST_DTMF_DIGITS[index] + return dtmf_string + + def apply_dmtf_frame(setting, obj): + LOG.debug("Setting DTMF-Code: " + str(setting.value)) + val_string = str(setting.value) + for i in range(0, 16): + obj[i] = 255 + i = 0 + for current_char in val_string: + current_char = current_char.upper() + index = LIST_DTMF_DIGITS.index(current_char) + obj[i] = LIST_DTMF_VALUES[index] + i = i + 1 + + codes = self._memobj.dtmf_codes + i = 1 + for dtmfcode in codes: + val = RadioSettingValueString(0, 16, memory2string( + dtmfcode.code), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_code_" + str(i) + "_code", + "DMTF Code " + str(i), val) + line.set_apply_callback(apply_dmtf_frame, dtmfcode.code) + dtmf_enc_settings.append(line) + i = i + 1 + + line = RadioSetting("dtmf_settings.mastervice", + "Master and Vice ID", + RadioSettingValueBoolean( + _mem.dtmf_settings.mastervice)) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.masterid), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.masterid", + "Master Control ID ", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.masterid) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.minspection", + "Master Inspection", + RadioSettingValueBoolean( + _mem.dtmf_settings.minspection)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.mmonitor", + "Master Monitor", + RadioSettingValueBoolean( + _mem.dtmf_settings.mmonitor)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.mstun", + "Master Stun", + RadioSettingValueBoolean( + _mem.dtmf_settings.mstun)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.mkill", + "Master Kill", + RadioSettingValueBoolean( + _mem.dtmf_settings.mkill)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.mrevive", + "Master Revive", + RadioSettingValueBoolean( + _mem.dtmf_settings.mrevive)) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.viceid), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.viceid", + "Vice Control ID ", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.viceid) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.vinspection", + "Vice Inspection", + RadioSettingValueBoolean( + _mem.dtmf_settings.vinspection)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.vmonitor", + "Vice Monitor", + RadioSettingValueBoolean( + _mem.dtmf_settings.vmonitor)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.vstun", + "Vice Stun", + RadioSettingValueBoolean( + _mem.dtmf_settings.vstun)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.vkill", + "Vice Kill", + RadioSettingValueBoolean( + _mem.dtmf_settings.vkill)) + dtmf_dec_settings.append(line) + + line = RadioSetting("dtmf_settings.vrevive", + "Vice Revive", + RadioSettingValueBoolean( + _mem.dtmf_settings.vrevive)) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.inspection), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.inspection", + "Inspection", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.inspection) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.alarmcode), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.alarmcode", + "Alarm", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.alarmcode) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.kill), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.kill", + "Kill", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.kill) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.monitor), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.monitor", + "Monitor", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.monitor) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.stun), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.stun", + "Stun", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.stun) + dtmf_dec_settings.append(line) + + val = RadioSettingValueString(0, 16, memory2string( + _mem.dtmf_settings.revive), + False, CHARSET_DTMF_DIGITS) + line = RadioSetting("dtmf_settings.revive", + "Revive", val) + line.set_apply_callback(apply_dmtf_frame, + _mem.dtmf_settings.revive) + dtmf_dec_settings.append(line) + + def apply_dmtf_listvalue(setting, obj): + LOG.debug("Setting value: " + str(setting.value) + " from list") + val = str(setting.value) + index = LIST_DTMF_SPECIAL_DIGITS.index(val) + val = LIST_DTMF_SPECIAL_VALUES[index] + obj.set_value(val) + + idx = LIST_DTMF_SPECIAL_VALUES.index(_mem.dtmf_settings.groupcode) + line = RadioSetting( + "dtmf_settings.groupcode", + "Group Code", + RadioSettingValueList(LIST_DTMF_SPECIAL_DIGITS, + LIST_DTMF_SPECIAL_DIGITS[idx])) + line.set_apply_callback(apply_dmtf_listvalue, + _mem.dtmf_settings.groupcode) + dtmf_dec_settings.append(line) + + idx = LIST_DTMF_SPECIAL_VALUES.index(_mem.dtmf_settings.spacecode) + line = RadioSetting( + "dtmf_settings.spacecode", + "Space Code", + RadioSettingValueList(LIST_DTMF_SPECIAL_DIGITS, + LIST_DTMF_SPECIAL_DIGITS[idx])) + line.set_apply_callback(apply_dmtf_listvalue, + _mem.dtmf_settings.spacecode) + dtmf_dec_settings.append(line) + + if self.COLOR_LCD: + line = RadioSetting( + "dtmf_settings.resettime", + "Reset time", + RadioSettingValueList(LIST_5TONE_RESET_COLOR, + LIST_5TONE_RESET_COLOR[ + _mem.dtmf_settings.resettime])) + dtmf_dec_settings.append(line) + else: + line = RadioSetting( + "dtmf_settings.resettime", + "Reset time", + RadioSettingValueList(LIST_5TONE_RESET, + LIST_5TONE_RESET[ + _mem.dtmf_settings.resettime])) + dtmf_dec_settings.append(line) + + line = RadioSetting( + "dtmf_settings.delayproctime", + "Delay processing time", + RadioSettingValueList(LIST_DTMF_DELAY, + LIST_DTMF_DELAY[ + _mem.dtmf_settings.delayproctime])) + dtmf_dec_settings.append(line) + + # 5 Tone Settings + stds_5tone = RadioSettingGroup("stds_5tone", "Standards") + codes_5tone = RadioSettingGroup("codes_5tone", "Codes") + + group_5tone = RadioSettingGroup("group_5tone", "5 Tone Settings") + group_5tone.append(stds_5tone) + group_5tone.append(codes_5tone) + + top.append(group_5tone) + + def apply_list_value(setting, obj): + options = setting.value.get_options() + obj.set_value(options.index(str(setting.value))) + + _5tone_standards = self._memobj._5tone_std_settings + i = 0 + for standard in _5tone_standards: + std_5tone = RadioSettingGroup("std_5tone_" + str(i), + LIST_5TONE_STANDARDS[i]) + stds_5tone.append(std_5tone) + + period = standard.period + if period == 255: + LOG.debug("Period for " + LIST_5TONE_STANDARDS[i] + + " is not yet configured. Setting to 70ms.") + period = 5 + + if period <= len(LIST_5TONE_STANDARD_PERIODS): + line = RadioSetting( + "_5tone_std_settings_" + str(i) + "_period", + "Period (ms)", RadioSettingValueList + (LIST_5TONE_STANDARD_PERIODS, + LIST_5TONE_STANDARD_PERIODS[period])) + line.set_apply_callback(apply_list_value, standard.period) + std_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone period! Disabling.") + + group_tone = standard.group_tone + if group_tone == 255: + LOG.debug("Group-Tone for " + LIST_5TONE_STANDARDS[i] + + " is not yet configured. Setting to A.") + group_tone = 10 + + if group_tone <= len(LIST_5TONE_DIGITS): + line = RadioSetting( + "_5tone_std_settings_" + str(i) + "_grouptone", + "Group Tone", + RadioSettingValueList(LIST_5TONE_DIGITS, + LIST_5TONE_DIGITS[ + group_tone])) + line.set_apply_callback(apply_list_value, + standard.group_tone) + std_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone digit! Disabling.") + + repeat_tone = standard.repeat_tone + if repeat_tone == 255: + LOG.debug("Repeat-Tone for " + LIST_5TONE_STANDARDS[i] + + " is not yet configured. Setting to E.") + repeat_tone = 14 + + if repeat_tone <= len(LIST_5TONE_DIGITS): + line = RadioSetting( + "_5tone_std_settings_" + str(i) + "_repttone", + "Repeat Tone", + RadioSettingValueList(LIST_5TONE_DIGITS, + LIST_5TONE_DIGITS[ + repeat_tone])) + line.set_apply_callback(apply_list_value, + standard.repeat_tone) + std_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone digit! Disabling.") + i = i + 1 + + def my_apply_5tonestdlist_value(setting, obj): + if LIST_5TONE_STANDARDS.index(str(setting.value)) == 15: + obj.set_value(0xFF) + else: + obj.set_value(LIST_5TONE_STANDARDS. + index(str(setting.value))) + + def apply_5tone_frame(setting, obj): + LOG.debug("Setting 5 Tone: " + str(setting.value)) + valstring = str(setting.value) + if len(valstring) == 0: + for i in range(0, 5): + obj[i] = 255 + else: + validFrame = True + for i in range(0, 5): + currentChar = valstring[i].upper() + if currentChar in LIST_5TONE_DIGITS: + obj[i] = LIST_5TONE_DIGITS.index(currentChar) + else: + validFrame = False + LOG.debug("invalid char: " + str(currentChar)) + if not validFrame: + LOG.debug("setting whole frame to FF") + for i in range(0, 5): + obj[i] = 255 + + def validate_5tone_frame(value): + if (len(str(value)) != 5) and (len(str(value)) != 0): + msg = ("5 Tone must have 5 digits or 0 digits") + raise InvalidValueError(msg) + for digit in str(value): + if digit.upper() not in LIST_5TONE_DIGITS: + msg = (str(digit) + " is not a valid digit for 5tones") + raise InvalidValueError(msg) + return value + + def frame2string(frame): + frameString = "" + for digit in frame: + if digit != 255: + frameString = frameString + LIST_5TONE_DIGITS[digit] + return frameString + + _5tone_codes = self._memobj._5tone_codes + i = 1 + for code in _5tone_codes: + code_5tone = RadioSettingGroup("code_5tone_" + str(i), + "5 Tone code " + str(i)) + codes_5tone.append(code_5tone) + if (code.standard == 255): + currentVal = 15 + else: + currentVal = code.standard + line = RadioSetting("_5tone_code_" + str(i) + "_std", + " Standard", + RadioSettingValueList(LIST_5TONE_STANDARDS, + LIST_5TONE_STANDARDS[ + currentVal])) + line.set_apply_callback(my_apply_5tonestdlist_value, + code.standard) + code_5tone.append(line) + + val = RadioSettingValueString(0, 6, + frame2string(code.frame1), False) + line = RadioSetting("_5tone_code_" + str(i) + "_frame1", + " Frame 1", val) + val.set_validate_callback(validate_5tone_frame) + line.set_apply_callback(apply_5tone_frame, code.frame1) + code_5tone.append(line) + + val = RadioSettingValueString(0, 6, + frame2string(code.frame2), False) + line = RadioSetting("_5tone_code_" + str(i) + "_frame2", + " Frame 2", val) + val.set_validate_callback(validate_5tone_frame) + line.set_apply_callback(apply_5tone_frame, code.frame2) + code_5tone.append(line) + + val = RadioSettingValueString(0, 6, + frame2string(code.frame3), False) + line = RadioSetting("_5tone_code_" + str(i) + "_frame3", + " Frame 3", val) + val.set_validate_callback(validate_5tone_frame) + line.set_apply_callback(apply_5tone_frame, code.frame3) + code_5tone.append(line) + i = i + 1 + + _5_tone_decode1 = RadioSetting( + "_5tone_settings._5tone_decode_call_frame1", + "5 Tone decode call Frame 1", + RadioSettingValueBoolean( + _mem._5tone_settings._5tone_decode_call_frame1)) + group_5tone.append(_5_tone_decode1) + + _5_tone_decode2 = RadioSetting( + "_5tone_settings._5tone_decode_call_frame2", + "5 Tone decode call Frame 2", + RadioSettingValueBoolean( + _mem._5tone_settings._5tone_decode_call_frame2)) + group_5tone.append(_5_tone_decode2) + + _5_tone_decode3 = RadioSetting( + "_5tone_settings._5tone_decode_call_frame3", + "5 Tone decode call Frame 3", + RadioSettingValueBoolean( + _mem._5tone_settings._5tone_decode_call_frame3)) + group_5tone.append(_5_tone_decode3) + + _5_tone_decode_disp1 = RadioSetting( + "_5tone_settings._5tone_decode_disp_frame1", + "5 Tone decode disp Frame 1", + RadioSettingValueBoolean( + _mem._5tone_settings._5tone_decode_disp_frame1)) + group_5tone.append(_5_tone_decode_disp1) + + _5_tone_decode_disp2 = RadioSetting( + "_5tone_settings._5tone_decode_disp_frame2", + "5 Tone decode disp Frame 2", + RadioSettingValueBoolean( + _mem._5tone_settings._5tone_decode_disp_frame2)) + group_5tone.append(_5_tone_decode_disp2) + + _5_tone_decode_disp3 = RadioSetting( + "_5tone_settings._5tone_decode_disp_frame3", + "5 Tone decode disp Frame 3", + RadioSettingValueBoolean( + _mem._5tone_settings._5tone_decode_disp_frame3)) + group_5tone.append(_5_tone_decode_disp3) + + decode_standard = _mem._5tone_settings.decode_standard + if decode_standard == 255: + decode_standard = 0 + if decode_standard <= len(LIST_5TONE_STANDARDS_without_none): + line = RadioSetting("_5tone_settings.decode_standard", + "5 Tone-decode Standard", + RadioSettingValueList( + LIST_5TONE_STANDARDS_without_none, + LIST_5TONE_STANDARDS_without_none[ + decode_standard])) + group_5tone.append(line) + else: + LOG.debug("Invalid decode std...") + + _5tone_delay1 = _mem._5tone_settings._5tone_delay1 + if _5tone_delay1 == 255: + _5tone_delay1 = 20 + + if _5tone_delay1 <= len(LIST_5TONE_DELAY): + list = RadioSettingValueList(LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + _5tone_delay1]) + line = RadioSetting("_5tone_settings._5tone_delay1", + "5 Tone Delay Frame 1", list) + group_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone delay (frame1) ! Disabling.") + + _5tone_delay2 = _mem._5tone_settings._5tone_delay2 + if _5tone_delay2 == 255: + _5tone_delay2 = 20 + LOG.debug("5 Tone delay unconfigured! Resetting to 200ms.") + + if _5tone_delay2 <= len(LIST_5TONE_DELAY): + list = RadioSettingValueList(LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + _5tone_delay2]) + line = RadioSetting("_5tone_settings._5tone_delay2", + "5 Tone Delay Frame 2", list) + group_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone delay (frame2)! Disabling.") + + _5tone_delay3 = _mem._5tone_settings._5tone_delay3 + if _5tone_delay3 == 255: + _5tone_delay3 = 20 + LOG.debug("5 Tone delay unconfigured! Resetting to 200ms.") + + if _5tone_delay3 <= len(LIST_5TONE_DELAY): + list = RadioSettingValueList(LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + _5tone_delay3]) + line = RadioSetting("_5tone_settings._5tone_delay3", + "5 Tone Delay Frame 3", list) + group_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone delay (frame3)! Disabling.") + + ext_length = _mem._5tone_settings._5tone_first_digit_ext_length + if ext_length == 255: + ext_length = 0 + LOG.debug("1st Tone ext lenght unconfigured! Resetting to 0") + + if ext_length <= len(LIST_5TONE_DELAY): + list = RadioSettingValueList( + LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + ext_length]) + line = RadioSetting( + "_5tone_settings._5tone_first_digit_ext_length", + "First digit extend length", list) + group_5tone.append(line) + else: + LOG.debug("Invalid value for 5tone ext length! Disabling.") + + decode_reset_time = _mem._5tone_settings.decode_reset_time + if decode_reset_time == 255: + decode_reset_time = 59 + LOG.debug("Decode reset time unconfigured. resetting.") + if decode_reset_time <= len(LIST_5TONE_RESET): + list = RadioSettingValueList( + LIST_5TONE_RESET, + LIST_5TONE_RESET[ + decode_reset_time]) + line = RadioSetting("_5tone_settings.decode_reset_time", + "Decode reset time", list) + group_5tone.append(line) + else: + LOG.debug("Invalid value decode reset time! Disabling.") + + # 2 Tone + encode_2tone = RadioSettingGroup("encode_2tone", "2 Tone Encode") + decode_2tone = RadioSettingGroup("decode_2tone", "2 Code Decode") + + top.append(encode_2tone) + top.append(decode_2tone) + + duration_1st_tone = self._memobj._2tone.duration_1st_tone + if duration_1st_tone == 255: + LOG.debug("Duration of first 2 Tone digit is not yet " + + "configured. Setting to 600ms") + duration_1st_tone = 60 + + if duration_1st_tone <= len(LIST_5TONE_DELAY): + line = RadioSetting("_2tone.duration_1st_tone", + "Duration 1st Tone", + RadioSettingValueList(LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + duration_1st_tone])) + encode_2tone.append(line) + + duration_2nd_tone = self._memobj._2tone.duration_2nd_tone + if duration_2nd_tone == 255: + LOG.debug("Duration of second 2 Tone digit is not yet " + + "configured. Setting to 600ms") + duration_2nd_tone = 60 + + if duration_2nd_tone <= len(LIST_5TONE_DELAY): + line = RadioSetting("_2tone.duration_2nd_tone", + "Duration 2nd Tone", + RadioSettingValueList(LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + duration_2nd_tone])) + encode_2tone.append(line) + + duration_gap = self._memobj._2tone.duration_gap + if duration_gap == 255: + LOG.debug("Duration of gap is not yet " + + "configured. Setting to 300ms") + duration_gap = 30 + + if duration_gap <= len(LIST_5TONE_DELAY): + line = RadioSetting("_2tone.duration_gap", "Duration of gap", + RadioSettingValueList(LIST_5TONE_DELAY, + LIST_5TONE_DELAY[ + duration_gap])) + encode_2tone.append(line) + + def _2tone_validate(value): + if value == 0: + return 65535 + if value == 65535: + return value + if not (300 <= value and value <= 3000): + msg = ("2 Tone Frequency: Must be between 300 and 3000 Hz") + raise InvalidValueError(msg) + return value + + def apply_2tone_freq(setting, obj): + val = int(setting.value) + if (val == 0) or (val == 65535): + obj.set_value(65535) + else: + obj.set_value(val) + + i = 1 + for code in self._memobj._2tone._2tone_encode: + code_2tone = RadioSettingGroup("code_2tone_" + str(i), + "Encode Code " + str(i)) + encode_2tone.append(code_2tone) + + tmp = code.freq1 + if tmp == 65535: + tmp = 0 + val1 = RadioSettingValueInteger(0, 65535, tmp) + freq1 = RadioSetting("2tone_code_" + str(i) + "_freq1", + "Frequency 1", val1) + val1.set_validate_callback(_2tone_validate) + freq1.set_apply_callback(apply_2tone_freq, code.freq1) + code_2tone.append(freq1) + + tmp = code.freq2 + if tmp == 65535: + tmp = 0 + val2 = RadioSettingValueInteger(0, 65535, tmp) + freq2 = RadioSetting("2tone_code_" + str(i) + "_freq2", + "Frequency 2", val2) + val2.set_validate_callback(_2tone_validate) + freq2.set_apply_callback(apply_2tone_freq, code.freq2) + code_2tone.append(freq2) + + i = i + 1 + + decode_reset_time = _mem._2tone.reset_time + if decode_reset_time == 255: + decode_reset_time = 59 + LOG.debug("Decode reset time unconfigured. resetting.") + if decode_reset_time <= len(LIST_5TONE_RESET): + list = RadioSettingValueList( + LIST_5TONE_RESET, + LIST_5TONE_RESET[ + decode_reset_time]) + line = RadioSetting("_2tone.reset_time", + "Decode reset time", list) + decode_2tone.append(line) + else: + LOG.debug("Invalid value decode reset time! Disabling.") + + def apply_2tone_freq_pair(setting, obj): + val = int(setting.value) + derived_val = 65535 + frqname = str(setting._name[-5:]) + derivedname = "derived_from_" + frqname + + if (val == 0): + val = 65535 + derived_val = 65535 + else: + derived_val = int(round(2304000.0/val)) + + obj[frqname].set_value(val) + obj[derivedname].set_value(derived_val) + + LOG.debug("Apply " + frqname + ": " + str(val) + " | " + + derivedname + ": " + str(derived_val)) + + i = 1 + for decode_code in self._memobj._2tone._2tone_decode: + _2tone_dec_code = RadioSettingGroup("code_2tone_" + str(i), + "Decode Code " + str(i)) + decode_2tone.append(_2tone_dec_code) + + j = 1 + for dec in decode_code.decs: + val = dec.dec + if val == 255: + LOG.debug("Dec for Code " + str(i) + " Dec " + str(j) + + " is not yet configured. Setting to 0.") + val = 0 + + if val <= len(LIST_2TONE_DEC): + line = RadioSetting( + "_2tone_dec_settings_" + str(i) + "_dec_" + str(j), + "Dec " + str(j), RadioSettingValueList + (LIST_2TONE_DEC, + LIST_2TONE_DEC[val])) + line.set_apply_callback(apply_list_value, dec.dec) + _2tone_dec_code.append(line) + else: + LOG.debug("Invalid value for 2tone dec! Disabling.") + + val = dec.response + if val == 255: + LOG.debug("Response for Code " + str(i) + " Dec " + + str(j) + " is not yet configured. Setting to 0.") + val = 0 + + if val <= len(LIST_2TONE_RESPONSE): + line = RadioSetting( + "_2tone_dec_settings_" + str(i) + "_resp_" + str(j), + "Response " + str(j), RadioSettingValueList + (LIST_2TONE_RESPONSE, + LIST_2TONE_RESPONSE[val])) + line.set_apply_callback(apply_list_value, dec.response) + _2tone_dec_code.append(line) + else: + LOG.debug("Invalid value for 2tone response! Disabling.") + + val = dec.alert + if val == 255: + LOG.debug("Alert for Code " + str(i) + " Dec " + str(j) + + " is not yet configured. Setting to 0.") + val = 0 + + if val <= len(PTTIDCODE_LIST): + line = RadioSetting( + "_2tone_dec_settings_" + str(i) + "_alert_" + str(j), + "Alert " + str(j), RadioSettingValueList + (PTTIDCODE_LIST, + PTTIDCODE_LIST[val])) + line.set_apply_callback(apply_list_value, dec.alert) + _2tone_dec_code.append(line) + else: + LOG.debug("Invalid value for 2tone alert! Disabling.") + j = j + 1 + + freq = self._memobj._2tone.freqs[i-1] + for char in ['A', 'B', 'C', 'D']: + setting_name = "freq" + str(char) + + tmp = freq[setting_name] + if tmp == 65535: + tmp = 0 + if tmp != 0: + expected = int(round(2304000.0/tmp)) + from_mem = freq["derived_from_" + setting_name] + if expected != from_mem: + LOG.error("Expected " + str(expected) + + " but read " + str(from_mem) + + ". Disabling 2Tone Decode Freqs!") + break + val = RadioSettingValueInteger(0, 65535, tmp) + frq = RadioSetting("2tone_dec_" + str(i) + "_freq" + str(char), + ("Decode Frequency " + str(char)), val) + val.set_validate_callback(_2tone_validate) + frq.set_apply_callback(apply_2tone_freq_pair, freq) + _2tone_dec_code.append(frq) + + i = i + 1 + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception as e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1:2, + spmute:1, + unknown2:3, + optsig:2; + u8 unknown3:3, + scramble:1, + unknown4:3, + power:1; + u8 unknown5:1, + wide:1, + unknown6:2, + bcl:1, + add:1, + pttid:2; +} memory[200]; + +#seekto 0x0E00; +struct { + u8 tdr; + u8 unknown1; + u8 sql; + u8 unknown2[2]; + u8 tot; + u8 apo; // BTech radios use this as the Auto Power Off time + // other radios use this as pre-Time Out Alert + u8 unknown3; + u8 abr; + u8 beep; + u8 unknown4[4]; + u8 dtmfst; + u8 unknown5[2]; + u8 prisc; + u8 prich; + u8 screv; + u8 unknown6[2]; + u8 pttid; + u8 pttlt; + u8 unknown7; + u8 emctp; + u8 emcch; + u8 ringt; + u8 unknown8; + u8 camdf; + u8 cbmdf; + u8 sync; // BTech radios use this as the display sync setting + // other radios use this as the auto keypad lock setting + u8 ponmsg; + u8 wtled; + u8 rxled; + u8 txled; + u8 unknown9[5]; + u8 anil; + u8 reps; + u8 repm; + u8 tdrab; + u8 ste; + u8 rpste; + u8 rptdl; + u8 mgain; + u8 dtmfg; +} settings; + +#seekto 0x0E80; +struct { + u8 unknown1; + u8 vfomr; + u8 keylock; + u8 unknown2; + u8 unknown3:4, + vfomren:1, + unknown4:1, + reseten:1, + menuen:1; + u8 unknown5[11]; + u8 dispab; + u8 mrcha; + u8 mrchb; + u8 menu; +} settings2; + +#seekto 0x0EC0; +struct { + char line1[6]; + char line2[6]; +} poweron_msg; + +struct settings_vfo { + u8 freq[8]; + u8 offset[6]; + u8 unknown2[2]; + ul16 rxtone; + ul16 txtone; + u8 scode; + u8 spmute; + u8 optsig; + u8 scramble; + u8 wide; + u8 power; + u8 shiftd; + u8 step; + u8 unknown3[4]; +}; + +#seekto 0x0F00; +struct { + struct settings_vfo a; + struct settings_vfo b; +} vfo; + +#seekto 0x1000; +struct { + char name[6]; + u8 unknown1[10]; +} names[200]; + +#seekto 0x2400; +struct { + u8 period; // one out of LIST_5TONE_STANDARD_PERIODS + u8 group_tone; + u8 repeat_tone; + u8 unused[13]; +} _5tone_std_settings[15]; + +#seekto 0x2500; +struct { + u8 frame1[5]; + u8 frame2[5]; + u8 frame3[5]; + u8 standard; // one out of LIST_5TONE_STANDARDS +} _5tone_codes[15]; + +#seekto 0x25F0; +struct { + u8 _5tone_delay1; // * 10ms + u8 _5tone_delay2; // * 10ms + u8 _5tone_delay3; // * 10ms + u8 _5tone_first_digit_ext_length; + u8 unknown1; + u8 unknown2; + u8 unknown3; + u8 unknown4; + u8 decode_standard; + u8 unknown5:5, + _5tone_decode_call_frame3:1, + _5tone_decode_call_frame2:1, + _5tone_decode_call_frame1:1; + u8 unknown6:5, + _5tone_decode_disp_frame3:1, + _5tone_decode_disp_frame2:1, + _5tone_decode_disp_frame1:1; + u8 decode_reset_time; // * 100 + 100ms +} _5tone_settings; + +#seekto 0x2900; +struct { + u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B +} dtmf_codes[15]; + +#seekto 0x29F0; +struct { + u8 dtmfspeed_on; //list with 50..2000ms in steps of 10 + u8 dtmfspeed_off; //list with 50..2000ms in steps of 10 + u8 unknown0[14]; + u8 inspection[16]; + u8 monitor[16]; + u8 alarmcode[16]; + u8 stun[16]; + u8 kill[16]; + u8 revive[16]; + u8 unknown1[16]; + u8 unknown2[16]; + u8 unknown3[16]; + u8 unknown4[16]; + u8 unknown5[16]; + u8 unknown6[16]; + u8 unknown7[16]; + u8 masterid[16]; + u8 viceid[16]; + u8 unused01:7, + mastervice:1; + u8 unused02:3, + mrevive:1, + mkill:1, + mstun:1, + mmonitor:1, + minspection:1; + u8 unused03:3, + vrevive:1, + vkill:1, + vstun:1, + vmonitor:1, + vinspection:1; + u8 unused04:6, + txdisable:1, + rxdisable:1; + u8 groupcode; + u8 spacecode; + u8 delayproctime; // * 100 + 100ms + u8 resettime; // * 100 + 100ms +} dtmf_settings; + +#seekto 0x2D00; +struct { + struct { + ul16 freq1; + u8 unused01[6]; + ul16 freq2; + u8 unused02[6]; + } _2tone_encode[15]; + u8 duration_1st_tone; // *10ms + u8 duration_2nd_tone; // *10ms + u8 duration_gap; // *10ms + u8 unused03[13]; + struct { + struct { + u8 dec; // one out of LIST_2TONE_DEC + u8 response; // one out of LIST_2TONE_RESPONSE + u8 alert; // 1-16 + } decs[4]; + u8 unused04[4]; + } _2tone_decode[15]; + u8 unused05[16]; + + struct { + ul16 freqA; + ul16 freqB; + ul16 freqC; + ul16 freqD; + // unknown what those values mean, but they are + // derived from configured frequencies + ul16 derived_from_freqA; // 2304000/freqA + ul16 derived_from_freqB; // 2304000/freqB + ul16 derived_from_freqC; // 2304000/freqC + ul16 derived_from_freqD; // 2304000/freqD + }freqs[15]; + u8 reset_time; // * 100 + 100ms - 100-8000ms +} _2tone; + +#seekto 0x3000; +struct { + u8 freq[8]; + char broadcast_station_name[6]; + u8 unknown[2]; +} fm_radio_preset[16]; + +#seekto 0x3C90; +struct { + u8 vhf_low[3]; + u8 vhf_high[3]; + u8 uhf_low[3]; + u8 uhf_high[3]; +} ranges; + +// the UV-2501+220 & KT8900R has different zones for storing ranges + +#seekto 0x3CD0; +struct { + u8 vhf_low[3]; + u8 vhf_high[3]; + u8 unknown1[4]; + u8 unknown2[6]; + u8 vhf2_low[3]; + u8 vhf2_high[3]; + u8 unknown3[4]; + u8 unknown4[6]; + u8 uhf_low[3]; + u8 uhf_high[3]; +} ranges220; + +#seekto 0x3F70; +struct { + char fp[6]; +} fingerprint; + +""" + + +class BTech(BTechMobileCommon): + """BTECH's UV-5001 and alike radios""" + BANDS = 2 + COLOR_LCD = False + NAME_LENGTH = 6 + + def set_options(self): + """This is to read the options from the image and set it in the + environment, for now just the limits of the freqs in the VHF/UHF + ranges""" + + # setting the correct ranges for each radio type + if self.MODEL in ["UV-2501+220", "KT8900R"]: + # the model 2501+220 has a segment in 220 + # and a different position in the memmap + # also the QYT KT8900R + ranges = self._memobj.ranges220 + else: + ranges = self._memobj.ranges + + # the normal dual bands + vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high) + uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high) + + # DEBUG + LOG.info("Radio ranges: VHF %d to %d" % vhf) + LOG.info("Radio ranges: UHF %d to %d" % uhf) + + # 220Mhz radios case + if self.MODEL in ["UV-2501+220", "KT8900R"]: + vhf2 = _decode_ranges(ranges.vhf2_low, ranges.vhf2_high) + LOG.info("Radio ranges: VHF(220) %d to %d" % vhf2) + self._220_range = vhf2 + + # set the class with the real data + self._vhf_range = vhf + self._uhf_range = uhf + + def process_mmap(self): + """Process the mem map into the mem object""" + + # Get it + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + # load specific parameters from the radio image + self.set_options() + + +# Declaring Aliases (Clones of the real radios) +class JT2705M(chirp_common.Alias): + VENDOR = "Jetstream" + MODEL = "JT2705M" + + +class JT6188Mini(chirp_common.Alias): + VENDOR = "Juentai" + MODEL = "JT-6188 Mini" + + +class JT6188Plus(chirp_common.Alias): + VENDOR = "Juentai" + MODEL = "JT-6188 Plus" + + +class SSGT890(chirp_common.Alias): + VENDOR = "Sainsonic" + MODEL = "GT-890" + + +class ZastoneMP300(chirp_common.Alias): + VENDOR = "Zastone" + MODEL = "MP-300" + + +# real radios +@directory.register +class UV2501(BTech): + """Baofeng Tech UV2501""" + MODEL = "UV-2501" + _fileid = [UV2501G3_fp, + UV2501G2_fp, + UV2501pp2_fp, + UV2501pp_fp] + + +@directory.register +class UV2501_220(BTech): + """Baofeng Tech UV2501+220""" + MODEL = "UV-2501+220" + BANDS = 3 + _magic = MSTRING_220 + _id2 = [UV2501_220pp_id, ] + _fileid = [UV2501_220G3_fp, + UV2501_220G2_fp, + UV2501_220_fp, + UV2501_220pp_fp] + + +@directory.register +class UV5001(BTech): + """Baofeng Tech UV5001""" + MODEL = "UV-5001" + _fileid = [UV5001G3_fp, + UV5001G22_fp, + UV5001G2_fp, + UV5001alpha_fp, + UV5001pp_fp] + _power_levels = [chirp_common.PowerLevel("High", watts=50), + chirp_common.PowerLevel("Low", watts=10)] + + +@directory.register +class MINI8900(BTech): + """WACCOM MINI-8900""" + VENDOR = "WACCOM" + MODEL = "MINI-8900" + _magic = MSTRING_MINI8900 + _fileid = [MINI8900_fp, ] + # Clones + ALIASES = [JT6188Plus, ] + + +@directory.register +class KTUV980(BTech): + """QYT KT-UV980""" + VENDOR = "QYT" + MODEL = "KT-UV980" + _vhf_range = (136000000, 175000000) + _uhf_range = (400000000, 481000000) + _magic = MSTRING_MINI8900 + _fileid = [KTUV980_fp, ] + # Clones + ALIASES = [JT2705M, ] + +# Please note that there is a version of this radios that is a clone of the +# Waccom Mini8900, maybe an early version? + + +@directory.register +class KT9800(BTech): + """QYT KT8900""" + VENDOR = "QYT" + MODEL = "KT8900" + _vhf_range = (136000000, 175000000) + _uhf_range = (400000000, 481000000) + _magic = MSTRING_KT8900 + _fileid = [KT8900_fp, + KT8900_fp1, + KT8900_fp2, + KT8900_fp3, + KT8900_fp4, + KT8900_fp5] + _id2 = [KT8900_id, ] + # Clones + ALIASES = [JT6188Mini, SSGT890, ZastoneMP300] + + +@directory.register +class KT9800R(BTech): + """QYT KT8900R""" + VENDOR = "QYT" + MODEL = "KT8900R" + BANDS = 3 + _vhf_range = (136000000, 175000000) + _220_range = (240000000, 271000000) + _uhf_range = (400000000, 481000000) + _magic = MSTRING_KT8900R + _fileid = [KT8900R_fp, + KT8900R_fp1, + KT8900R_fp2, + KT8900R_fp3, + KT8900R_fp4] + _id2 = [KT8900R_id, KT8900R_id2] + + +@directory.register +class LT588UV(BTech): + """LUITON LT-588UV""" + VENDOR = "LUITON" + MODEL = "LT-588UV" + _vhf_range = (136000000, 175000000) + _uhf_range = (400000000, 481000000) + _magic = MSTRING_KT8900 + _fileid = [LT588UV_fp, + LT588UV_fp1] + _power_levels = [chirp_common.PowerLevel("High", watts=60), + chirp_common.PowerLevel("Low", watts=10)] + + +COLOR_MEM_FORMAT = """ +#seekto 0x0000; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1:2, + spmute:1, + unknown2:3, + optsig:2; + u8 unknown3:3, + scramble:1, + unknown4:3, + power:1; + u8 unknown5:1, + wide:1, + unknown6:2, + bcl:1, + add:1, + pttid:2; +} memory[200]; + +#seekto 0x0E00; +struct { + u8 tmr; + u8 unknown1; + u8 sql; + u8 unknown2[2]; + u8 tot; + u8 apo; + u8 unknown3; + u8 abr; + u8 beep; + u8 unknown4[4]; + u8 dtmfst; + u8 unknown5[2]; + u8 screv; + u8 unknown6[2]; + u8 pttid; + u8 pttlt; + u8 unknown7; + u8 emctp; + u8 emcch; + u8 sigbp; + u8 unknown8; + u8 camdf; + u8 cbmdf; + u8 ccmdf; + u8 cdmdf; + u8 langua; + u8 sync; // BTech radios use this as the display sync + // setting, other radios use this as the auto + // keypad lock setting + u8 mainfc; + u8 mainbc; + u8 menufc; + u8 menubc; + u8 stafc; + u8 stabc; + u8 sigfc; + u8 sigbc; + u8 rxfc; + u8 txfc; + u8 txdisp; + u8 unknown9[5]; + u8 anil; + u8 reps; + u8 repm; + u8 tmrmr; + u8 ste; + u8 rpste; + u8 rptdl; + u8 dtmfg; + u8 mgain; + u8 skiptx; + u8 scmode; +} settings; + +#seekto 0x0E80; +struct { + u8 unknown1; + u8 vfomr; + u8 keylock; + u8 unknown2; + u8 unknown3:4, + vfomren:1, + unknown4:1, + reseten:1, + menuen:1; + u8 unknown5[11]; + u8 dispab; + u8 unknown6[2]; + u8 menu; + u8 unknown7[7]; + u8 vfomra; + u8 vfomrb; + u8 vfomrc; + u8 vfomrd; + u8 mrcha; + u8 mrchb; + u8 mrchc; + u8 mrchd; +} settings2; + +struct settings_vfo { + u8 freq[8]; + u8 offset[6]; + u8 unknown2[2]; + ul16 rxtone; + ul16 txtone; + u8 scode; + u8 spmute; + u8 optsig; + u8 scramble; + u8 wide; + u8 power; + u8 shiftd; + u8 step; + u8 unknown3[4]; +}; + +#seekto 0x0F00; +struct { + struct settings_vfo a; + struct settings_vfo b; + struct settings_vfo c; + struct settings_vfo d; +} vfo; + +#seekto 0x0F80; +struct { + char line1[8]; + char line2[8]; + char line3[8]; + char line4[8]; + char line5[8]; + char line6[8]; + char line7[8]; + char line8[8]; +} poweron_msg; + +#seekto 0x1000; +struct { + char name[8]; + u8 unknown1[8]; +} names[200]; + +#seekto 0x2400; +struct { + u8 period; // one out of LIST_5TONE_STANDARD_PERIODS + u8 group_tone; + u8 repeat_tone; + u8 unused[13]; +} _5tone_std_settings[15]; + +#seekto 0x2500; +struct { + u8 frame1[5]; + u8 frame2[5]; + u8 frame3[5]; + u8 standard; // one out of LIST_5TONE_STANDARDS +} _5tone_codes[15]; + +#seekto 0x25F0; +struct { + u8 _5tone_delay1; // * 10ms + u8 _5tone_delay2; // * 10ms + u8 _5tone_delay3; // * 10ms + u8 _5tone_first_digit_ext_length; + u8 unknown1; + u8 unknown2; + u8 unknown3; + u8 unknown4; + u8 decode_standard; + u8 unknown5:5, + _5tone_decode_call_frame3:1, + _5tone_decode_call_frame2:1, + _5tone_decode_call_frame1:1; + u8 unknown6:5, + _5tone_decode_disp_frame3:1, + _5tone_decode_disp_frame2:1, + _5tone_decode_disp_frame1:1; + u8 decode_reset_time; // * 100 + 100ms +} _5tone_settings; + +#seekto 0x2900; +struct { + u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B +} dtmf_codes[15]; + +#seekto 0x29F0; +struct { + u8 dtmfspeed_on; //list with 50..2000ms in steps of 10 + u8 dtmfspeed_off; //list with 50..2000ms in steps of 10 + u8 unknown0[14]; + u8 inspection[16]; + u8 monitor[16]; + u8 alarmcode[16]; + u8 stun[16]; + u8 kill[16]; + u8 revive[16]; + u8 unknown1[16]; + u8 unknown2[16]; + u8 unknown3[16]; + u8 unknown4[16]; + u8 unknown5[16]; + u8 unknown6[16]; + u8 unknown7[16]; + u8 masterid[16]; + u8 viceid[16]; + u8 unused01:7, + mastervice:1; + u8 unused02:3, + mrevive:1, + mkill:1, + mstun:1, + mmonitor:1, + minspection:1; + u8 unused03:3, + vrevive:1, + vkill:1, + vstun:1, + vmonitor:1, + vinspection:1; + u8 unused04:6, + txdisable:1, + rxdisable:1; + u8 groupcode; + u8 spacecode; + u8 delayproctime; // * 100 + 100ms + u8 resettime; // * 100 + 100ms +} dtmf_settings; + +#seekto 0x2D00; +struct { + struct { + ul16 freq1; + u8 unused01[6]; + ul16 freq2; + u8 unused02[6]; + } _2tone_encode[15]; + u8 duration_1st_tone; // *10ms + u8 duration_2nd_tone; // *10ms + u8 duration_gap; // *10ms + u8 unused03[13]; + struct { + struct { + u8 dec; // one out of LIST_2TONE_DEC + u8 response; // one out of LIST_2TONE_RESPONSE + u8 alert; // 1-16 + } decs[4]; + u8 unused04[4]; + } _2tone_decode[15]; + u8 unused05[16]; + + struct { + ul16 freqA; + ul16 freqB; + ul16 freqC; + ul16 freqD; + // unknown what those values mean, but they are + // derived from configured frequencies + ul16 derived_from_freqA; // 2304000/freqA + ul16 derived_from_freqB; // 2304000/freqB + ul16 derived_from_freqC; // 2304000/freqC + ul16 derived_from_freqD; // 2304000/freqD + }freqs[15]; + u8 reset_time; // * 100 + 100ms - 100-8000ms +} _2tone; + +#seekto 0x3D80; +struct { + u8 vhf_low[3]; + u8 vhf_high[3]; + u8 unknown1[4]; + u8 unknown2[6]; + u8 vhf2_low[3]; + u8 vhf2_high[3]; + u8 unknown3[4]; + u8 unknown4[6]; + u8 uhf_low[3]; + u8 uhf_high[3]; + u8 unknown5[4]; + u8 unknown6[6]; + u8 uhf2_low[3]; + u8 uhf2_high[3]; +} ranges; + +#seekto 0x3F70; +struct { + char fp[6]; +} fingerprint; + +""" + + +class BTechColor(BTechMobileCommon): + """BTECH's Color LCD Mobile and alike radios""" + COLOR_LCD = True + NAME_LENGTH = 8 + LIST_TMR = LIST_TMR16 + + def process_mmap(self): + """Process the mem map into the mem object""" + + # Get it + self._memobj = bitwise.parse(COLOR_MEM_FORMAT, self._mmap) + + # load specific parameters from the radio image + self.set_options() + + def set_options(self): + """This is to read the options from the image and set it in the + environment, for now just the limits of the freqs in the VHF/UHF + ranges""" + + # setting the correct ranges for each radio type + ranges = self._memobj.ranges + + # the normal dual bands + vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high) + uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high) + + # DEBUG + LOG.info("Radio ranges: VHF %d to %d" % vhf) + LOG.info("Radio ranges: UHF %d to %d" % uhf) + + # the additional bands + if self.MODEL in ["UV-25X4", "KT7900D"]: + # 200Mhz band + vhf2 = _decode_ranges(ranges.vhf2_low, ranges.vhf2_high) + LOG.info("Radio ranges: VHF(220) %d to %d" % vhf2) + self._220_range = vhf2 + + # 350Mhz band + uhf2 = _decode_ranges(ranges.uhf2_low, ranges.uhf2_high) + LOG.info("Radio ranges: UHF(350) %d to %d" % uhf2) + self._350_range = uhf2 + + # set the class with the real data + self._vhf_range = vhf + self._uhf_range = uhf + + +# Declaring Aliases (Clones of the real radios) +class SKT8900D(chirp_common.Alias): + VENDOR = "Surecom" + MODEL = "S-KT8900D" + + +class QB25(chirp_common.Alias): + VENDOR = "Radioddity" + MODEL = "QB25" + + +# real radios +@directory.register +class UV25X2(BTechColor): + """Baofeng Tech UV25X2""" + MODEL = "UV-25X2" + BANDS = 2 + _vhf_range = (130000000, 180000000) + _uhf_range = (400000000, 521000000) + _magic = MSTRING_UV25X2 + _fileid = [UV25X2_fp, ] + + +@directory.register +class UV25X4(BTechColor): + """Baofeng Tech UV25X4""" + MODEL = "UV-25X4" + BANDS = 4 + _vhf_range = (130000000, 180000000) + _220_range = (200000000, 271000000) + _uhf_range = (400000000, 521000000) + _350_range = (350000000, 391000000) + _magic = MSTRING_UV25X4 + _fileid = [UV25X4_fp, ] + + +@directory.register +class UV50X2(BTechColor): + """Baofeng Tech UV50X2""" + MODEL = "UV-50X2" + BANDS = 2 + _vhf_range = (130000000, 180000000) + _uhf_range = (400000000, 521000000) + _magic = MSTRING_UV25X2 + _fileid = [UV50X2_fp, ] + _power_levels = [chirp_common.PowerLevel("High", watts=50), + chirp_common.PowerLevel("Low", watts=10)] + + +@directory.register +class KT7900D(BTechColor): + """QYT KT7900D""" + VENDOR = "QYT" + MODEL = "KT7900D" + BANDS = 4 + LIST_TMR = LIST_TMR15 + _vhf_range = (136000000, 175000000) + _220_range = (200000000, 271000000) + _uhf_range = (400000000, 481000000) + _350_range = (350000000, 371000000) + _magic = MSTRING_KT8900D + _fileid = [KT7900D_fp, KT7900D_fp1, KT7900D_fp2, QB25_fp, ] + # Clones + ALIASES = [SKT8900D, QB25, ] + + +@directory.register +class KT8900D(BTechColor): + """QYT KT8900D""" + VENDOR = "QYT" + MODEL = "KT8900D" + BANDS = 2 + LIST_TMR = LIST_TMR15 + _vhf_range = (136000000, 175000000) + _uhf_range = (400000000, 481000000) + _magic = MSTRING_KT8900D + _fileid = [KT8900D_fp, KT8900D_fp1] + + +GMRS_MEM_FORMAT = """ +#seekto 0x0000; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1:2, + spmute:1, + unknown2:3, + optsig:2; + u8 unknown3:3, + scramble:1, + unknown4:2, + power:2; + u8 unknown5:1, + wide:1, + unknown6:2, + bcl:1, + add:1, + pttid:2; +} memory[256]; + +#seekto 0x1000; +struct { + char name[7]; + u8 unknown1[9]; +} names[256]; + +#seekto 0x2400; +struct { + u8 period; // one out of LIST_5TONE_STANDARD_PERIODS + u8 group_tone; + u8 repeat_tone; + u8 unused[13]; +} _5tone_std_settings[15]; + +#seekto 0x2500; +struct { + u8 frame1[5]; + u8 frame2[5]; + u8 frame3[5]; + u8 standard; // one out of LIST_5TONE_STANDARDS +} _5tone_codes[15]; + +#seekto 0x25F0; +struct { + u8 _5tone_delay1; // * 10ms + u8 _5tone_delay2; // * 10ms + u8 _5tone_delay3; // * 10ms + u8 _5tone_first_digit_ext_length; + u8 unknown1; + u8 unknown2; + u8 unknown3; + u8 unknown4; + u8 decode_standard; + u8 unknown5:5, + _5tone_decode_call_frame3:1, + _5tone_decode_call_frame2:1, + _5tone_decode_call_frame1:1; + u8 unknown6:5, + _5tone_decode_disp_frame3:1, + _5tone_decode_disp_frame2:1, + _5tone_decode_disp_frame1:1; + u8 decode_reset_time; // * 100 + 100ms +} _5tone_settings; + +#seekto 0x2900; +struct { + u8 code[16]; // 0=x0A, A=0x0D, B=0x0E, C=0x0F, D=0x00, #=0x0C *=0x0B +} dtmf_codes[15]; + +#seekto 0x29F0; +struct { + u8 dtmfspeed_on; //list with 50..2000ms in steps of 10 + u8 dtmfspeed_off; //list with 50..2000ms in steps of 10 + u8 unknown0[14]; + u8 inspection[16]; + u8 monitor[16]; + u8 alarmcode[16]; + u8 stun[16]; + u8 kill[16]; + u8 revive[16]; + u8 unknown1[16]; + u8 unknown2[16]; + u8 unknown3[16]; + u8 unknown4[16]; + u8 unknown5[16]; + u8 unknown6[16]; + u8 unknown7[16]; + u8 masterid[16]; + u8 viceid[16]; + u8 unused01:7, + mastervice:1; + u8 unused02:3, + mrevive:1, + mkill:1, + mstun:1, + mmonitor:1, + minspection:1; + u8 unused03:3, + vrevive:1, + vkill:1, + vstun:1, + vmonitor:1, + vinspection:1; + u8 unused04:6, + txdisable:1, + rxdisable:1; + u8 groupcode; + u8 spacecode; + u8 delayproctime; // * 100 + 100ms + u8 resettime; // * 100 + 100ms +} dtmf_settings; + +#seekto 0x2D00; +struct { + struct { + ul16 freq1; + u8 unused01[6]; + ul16 freq2; + u8 unused02[6]; + } _2tone_encode[15]; + u8 duration_1st_tone; // *10ms + u8 duration_2nd_tone; // *10ms + u8 duration_gap; // *10ms + u8 unused03[13]; + struct { + struct { + u8 dec; // one out of LIST_2TONE_DEC + u8 response; // one out of LIST_2TONE_RESPONSE + u8 alert; // 1-16 + } decs[4]; + u8 unused04[4]; + } _2tone_decode[15]; + u8 unused05[16]; + + struct { + ul16 freqA; + ul16 freqB; + ul16 freqC; + ul16 freqD; + // unknown what those values mean, but they are + // derived from configured frequencies + ul16 derived_from_freqA; // 2304000/freqA + ul16 derived_from_freqB; // 2304000/freqB + ul16 derived_from_freqC; // 2304000/freqC + ul16 derived_from_freqD; // 2304000/freqD + }freqs[15]; + u8 reset_time; // * 100 + 100ms - 100-8000ms +} _2tone; + +#seekto 0x3000; +struct { + u8 freq[8]; + char broadcast_station_name[6]; + u8 unknown[2]; +} fm_radio_preset[16]; + +#seekto 0x3200; +struct { + u8 tmr; + u8 unknown1; + u8 sql; + u8 unknown2; + u8 autolk; + u8 tot; + u8 apo; + u8 unknown3; + u8 abr; + u8 beep; + u8 unknown4[4]; + u8 dtmfst; + u8 unknown5[2]; + u8 screv; + u8 unknown6[2]; + u8 pttid; + u8 pttlt; + u8 unknown7; + u8 emctp; + u8 emcch; + u8 sigbp; + u8 unknown8; + u8 camdf; + u8 cbmdf; + u8 ccmdf; + u8 cdmdf; + u8 langua; + u8 sync; + + + u8 stfc; + u8 mffc; + u8 sfafc; + u8 sfbfc; + u8 sfcfc; + u8 sfdfc; + u8 subfc; + u8 fmfc; + u8 sigfc; + u8 modfc; + u8 menufc; + u8 txfc; + u8 txdisp; + u8 unknown9[5]; + u8 anil; + u8 reps; + u8 repm; + u8 tmrmr; + u8 ste; + u8 rpste; + u8 rptdl; + u8 dtmfg; + u8 mgain; + u8 skiptx; + u8 scmode; +} settings; + +#seekto 0x3280; +struct { + u8 unknown1; + u8 vfomr; + u8 keylock; + u8 unknown2; + u8 unknown3:4, + vfomren:1, + unknown4:1, + reseten:1, + menuen:1; + u8 unknown5[11]; + u8 dispab; + u8 unknown6[2]; + u8 smenu; + u8 unknown7[7]; + u8 vfomra; + u8 vfomrb; + u8 vfomrc; + u8 vfomrd; + u8 mrcha; + u8 mrchb; + u8 mrchc; + u8 mrchd; +} settings2; + +struct settings_vfo { + u8 freq[8]; + u8 offset[6]; + u8 unknown2[2]; + ul16 rxtone; + ul16 txtone; + u8 scode; + u8 spmute; + u8 optsig; + u8 scramble; + u8 wide; + u8 power; + u8 shiftd; + u8 step; + u8 unknown3[4]; +}; + +#seekto 0x3300; +struct { + struct settings_vfo a; + struct settings_vfo b; + struct settings_vfo c; + struct settings_vfo d; +} vfo; + +#seekto 0x3D80; +struct { + u8 vhf_low[3]; + u8 vhf_high[3]; + u8 unknown1[4]; + u8 unknown2[6]; + u8 vhf2_low[3]; + u8 vhf2_high[3]; + u8 unknown3[4]; + u8 unknown4[6]; + u8 uhf_low[3]; + u8 uhf_high[3]; + u8 unknown5[4]; + u8 unknown6[6]; + u8 uhf2_low[3]; + u8 uhf2_high[3]; +} ranges; + +#seekto 0x33B0; +struct { + char line[16]; +} static_msg; + +#seekto 0x3F70; +struct { + char fp[6]; +} fingerprint; + +""" + + +class BTechGMRS(BTechMobileCommon): + """BTECH's GMRS Mobile""" + COLOR_LCD = True + COLOR_LCD2 = True + NAME_LENGTH = 7 + UPLOAD_MEM_SIZE = 0X3400 + + def process_mmap(self): + """Process the mem map into the mem object""" + + # Get it + self._memobj = bitwise.parse(GMRS_MEM_FORMAT, self._mmap) + + # load specific parameters from the radio image + self.set_options() + + def set_options(self): + """This is to read the options from the image and set it in the + environment, for now just the limits of the freqs in the VHF/UHF + ranges""" + + # setting the correct ranges for each radio type + ranges = self._memobj.ranges + + # the normal dual bands + vhf = _decode_ranges(ranges.vhf_low, ranges.vhf_high) + uhf = _decode_ranges(ranges.uhf_low, ranges.uhf_high) + + # DEBUG + LOG.info("Radio ranges: VHF %d to %d" % vhf) + LOG.info("Radio ranges: UHF %d to %d" % uhf) + + # set the class with the real data + self._vhf_range = vhf + self._uhf_range = uhf + + +# real radios +@directory.register +class GMRS50X1(BTechGMRS): + """Baofeng Tech GMRS50X1""" + MODEL = "GMRS-50X1" + BANDS = 2 + LIST_TMR = LIST_TMR16 + _power_levels = [chirp_common.PowerLevel("High", watts=50), + chirp_common.PowerLevel("Mid", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + _vhf_range = (136000000, 175000000) + _uhf_range = (400000000, 521000000) + _upper = 255 + _magic = MSTRING_GMRS50X1 + _fileid = [GMRS50X1_fp1, GMRS50X1_fp, ] diff --git a/chirp/drivers/fd268.py b/chirp/drivers/fd268.py new file mode 100644 index 0000000..8d9128b --- /dev/null +++ b/chirp/drivers/fd268.py @@ -0,0 +1,914 @@ +# Copyright 2015 Pavel Milanes CO7WT +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import os +import logging + +from chirp import chirp_common, directory, memmap, errors, util, bitwise +from textwrap import dedent +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 unknown:4, + scrambler:1, + unknown1:1, + unknown2:1, + busy_lock:1; + u8 unknown3[3]; +} memory[99]; + +#seekto 0x0640; +struct { + lbcd vrx_freq[4]; + lbcd vtx_freq[4]; + lbcd vrx_tone[2]; + lbcd vtx_tone[2]; + u8 shift_plus:1, + shift_minus:1, + unknown11:2, + scramble:1, + unknown12:1, + unknown13:1, + busy_lock:1; + u8 unknown14[3]; +} vfo; + +#seekto 0x07B0; +struct { + u8 ani_mode; + char ani[3]; + u8 unknown21[12]; + u8 unknown22:5, + bw1:1, // twin setting of bw (LCD "romb") + bs1:1, // twin setting of bs (LCD "S") + warning1:1; // twin setting of warning (LCD "Tune") + u8 sql[1]; + u8 monitorval; + u8 tot[1]; + u8 powerrank; + u8 unknown23[3]; + u8 unknown24[8]; + char model[8]; + u8 unknown26[8]; + u8 step; + u8 unknown27:2, + power:1, + lamp:1, + lamp_auto:1, + key:1, + monitor:1, + bw:1; + u8 unknown28:3, + warning:1, + bs:1, + unknown29:1, + wmem:1, + wvfo:1; + u8 active_ch; + u8 unknown30[4]; + u8 unknown31[4]; + bbcd vfo_shift[4]; +} settings; +""" + +MEM_SIZE = 0x0800 +CMD_ACK = "\x06" +BLOCK_SIZE = 0x08 +POWER_LEVELS = ["Low", "High"] +LIST_SQL = ["Off"] + ["%s" % x for x in range(1, 10)] +LIST_TOT = ["Off"] + ["%s" % x for x in range(10, 100, 10)] +ONOFF = ["Off", "On"] +STEPF = ["5", "10", "6.25", "12.5", "25"] +ACTIVE_CH = ["%s" % x for x in range(1, 100)] +KEY_LOCK = ["Automatic", "Manual"] +BW = ["Narrow", "Wide"] +W_MODE = ["VFO", "Memory"] +VSHIFT = ["None", "-", "+"] +POWER_RANK = ["%s" % x for x in range(0, 28)] +ANI = ["Off", "BOT", "EOT", "Both"] + + +def raw_recv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + raise errors.RadioError("Error reading data from radio") + + return data + + +def raw_send(radio, data): + """Raw write to the radio device""" + try: + data = radio.pipe.write(data) + except: + raise errors.RadioError("Error writing data to radio") + + +def make_frame(cmd, addr, length=BLOCK_SIZE): + """Pack the info in the format it likes""" + return struct.pack(">BHB", ord(cmd), addr, length) + + +def check_ack(r, text): + """Check for a correct ACK from radio, raising error 'Text' + if something was wrong""" + ack = raw_recv(r, 1) + if ack != CMD_ACK: + raise errors.RadioError(text) + else: + return True + + +def send(radio, frame, data=""): + """Generic send data to the radio""" + raw_send(radio, frame) + if data != "": + raw_send(radio, data) + check_ack(radio, "Radio didn't ack the last block of data") + + +def recv(radio): + """Generic receive data from the radio, return just data""" + # you must get it all 12 at once (4 header + 8 data) + rxdata = raw_recv(radio, 12) + if (len(rxdata) != 12): + raise errors.RadioError( + "Radio sent %i bytes, we expected 12" % (len(rxdata))) + else: + data = rxdata[4:] + send(radio, CMD_ACK) + check_ack(radio, "Radio didn't ack the sended data") + return data + + +def do_magic(radio): + """Try to get the radio in program mode, the factory software + (FDX-288) tries up to ~16 times to get the correct response, + we will do the same, but with a lower count.""" + tries = 8 + # UI information + status = chirp_common.Status() + status.cur = 0 + status.max = tries + status.msg = "Linking to radio, please wait." + radio.status_fn(status) + + # every byte of this magic chain must be send separatedly + magic = "\x02PROGRA" + + # start the fun, finger crossed please... + for a in range(0, tries): + + # UI update + status.cur = a + radio.status_fn(status) + + for i in range(0, len(magic)): + send(radio, magic[i]) + + # Now you get a x06 of ACK + ack = raw_recv(radio, 1) + if ack == CMD_ACK: + return True + + return False + + +def do_program(radio): + """Feidaxin program mode and identification dance""" + # try to get the radio in program mode + ack = do_magic(radio) + if not ack: + erc = "Radio did not accept program mode, " + erc += "check your cable and radio; then try again." + raise errors.RadioError(erc) + + # now we request identification + send(radio, "M") + send(radio, "\x02") + ident = raw_recv(radio, 8) + + # ################ WARNING ########################################## + # Feidaxin radios has a "send id" procedure in the initial handshake + # but it's worthless, once you do a hardware reset the ident area + # get all 0xFF. + # + # Even FDX-288 software appears to match the model by any other + # mean, so I detected on a few images that the 3 first bytes are + # unique to each radio model, so for now we use that method untill + # proven otherwise + # ################################################################### + + LOG.debug("Radio's ID string:") + LOG.debug(util.hexprint(ident)) + + # final ACK + send(radio, CMD_ACK) + check_ack(radio, "Radio refused to enter programming mode") + + +def do_download(radio): + """ The download function """ + do_program(radio) + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + data = "" + for addr in range(0x0000, MEM_SIZE, BLOCK_SIZE): + send(radio, make_frame("R", addr)) + d = recv(radio) + data += d + # UI Update + status.cur = addr + radio.status_fn(status) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + """The upload function""" + do_program(radio) + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + for addr in range(0x0000, MEM_SIZE, BLOCK_SIZE): + send(radio, make_frame("W", addr), + radio.get_mmap()[addr:addr+BLOCK_SIZE]) + # UI Update + status.cur = addr + radio.status_fn(status) + + +def model_match(cls, data): + """Use a experimental guess to determine if the radio you just + downloaded or the img opened you is for this model""" + + # Using a few imgs of some FD radio I found that the four first + # bytes it's like the model fingerprint, so we have to testing the + # model type with this experimental method so far. + fp = data[0:4] + if fp == cls._IDENT: + return True + else: + LOG.debug("Unknowd Feidaxing radio, ID:") + LOG.debug(util.hexprint(fp)) + + return False + + +class FeidaxinFD2x8yRadio(chirp_common.CloneModeRadio): + """Feidaxin FD-268 & alike Radios""" + VENDOR = "Feidaxin" + MODEL = "FD-268 & alike Radios" + BAUD_RATE = 9600 + _memsize = MEM_SIZE + _upper = 99 + _VFO_DEFAULT = 0 + _IDENT = "" + _active_ch = ACTIVE_CH + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The program mode of this radio has his tricks, ' + 'so this driver is *completely experimental*.') + rp.pre_download = _(dedent("""\ + This radio has a tricky way of enter into program mode, + even the original software has a few tries to get inside. + + I will try 8 times (most of the time ~3 will doit) and this + can take a few seconds, if don't work, try again a few times. + + If you can get into it, please check the radio and cable. + """)) + rp.pre_upload = _(dedent("""\ + This radio has a tricky way of enter into program mode, + even the original software has a few tries to get inside. + + I will try 8 times (most of the time ~3 will doit) and this + can take a few seconds, if don't work, try again a few times. + + If you can get into it, please check the radio and cable. + """)) + return rp + + def get_features(self): + """Return information about this radio's features""" + rf = chirp_common.RadioFeatures() + # this feature is READ ONLY by now. + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.has_name = False + rf.has_offset = True + rf.has_mode = False + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + # we have to remove "Tone->" because this is the same to "TQSL" + # I get a few days hitting the wall with my head about this... + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + # Power levels are golbal and no per channel, so disabled + # rf.valid_power_levels = POWER_LEVELS + # this radio has no skips + rf.valid_skips = [] + # this radio modes are global and not per channel, so just FM + rf.valid_modes = ["FM"] + rf.valid_bands = [self._range] + rf.memory_bounds = (1, self._upper) + return rf + + def sync_in(self): + """Do a download of the radio eeprom""" + data = do_download(self) + + # as the radio comm protocol's identification is useless + # we test the model after having the img + if not model_match(self, data): + # ok, wrong model, fire an error + erc = "EEPROM fingerprint don't match, check if you " + erc += "selected the right radio model." + raise errors.RadioError(erc) + + # all ok + self._mmap = data + self.process_mmap() + + def sync_out(self): + """Do an upload to the radio eeprom""" + do_upload(self) + + def process_mmap(self): + """Process the memory object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + """Return a raw representation of the memory object""" + return repr(self._memobj.memory[number]) + + def _decode_tone(self, val): + """Parse the tone data to decode from mem, it returns""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def _encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_memory(self, number): + """Extract a high-level memory object from the low-level + memory map, This is called to populate a memory in the UI""" + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[number - 1] + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + # number + mem.number = number + + # empty + if _mem.get_raw()[0] == "\xFF": + mem.empty = True + return mem + + # rx freq + mem.freq = int(_mem.rx_freq) * 10 + + # checking if tx freq is empty, this is "possible" on the + # original soft after a warning, and radio is happy with it + if _mem.tx_freq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.duplex = "off" + mem.offset = 0 + else: + rx = int(_mem.rx_freq) * 10 + tx = int(_mem.tx_freq) * 10 + if tx == rx: + mem.offset = 0 + mem.duplex = "" + else: + mem.duplex = rx > tx and "-" or "+" + mem.offset = abs(tx - rx) + + # tone data + txtone = self._decode_tone(_mem.tx_tone) + rxtone = self._decode_tone(_mem.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra setting group, FD-268 don't uset it at all + # FD-288's & others do it? + mem.extra = RadioSettingGroup("extra", "Extra") + busy = RadioSetting("Busy", "Busy Channel Lockout", + RadioSettingValueBoolean( + bool(_mem.busy_lock))) + mem.extra.append(busy) + scramble = RadioSetting("Scrambler", "Scrambler Option", + RadioSettingValueBoolean( + bool(_mem.scrambler))) + mem.extra.append(scramble) + + # return mem + return mem + + def set_memory(self, mem): + """Store details about a high-level memory to the memory map + This is called when a user edits a memory in the UI""" + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number - 1] + + # Empty memory + if mem.empty: + _mem.set_raw("\xFF" * 16) + return + + # freq rx + _mem.rx_freq = mem.freq / 10 + + # freq tx + if mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + else: + _mem.tx_freq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.tx_tone, txmode, txtone, txpol) + self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol) + + # extra settings + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + return mem + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic") + work = RadioSettingGroup("work", "Work Mode Settings") + top = RadioSettings(basic, work) + + # Basic + sql = RadioSetting("settings.sql", "Squelch Level", + RadioSettingValueList(LIST_SQL, LIST_SQL[ + _mem.settings.sql])) + basic.append(sql) + + tot = RadioSetting("settings.tot", "Time out timer", + RadioSettingValueList(LIST_TOT, LIST_TOT[ + _mem.settings.tot])) + basic.append(tot) + + key_lock = RadioSetting("settings.key", "Keyboard Lock", + RadioSettingValueList(KEY_LOCK, KEY_LOCK[ + _mem.settings.key])) + basic.append(key_lock) + + bw = RadioSetting("settings.bw", "Bandwidth", + RadioSettingValueList(BW, BW[_mem.settings.bw])) + basic.append(bw) + + powerrank = RadioSetting("settings.powerrank", "Power output adjust", + RadioSettingValueList(POWER_RANK, POWER_RANK[ + _mem.settings.powerrank])) + basic.append(powerrank) + + lamp = RadioSetting("settings.lamp", "LCD Lamp", + RadioSettingValueBoolean(_mem.settings.lamp)) + basic.append(lamp) + + lamp_auto = RadioSetting("settings.lamp_auto", "LCD Lamp auto on/off", + RadioSettingValueBoolean( + _mem.settings.lamp_auto)) + basic.append(lamp_auto) + + bs = RadioSetting("settings.bs", "Battery Save", + RadioSettingValueBoolean(_mem.settings.bs)) + basic.append(bs) + + warning = RadioSetting("settings.warning", "Warning Alerts", + RadioSettingValueBoolean(_mem.settings.warning)) + basic.append(warning) + + monitor = RadioSetting("settings.monitor", "Monitor key", + RadioSettingValueBoolean(_mem.settings.monitor)) + basic.append(monitor) + + # Work mode settings + wmset = RadioSetting("settings.wmem", "VFO/MR Mode", + RadioSettingValueList( + W_MODE, W_MODE[_mem.settings.wmem])) + work.append(wmset) + + power = RadioSetting("settings.power", "Actual Power", + RadioSettingValueList(POWER_LEVELS, POWER_LEVELS[ + _mem.settings.power])) + work.append(power) + + active_ch = RadioSetting("settings.active_ch", "Work Channel", + RadioSettingValueList(ACTIVE_CH, ACTIVE_CH[ + _mem.settings.active_ch])) + work.append(active_ch) + + # vfo rx validation + if _mem.vfo.vrx_freq.get_raw()[0] == "\xFF": + # if the vfo is not set, the UI cares about the + # length of the field, so set a default + LOG.debug("Setting VFO to default %s" % self._VFO_DEFAULT) + vfo = self._VFO_DEFAULT + else: + vfo = int(_mem.vfo.vrx_freq) * 10 + + # frecuency apply_callback + def apply_freq(setting, obj): + """Setting is the UI returned value, obj is the memmap object""" + value = chirp_common.parse_freq(str(setting.value)) + obj.set_value(value / 10) + + # preparing for callback on vrxfreq (handled also in a special ) + vf_freq = RadioSetting("none.vrx_freq", "VFO frequency", + RadioSettingValueString(0, 10, chirp_common. + format_freq(vfo))) + vf_freq.set_apply_callback(apply_freq, _mem.vfo.vrx_freq) + work.append(vf_freq) + + # shift works + # VSHIFT = ["None", "-", "+"] + sset = 0 + if bool(_mem.vfo.shift_minus) is True: + sset = 1 + elif bool(_mem.vfo.shift_plus) is True: + sset = 2 + + shift = RadioSetting("none.shift", "VFO Shift", + RadioSettingValueList(VSHIFT, VSHIFT[sset])) + work.append(shift) + + # vfo shift validation if none set it to ZERO + if _mem.settings.vfo_shift.get_raw()[0] == "\xFF": + # if the shift is not set, the UI cares about the + # length of the field, so set to zero + LOG.debug("VFO shift not set, setting it to zero") + vfo_shift = 0 + else: + vfo_shift = int(_mem.settings.vfo_shift) * 10 + + offset = RadioSetting("none.vfo_shift", "VFO Offset", + RadioSettingValueString(0, 9, chirp_common. + format_freq(vfo_shift))) + work.append(offset) + + step = RadioSetting("settings", "VFO step", + RadioSettingValueList(STEPF, STEPF[ + _mem.settings.step])) + work.append(step) + + # FD-288 Family ANI settings + if "FD-288" in self.MODEL: + ani_mode = RadioSetting("ani_mode", "ANI ID", + RadioSettingValueList(ANI, ANI[ + _mem.settings.ani_mode])) + work.append(ani_mode) + + # it can't be \xFF + ani_value = str(_mem.settings.ani) + if ani_value == "\xFF\xFF\xFF": + ani_value = "200" + + ani_value = "".join(x for x in ani_value if (int(x) >= 2 and + int(x) <= 9)) + + ani = RadioSetting("ani", "ANI (200-999)", + RadioSettingValueString(0, 3, ani_value)) + work.append(ani) + + return top + + def set_settings(self, settings): + """Set settings in the Chirp way.""" + + # special case: shift handling + def handle_shift(_vfo, settings): + """_vfo is mmap obj for vfo, settings is for all UI settings""" + + # reset the shift in the memmap + _vfo.shift_minus = 0 + _vfo.shift_plus = 0 + + # parse and set if needed + rx = chirp_common.parse_freq( + str(settings["none.vrx_freq"]).split(":")[1]) + + offset = chirp_common.parse_freq( + str(settings["none.vfo_shift"]).split(":")[1]) + + shift = str(settings["none.shift"]).split(":")[1] + + if shift == "None" or shift == "": + # no shift + _vfo.vtx_freq = rx / 10 + if shift == "-": + # minus shift + _vfo.vtx_freq = (rx - offset) / 10 + if shift == "+": + # plus shift + _vfo.vtx_freq = (rx + offset) / 10 + + # special case: narrow/wide at radio level and display icon + def handle_width(_settings, settings): + """_settings is mmap obj for settings, + settings is all the UI settings""" + + value = str(settings["settings.bw"]).split(":")[1] + + # set bw in settings + if value == "Wide": + _settings.bw.set_value(1) + _settings.bw1.set_value(0) + else: + _settings.bw.set_value(0) + _settings.bw1.set_value(1) + + # special case: battery save and display icon + def handle_bsave(_settings, settings): + """_settings is mmap obj for settings, + settings is all the UI settings""" + + value = str(settings["settings.bs"]).split(":")[1] + + # set bs in settings + if value == "True": + _settings.bs.set_value(1) + _settings.bs1.set_value(0) + else: + _settings.bs.set_value(0) + _settings.bs1.set_value(1) + + # special case: warning tones and display icon + def handle_warning(_settings, settings): + """_settings is mmap obj for settings, + settings is all the UI settings""" + + value = str(settings["settings.warning"]).split(":")[1] + + # set warning in settings + if value == "True": + _settings.warning.set_value(1) + _settings.warning1.set_value(0) + else: + _settings.warning.set_value(0) + _settings.warning1.set_value(1) + + _mem = self._memobj + for element in settings: + name = element.get_name() + + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + + if not element.changed(): + continue + + sett, name = name.split(".") + + try: + # special cases + if name in ["vrx_freq", "vfo_shift", "shift"]: + # call local callback + print("Calling local callback for shift handling") + handle_shift(self._memobj.vfo, settings) + continue + + if name == "bw": + # call local callback + print("Calling local callback for bw handling") + handle_width(self._memobj.settings, settings) + continue + + if name == "bs": + # call local callback + print("Calling local callback for bs handling") + handle_bsave(self._memobj.settings, settings) + continue + + if name == "warning": + # call local callback + print("Calling local callback for warning handling") + handle_warning(self._memobj.settings, settings) + continue + + if element.has_apply_callback(): + try: + element.run_apply_callback() + continue + except NotImplementedError as e: + raise + + elif sett == "none": + LOG.debug("Setting %sett.%s is ignored" % (sett, name)) + continue + + elif element.value.get_mutable(): + # value is mutable, find it on the mem space + + LOG.debug("Setting %s.%s = %s" % (sett, name, str( + element.value))) + + # process it + try: + obj = getattr(_mem, sett) + setattr(obj, name, element.value) + + except AttributeError, e: + m = "Setting %s is not in this setting block" % name + LOG.debug(m) + + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware fingerprint, this experimental + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + +# ##########################################################################3 +# FD-268 family: this are the original tested models, FD-268B UHF +# was tested "remotely" with images thanks to AG5M +# I just have the 268A in hand to test + + +@directory.register +class FD268ARadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-268A Radio""" + MODEL = "FD-268A" + _range = (136000000, 174000000) + _VFO_DEFAULT = 145000000 + _IDENT = "\xFF\xEE\x46\xFF" + + +@directory.register +class FD268BRadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-268B Radio""" + MODEL = "FD-268B" + _range = (400000000, 470000000) + _VFO_DEFAULT = 439000000 + _IDENT = "\xFF\xEE\x47\xFF" + + +# ##################################################################### +# FD-288 Family: the only difference from this family to the FD-268's +# are the ANI settings +# Tested hacking the FD-268A memmory + + +@directory.register +class FD288ARadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-288A Radio""" + MODEL = "FD-288A" + _range = (136000000, 174000000) + _VFO_DEFAULT = 145000000 + _IDENT = "\xFF\xEE\x4B\xFF" + + +@directory.register +class FD288BRadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-288B Radio""" + MODEL = "FD-288B" + _range = (400000000, 470000000) + _VFO_DEFAULT = 439000000 + _IDENT = "\xFF\xEE\x4C\xFF" + + +# ##################################################################### +# The following radios was tested hacking the FD-268A memmory with +# the software and found to be clones of FD-268 ones +# please report any problems to driver author (see head of this file) + + +@directory.register +class FD150ARadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-150A Radio""" + MODEL = "FD-150A" + _range = (136000000, 174000000) + _VFO_DEFAULT = 145000000 + _IDENT = "\xFF\xEE\x45\xFF" + + +@directory.register +class FD160ARadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-160A Radio""" + MODEL = "FD-160A" + _range = (136000000, 174000000) + _VFO_DEFAULT = 145000000 + _IDENT = "\xFF\xEE\x48\xFF" + + +@directory.register +class FD450ARadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-450A Radio""" + MODEL = "FD-450A" + _range = (400000000, 470000000) + _VFO_DEFAULT = 439000000 + _IDENT = "\xFF\xEE\x44\xFF" + + +@directory.register +class FD460ARadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-460A Radio""" + MODEL = "FD-460A" + _range = (400000000, 470000000) + _VFO_DEFAULT = 439000000 + _IDENT = "\xFF\xEE\x4A\xFF" + + +@directory.register +class FD460UHRadio(FeidaxinFD2x8yRadio): + """Feidaxin FD-460UH Radio""" + MODEL = "FD-460UH" + _range = (400000000, 480000000) + _VFO_DEFAULT = 439000000 + _IDENT = "\xFF\xF4\x44\xFF" diff --git a/chirp/drivers/ft1500m.py b/chirp/drivers/ft1500m.py new file mode 100644 index 0000000..16778f4 --- /dev/null +++ b/chirp/drivers/ft1500m.py @@ -0,0 +1,235 @@ +# Copyright 2019 Josh Small VK2HFF +# - Derived from ./ft1802.py Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# FT-1500M Clone Proceedure +# 1. Turn radio off. +# 2. Connect cable to mic jack. +# 3. Press and hold in the [MHz],[LOW] and [D/MR] keys +# while turning the radio on. +# 4. In Chirp, choose Download from Radio. +# 5. Press the [MHz(SET)] key to send image. +# or +# 4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD). +# 5. In Chirp, choose Upload to Radio. + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, bitwise, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettings +from textwrap import dedent + +MEM_FORMAT = """ +#seekto 0x002a; +struct { + u8 unknown_f:4, + pskip:1, + skip:1, + visible:1, + valid:1; +} flags[130]; + +#seekto 0x00ca; +struct { + u8 unknown1a:2, + narrow:1, + clk_shift:1, + unknown1b:4; + u8 unknown2a:3, + unknown2b:2, + tune_step:3; + bbcd freq[3]; + u8 tone; + u8 name[6]; + u8 unknown3; + u8 offset; + u8 unknown4a:1, + unknown4b:1, + unknown4c:2, + unknown4d:4; + u8 unknown5a:2, + tmode:2, + power:2, + duplex:2; +} memory[130]; +""" + + +MODES = ["FM", "NFM"] +TMODES = ["", "Tone", "TSQL"] +DUPLEX = ["", "-", "+"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +POWER_LEVELS = [chirp_common.PowerLevel("LOW1", watts=5), + chirp_common.PowerLevel("LOW2", watts=10), + chirp_common.PowerLevel("LOW3", watts=25), + chirp_common.PowerLevel("HIGH", watts=50), + ] +CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?()?_" + + +@directory.register +class FT1500Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FT-1500M""" + VENDOR = "Yaesu" + MODEL = "FT-1500M" + BAUD_RATE = 9600 + + _model = "AH4N0" + _block_lengths = [10, 16, 3953] + _memsize = 3979 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to mic jack. +3. Press and hold in the [MHz], [LOW], and [D/MR] keys + while turning the radio on. +4. After clicking OK, press the [MHz(SET)] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to mic jack. +3. Press and hold in the [MHz], [LOW], and [D/MR] keys + while turning the radio on. +4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD).""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.memory_bounds = (0, 129) + + rf.can_odd_split = False + rf.has_ctone = False + rf.has_tuning_step = True + rf.has_dtcs_polarity = False + rf.has_bank = False + + rf.valid_tuning_steps = STEPS + rf.valid_modes = MODES + rf.valid_tmodes = TMODES + rf.valid_bands = [(137000000, 174000000)] + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = DUPLEX + rf.valid_skips = ["", "S", "P"] + rf.valid_name_length = 6 + rf.valid_characters = CHARSET + rf.has_cross = False + + return rf + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0, self._memsize-2)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + \ + repr(self._memobj.flags[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number] + visible = _flag["visible"] + valid = _flag["valid"] + skip = _flag["skip"] + pskip = _flag["pskip"] + mem = chirp_common.Memory() + mem.number = number + + if not visible: + mem.empty = True + if not valid: + mem.empty = True + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 100000) + mem.duplex = DUPLEX[_mem.duplex] + mem.tuning_step = STEPS[_mem.tune_step] or STEPS[0] + mem.tmode = TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.tone] + for i in _mem.name: + if i == 0xFF: + break + if i & 0x80 == 0x80: + mem.name += CHARSET[0x80 ^ int(i)] + else: + mem.name += CHARSET[i] + mem.name = mem.name.rstrip() + mem.mode = _mem.narrow and "NFM" or "FM" + mem.skip = pskip and "P" or skip and "S" or "" + mem.power = POWER_LEVELS[_mem.power] + + mem.extra = RadioSettingGroup("extra", "Extra Settings") + rs = RadioSetting("clk_shift", "Clock Shift", + RadioSettingValueBoolean(_mem.clk_shift)) + mem.extra.append(rs) + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _flag = self._memobj.flags[mem.number] + valid = _flag["valid"] + visible = _flag["visible"] + + if not mem.empty and not valid: + _flag["valid"] = True + _mem.unknown1a = 0x00 + _mem.clk_shift = 0x00 + _mem.unknown1b = 0x00 + _mem.unknown2a = 0x00 + _mem.unknown2b = 0x00 + _mem.unknown3 = 0x00 + _mem.unknown4a = 0x00 + _mem.unknown4b = 0x00 + _mem.unknown4c = 0x00 + _mem.unknown4d = 0x00 + _mem.unknown5a = 0x00 + + if mem.empty and valid and not visible: + _flag["valid"] = False + return + _flag["visible"] = not mem.empty + + if mem.empty: + return + + _flag["valid"] = True + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 100000 + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.tune_step = STEPS.index(mem.tuning_step) + _mem.tmode = TMODES.index(mem.tmode) + _mem.tone = chirp_common.TONES.index(mem.rtone) + + _mem.name = [0xFF] * 6 + for i in range(0, len(mem.name)): + try: + _mem.name[i] = CHARSET.index(mem.name[i]) + except IndexError: + raise Exception("Character `%s' not supported") + + _mem.narrow = MODES.index(mem.mode) + _mem.power = 3 if mem.power is None else POWER_LEVELS.index(mem.power) + + _flag["pskip"] = mem.skip == "P" + _flag["skip"] = mem.skip == "S" + + for element in mem.extra: + setattr(_mem, element.get_name(), element.value) diff --git a/chirp/drivers/ft1802.py b/chirp/drivers/ft1802.py new file mode 100644 index 0000000..952a23c --- /dev/null +++ b/chirp/drivers/ft1802.py @@ -0,0 +1,253 @@ +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# FT-1802 Clone Proceedure +# 1. Turn radio off. +# 2. Connect cable to mic jack. +# 3. Press and hold in the [LOW(A/N)] key while turning the radio on. +# 4. In Chirp, choose Download from Radio. +# 5. Press the [MHz(SET)] key to send image. +# or +# 4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD). +# 5. In Chirp, choose Upload to Radio. + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, bitwise, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettings +from textwrap import dedent + +MEM_FORMAT = """ +#seekto 0x06ea; +struct { + u8 odd_pskip:1, + odd_skip:1, + odd_visible:1, + odd_valid:1, + even_pskip:1, + even_skip:1, + even_visible:1, + even_valid:1; +} flags[100]; + +#seekto 0x076a; +struct { + u8 unknown1a:1, + step_changed:1, + narrow:1, + clk_shift:1, + unknown1b:4; + u8 unknown2a:2, + duplex:2, + unknown2b:1, + tune_step:3; + bbcd freq[3]; + u8 power:2, + unknown3:3, + tmode:3; + u8 name[6]; + bbcd offset[3]; + u8 tone; + u8 dtcs; + u8 unknown4; +} memory[200]; +""" + + +MODES = ["FM", "NFM"] +TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"] +CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone"] +DUPLEX = ["", "-", "+", "split"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +POWER_LEVELS = [chirp_common.PowerLevel("LOW1", watts=5), + chirp_common.PowerLevel("LOW2", watts=10), + chirp_common.PowerLevel("LOW3", watts=25), + chirp_common.PowerLevel("HIGH", watts=50), + ] +CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?()?_" + + +@directory.register +class FT1802Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FT-1802""" + VENDOR = "Yaesu" + MODEL = "FT-1802M" + BAUD_RATE = 19200 + + _model = "AH023" + _block_lengths = [10, 8001] + _memsize = 8011 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to mic jack. +3. Press and hold in the [LOW(A/N)] key while turning the radio on. +4. After clicking OK, press the [MHz(SET)] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to mic jack. +3. Press and hold in the [LOW(A/N)] key while turning the radio on. +4. Press the [D/MR(MW)] key ("--WAIT--" will appear on the LCD).""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.memory_bounds = (0, 199) + + rf.can_odd_split = True + rf.has_ctone = False + rf.has_tuning_step = True + rf.has_dtcs_polarity = False # in radio settings, not per memory + rf.has_bank = False # has banks, but not implemented + + rf.valid_tuning_steps = STEPS + rf.valid_modes = MODES + rf.valid_tmodes = TMODES + rf.valid_bands = [(137000000, 174000000)] + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = DUPLEX + rf.valid_skips = ["", "S", "P"] + rf.valid_name_length = 6 + rf.valid_characters = CHARSET + rf.has_cross = True + rf.valid_cross_modes = list(CROSS_MODES) + + return rf + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0, self._memsize-2)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + \ + repr(self._memobj.flags[number/2]) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number/2] + + nibble = (number % 2) and "odd" or "even" + visible = _flag["%s_visible" % nibble] + valid = _flag["%s_valid" % nibble] + pskip = _flag["%s_pskip" % nibble] + skip = _flag["%s_skip" % nibble] + + mem = chirp_common.Memory() + mem.number = number + + if not visible: + mem.empty = True + if not valid: + mem.empty = True + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000) + mem.duplex = DUPLEX[_mem.duplex] + mem.tuning_step = _mem.step_changed and \ + STEPS[_mem.tune_step] or STEPS[0] + if _mem.tmode < TMODES.index("Cross"): + mem.tmode = TMODES[_mem.tmode] + mem.cross_mode = CROSS_MODES[0] + else: + mem.tmode = "Cross" + mem.cross_mode = CROSS_MODES[_mem.tmode - TMODES.index("Cross")] + mem.rtone = chirp_common.TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + for i in _mem.name: + if i == 0xFF: + break + if i & 0x80 == 0x80: + # first bit in name is "show name" + mem.name += CHARSET[0x80 ^ int(i)] + else: + mem.name += CHARSET[i] + mem.name = mem.name.rstrip() + mem.mode = _mem.narrow and "NFM" or "FM" + mem.skip = pskip and "P" or skip and "S" or "" + mem.power = POWER_LEVELS[_mem.power] + + mem.extra = RadioSettingGroup("extra", "Extra Settings") + rs = RadioSetting("clk_shift", "Clock Shift", + RadioSettingValueBoolean(_mem.clk_shift)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _flag = self._memobj.flags[mem.number/2] + + nibble = (mem.number % 2) and "odd" or "even" + + valid = _flag["%s_valid" % nibble] + visible = _flag["%s_visible" % nibble] + + if not mem.empty and not valid: + _flag["%s_valid" % nibble] = True + _mem.unknown1a = 0x00 + _mem.clk_shift = 0x00 + _mem.unknown1b = 0x00 + _mem.unknown2a = 0x00 + _mem.unknown2b = 0x00 + _mem.unknown3 = 0x00 + _mem.unknown4 = 0x00 + + if mem.empty and valid and not visible: + _flag["%s_valid" % nibble] = False + return + _flag["%s_visible" % nibble] = not mem.empty + + if mem.empty: + return + + _flag["%s_valid" % nibble] = True + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 1000 + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.tune_step = STEPS.index(mem.tuning_step) + _mem.step_changed = mem.tuning_step != STEPS[0] + if mem.tmode != "Cross": + _mem.tmode = TMODES.index(mem.tmode) + else: + _mem.tmode = TMODES.index("Cross") + \ + CROSS_MODES.index(mem.cross_mode) + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + + _mem.name = [0xFF] * 6 + for i in range(0, len(mem.name)): + try: + _mem.name[i] = CHARSET.index(mem.name[i]) + except IndexError: + raise Exception("Character `%s' not supported") + if _mem.name[0] != 0xFF: + _mem.name[0] += 0x80 # show name instead of frequency + + _mem.narrow = MODES.index(mem.mode) + _mem.power = 3 if mem.power is None else POWER_LEVELS.index(mem.power) + + _flag["%s_pskip" % nibble] = mem.skip == "P" + _flag["%s_skip" % nibble] = mem.skip == "S" + + for element in mem.extra: + setattr(_mem, element.get_name(), element.value) diff --git a/chirp/drivers/ft1d.py b/chirp/drivers/ft1d.py new file mode 100644 index 0000000..06a20ed --- /dev/null +++ b/chirp/drivers/ft1d.py @@ -0,0 +1,1988 @@ +# Copyright 2010 Dan Smith +# Copyright 2014 Angus Ainslie +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re +import string +import logging + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueList, RadioSettingValueBoolean, \ + InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_SETTINGS_FORMAT = """ +#seekto 0x049a; +struct { + u8 vfo_a; + u8 vfo_b; +} squelch; + +#seekto 0x04c1; +struct { + u8 beep; +} beep_select; + +#seekto 0x04ce; +struct { + u8 lcd_dimmer; + u8 dtmf_delay; + u8 unknown0[3]; + u8 unknown1:4 + lcd_contrast:4; + u8 lamp; + u8 unknown2[7]; + u8 scan_restart; + u8 unknown3; + u8 scan_resume; + u8 unknown4[5]; + u8 tot; + u8 unknown5[3]; + u8 unknown6:2, + scan_lamp:1, + unknown7:2, + dtmf_speed:1, + unknown8:1, + dtmf_mode:1; + u8 busy_led:1, + unknown9:7; + u8 unknown10[2]; + u8 vol_mode:1, + unknown11:7; +} scan_settings; + +#seekto 0x064a; +struct { + u8 unknown0[4]; + u8 frequency_band; + u8 unknown1:6, + manual_or_mr:2; + u8 unknown2:7, + mr_banks:1; + u8 unknown3; + u16 mr_index; + u16 bank_index; + u16 bank_enable; + u8 unknown4[5]; + u8 unknown5:6, + power:2; + u8 unknown6:4, + tune_step:4; + u8 unknown7:6, + duplex:2; + u8 unknown8:6, + tone_mode:2; + u8 unknown9:2, + tone:6; + u8 unknown10; + u8 unknown11:6, + mode:2; + bbcd freq0[4]; + bbcd offset_freq[4]; + u8 unknown12[2]; + char label[16]; + u8 unknown13[6]; + bbcd band_lower[4]; + bbcd band_upper[4]; + bbcd rx_freq[4]; + u8 unknown14[22]; + bbcd freq1[4]; + u8 unknown15[11]; + u8 unknown16:3, + volume:5; + u8 unknown17[18]; + u8 active_menu_item; + u8 checksum; +} vfo_info[6]; + +#seekto 0x047e; +struct { + u8 unknown1; + u8 flag; + u16 unknown2; + struct { + u8 padded_yaesu[16]; + } message; +} opening_message; + +#seekto 0x%04X; // FT-1D:0e4a, FT2D:094a +struct { + u8 memory[16]; +} dtmf[10]; + +#seekto 0x154a; +// These "channels" seem to actually be a structure: +// first five bits are flags +// 0 Unused (1=entry is unused) +// 1 SW Broadcast +// 2 VHF Marine +// 3 WX (weather) +// 4 ? a mode? ? +// 11 bits of index into frequency tables +// +struct { + u16 channel[100]; +} bank_members[24]; + +#seekto 0x54a; +struct { + u16 in_use; +} bank_used[24]; + +#seekto 0x0EFE; +struct { + u8 unknown[2]; + u8 name[16]; +} bank_info[24]; +""" + +MEM_FORMAT = """ +#seekto 0x2D4A; +struct { + u8 unknown0:2, + mode_alt:1, // mode for FTM-3200D + unknown1:5; + u8 mode:2, + duplex:2, + tune_step:4; + bbcd freq[3]; + u8 power:2, + unknown2:2, + tone_mode:4; + u8 charsetbits[2]; + char label[16]; + bbcd offset[3]; + u8 unknown5:2, + tone:6; + u8 unknown6:1, + dcs:7; + u8 unknown7[3]; +} memory[%d]; + +#seekto 0x280A; +struct { + u8 nosubvfo:1, + unknown:3, + pskip:1, + skip:1, + used:1, + valid:1; +} flag[%d]; +""" + +MEM_APRS_FORMAT = """ +#seekto 0xbeca; +struct { + u8 rx_baud; + u8 custom_symbol; + struct { + char callsign[6]; + u8 ssid; + } my_callsign; + u8 unknown3:4, + selected_position_comment:4; + u8 unknown4; + u8 set_time_manually:1, + tx_interval_beacon:1, + ring_beacon:1, + ring_msg:1, + aprs_mute:1, + unknown6:1, + tx_smartbeacon:1, + af_dual:1; + u8 unknown7:1, + aprs_units_wind_mph:1, + aprs_units_rain_inch:1, + aprs_units_temperature_f:1 + aprs_units_altitude_ft:1, + unknown8:1, + aprs_units_distance_m:1, + aprs_units_position_mmss:1; + u8 unknown9:6, + aprs_units_speed:2; + u8 unknown11:1, + filter_other:1, + filter_status:1, + filter_item:1, + filter_object:1, + filter_weather:1, + filter_position:1, + filter_mic_e:1; + u8 unknown12; + u8 unknown13; + u8 unknown14; + u8 unknown15:7, + latitude_sign:1; + u8 latitude_degree; + u8 latitude_minute; + u8 latitude_second; + u8 unknown16:7, + longitude_sign:1; + u8 longitude_degree; + u8 longitude_minute; + u8 longitude_second; + u8 unknown17:4, + selected_position:4; + u8 unknown18:5, + selected_beacon_status_txt:3; + u8 unknown19:4, + beacon_interval:4; + u8 unknowni21:4, + tx_delay:4; + u8 unknown21b:6, + gps_units_altitude_ft:1, + gps_units_position_sss:1; + u8 unknown20:6, + gps_units_speed:2; + u8 unknown21c[4]; + struct { + struct { + char callsign[6]; + u8 ssid; + } entry[8]; + } digi_path_7; + u8 unknown22[18]; + struct { + char padded_string[16]; + } message_macro[7]; + u8 unknown23:5, + selected_msg_group:3; + u8 unknown24; + struct { + char padded_string[9]; + } msg_group[8]; + u8 unknown25; + u8 unknown25a:2, + timezone:6; + u8 unknown25b[2]; + u8 active_smartbeaconing; + struct { + u8 low_speed_mph; + u8 high_speed_mph; + u8 slow_rate_min; + u8 fast_rate_sec; + u8 turn_angle; + u8 turn_slop; + u8 turn_time_sec; + } smartbeaconing_profile[3]; + u8 unknown26:2, + flash_msg:6; + u8 unknown27:2, + flash_grp:6; + u8 unknown28:2, + flash_bln:6; + u8 selected_digi_path; + struct { + struct { + char callsign[6]; + u8 ssid; + } entry[2]; + } digi_path_3_6[4]; + u8 unknown30:6, + selected_my_symbol:2; + u8 unknown31[3]; + u8 unknown32:2, + vibrate_msg:6; + u8 unknown33:2, + vibrate_grp:6; + u8 unknown34:2, + vibrate_bln:6; +} aprs; + +#seekto 0xc26a; +struct { + char padded_string[60]; +} aprs_beacon_status_txt[5]; + +#seekto 0x%04X; +struct { + bbcd date[3]; + bbcd time[2]; + u8 sequence; + u8 unknown1; + u8 unknown2; + char sender_callsign[9]; + u8 data_type; + u8 yeasu_data_type; + u8 unknown4:1, + callsign_is_ascii:1, + unknown5:6; + u8 unknown6; + u16 pkt_len; + u8 unknown7; + u16 in_use; + u16 unknown8; + u16 unknown9; + u16 unknown10; +} aprs_beacon_meta[%d]; + +#seekto 0x%04X; +struct { + char dst_callsign[9]; + char path[30]; + u16 flags; + u8 seperator; + char body[%d]; +} aprs_beacon_pkt[%d]; + +#seekto 0x137c4; +struct { + u8 flag; + char dst_callsign[6]; + u8 dst_callsign_ssid; + char path_and_body[66]; + u8 unknown[70]; +} aprs_message_pkt[60]; +""" + +MEM_BACKTRACK_FORMAT = """ +#seekto 0xdf06; +struct { + u8 status; // 01 full 08 empty + u8 reserved0; // 00 + bbcd year; // 17 + bbcd mon; // 06 + bbcd day; // 01 + u8 reserved1; // 06 + bbcd hour; // 21 + bbcd min; // xx + u8 reserved2; // 00 + u8 reserved3; // 00 + char NShemi[1]; + char lat[3]; + char lat_min[2]; + char lat_dec_sec[4]; + char WEhemi[1]; + char lon[3]; + char lon_min[2]; + char lon_dec_sec[4]; +} backtrack[3]; + +""" +MEM_CHECKSUM_FORMAT = """ +#seekto 0x1FDC9; +u8 checksum; +""" + +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+", "split"] +MODES = ["FM", "AM", "WFM"] +STEPS = list(chirp_common.TUNING_STEPS) +STEPS.remove(30.0) +STEPS.append(100.0) +STEPS.insert(2, 0.0) # There is a skipped tuning step at index 2 (?) +SKIPS = ["", "S", "P"] +FT1_DTMF_CHARS = list("0123456789ABCD*#-") + +CHARSET = ["%i" % int(x) for x in range(0, 10)] + \ + [chr(x) for x in range(ord("A"), ord("Z") + 1)] + \ + [" ", ] + \ + [chr(x) for x in range(ord("a"), ord("z") + 1)] + \ + list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100) + +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("L3", watts=2.50), + chirp_common.PowerLevel("L2", watts=1.00), + chirp_common.PowerLevel("L1", watts=0.05)] + + +class FT1Bank(chirp_common.NamedBank): + """A FT1D bank""" + + def get_name(self): + _bank = self._model._radio._memobj.bank_info[self.index] + + name = "" + for i in _bank.name: + if i == 0xFF: + break + name += CHARSET[i & 0x7F] + return name.rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_info[self.index] + _bank.name = [CHARSET.index(x) for x in name.ljust(16)[:16]] + + +class FT1BankModel(chirp_common.BankModel): + """A FT1D bank model""" + def __init__(self, radio, name='Banks'): + super(FT1BankModel, self).__init__(radio, name) + + _banks = self._radio._memobj.bank_info + self._bank_mappings = [] + for index, _bank in enumerate(_banks): + bank = FT1Bank(self, "%i" % index, "BANK-%i" % index) + bank.index = index + self._bank_mappings.append(bank) + + def get_num_mappings(self): + return len(self._bank_mappings) + + def get_mappings(self): + return self._bank_mappings + + def _channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xFFFF: + return set() + + _members = self._radio._memobj.bank_members[bank.index] + return set([int(ch) + 1 for ch in _members.channel if ch != 0xFFFF]) + + def update_vfo(self): + chosen_bank = [None, None] + chosen_mr = [None, None] + + flags = self._radio._memobj.flag + + # Find a suitable bank and MR for VFO A and B. + for bank in self.get_mappings(): + for channel in self._channel_numbers_in_bank(bank): + chosen_bank[0] = bank.index + chosen_mr[0] = channel + if channel & 0x7000 != 0: + # Ignore preset channels without comment DAR + break + if not flags[channel].nosubvfo: + chosen_bank[1] = bank.index + chosen_mr[1] = channel + break + if chosen_bank[1]: + break + + for vfo_index in (0, 1): + # 3 VFO info structs are stored as 3 pairs of (master, backup) + vfo = self._radio._memobj.vfo_info[vfo_index * 2] + vfo_bak = self._radio._memobj.vfo_info[(vfo_index * 2) + 1] + + if vfo.checksum != vfo_bak.checksum: + LOG.warn("VFO settings are inconsistent with backup") + else: + if ((chosen_bank[vfo_index] is None) and (vfo.bank_index != + 0xFFFF)): + LOG.info("Disabling banks for VFO %d" % vfo_index) + vfo.bank_index = 0xFFFF + vfo.mr_index = 0xFFFF + vfo.bank_enable = 0xFFFF + elif ((chosen_bank[vfo_index] is not None) and + (vfo.bank_index == 0xFFFF)): + LOG.info("Enabling banks for VFO %d" % vfo_index) + vfo.bank_index = chosen_bank[vfo_index] + vfo.mr_index = chosen_mr[vfo_index] + vfo.bank_enable = 0x0000 + vfo_bak.bank_index = vfo.bank_index + vfo_bak.mr_index = vfo.mr_index + vfo_bak.bank_enable = vfo.bank_enable + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.bank_members[bank.index] + if len(channels_in_bank) > len(_members.channel): + raise Exception("Too many entries in bank %d" % bank.index) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channel[index] = channel_number - 1 + if channel_number & 0x7000 != 0: + LOG.warn("Bank %d uses Yaesu preset frequency id=%04X. " + "Chirp cannot see or change that entry." % ( + bank.index, channel_number)) + empty = index + 1 + for index in range(empty, len(_members.channel)): + _members.channel[index] = 0xFFFF + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0x06 + + self.update_vfo() + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + if not channels_in_bank: + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0xFFFF + + self.update_vfo() + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +# Note: other radios like FTM3200Radio subclass this radio +@directory.register +class FT1Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FT1DR""" + BAUD_RATE = 38400 + VENDOR = "Yaesu" + MODEL = "FT-1D" + VARIANT = "R" + + _model = "AH44M" + _memsize = 130507 + _block_lengths = [10, 130497] + _block_size = 32 + _mem_params = (0xe4a, # location of DTMF storage + 900, # size of memories array + 900, # size of flags array + 0xFECA, # APRS beacon metadata address. + 60, # Number of beacons stored. + 0x1064A, # APRS beacon content address. + 134, # Length of beacon data stored. + 60) # Number of beacons stored. + _has_vibrate = False + _has_af_dual = True + + _SG_RE = re.compile(r"(?P[-+NESW]?)(?P[\d]+)[\s\.,]*" + "(?P[\d]*)[\s\']*(?P[\d]*)") + + _RX_BAUD = ("off", "1200 baud", "9600 baud") + _TX_DELAY = ("100ms", "150ms", "200ms", "250ms", "300ms", + "400ms", "500ms", "750ms", "1000ms") + _WIND_UNITS = ("m/s", "mph") + _RAIN_UNITS = ("mm", "inch") + _TEMP_UNITS = ("C", "F") + _ALT_UNITS = ("m", "ft") + _DIST_UNITS = ("km", "mile") + _POS_UNITS = ("dd.mmmm'", "dd mm'ss\"") + _SPEED_UNITS = ("km/h", "knot", "mph") + _TIME_SOURCE = ("manual", "GPS") + _TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30", + "-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30", + "-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30", + "-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30", + "-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30", + "+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30", + "+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30", + "+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30", + "+11:00", "+11:30") + _BEACON_TYPE = ("Off", "Interval", "SmartBeaconing") + _SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3") + _BEACON_INT = ("30s", "1m", "2m", "3m", "5m", "10m", "15m", + "20m", "30m", "60m") + _DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4", + "Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8") + _MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2", + "Message Group 3", "Message Group 4", + "Message Group 5", "Message Group 6", + "Message Group 7", "Message Group 8") + _POSITIONS = ("GPS", "Manual Latitude/Longitude", + "Manual Latitude/Longitude", "P1", "P2", "P3", "P4", + "P5", "P6", "P7", "P8", "P9") + _FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds", + "10 seconds", "20 seconds", "30 seconds", "60 seconds", + "CONTINUOUS", "every 2 seconds", "every 3 seconds", + "every 4 seconds", "every 5 seconds", "every 6 seconds", + "every 7 seconds", "every 8 seconds", "every 9 seconds", + "every 10 seconds", "every 20 seconds", "every 30 seconds", + "every 40 seconds", "every 50 seconds", "every minute", + "every 2 minutes", "every 3 minutes", "every 4 minutes", + "every 5 minutes", "every 6 minutes", "every 7 minutes", + "every 8 minutes", "every 9 minutes", "every 10 minutes") + _BEEP_SELECT = ("Off", "Key+Scan", "Key") + _SQUELCH = ["%d" % x for x in range(0, 16)] + _VOLUME = ["%d" % x for x in range(0, 33)] + _OPENING_MESSAGE = ("Off", "DC", "Message", "Normal") + _SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \ + ["Busy", "Hold"] + _SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \ + ["%.1fs" % (0.5 * x) for x in range(2, 21)] + _LAMP_KEY = ["Key %d sec" % x + for x in range(2, 11)] + ["Continuous", "OFF"] + _LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)] + _LCD_DIMMER = ["Level %d" % x for x in range(1, 7)] + _TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)] + _OFF_ON = ("Off", "On") + _VOL_MODE = ("Normal", "Auto Back") + _DTMF_MODE = ("Manual", "Auto") + _DTMF_SPEED = ("50ms", "100ms") + _DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms") + _MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected") + _BACKTRACK_STATUS = ("Valid", "Invalid") + _NS_HEMI = ("N", "S") + _WE_HEMI = ("W", "E") + _APRS_HIGH_SPEED_MAX = 70 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA terminal. + 3. Press and hold in the [F] key while turning the radio on + ("CLONE" will appear on the display). + 4. After clicking OK, press the [BAND] key to send image.""" + )) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA terminal. + 3. Press and hold in the [F] key while turning the radio on + ("CLONE" will appear on the display). + 4. Press the [Dx] key ("-WAIT-" will appear on the LCD).""")) + return rp + + def process_mmap(self): + mem_format = MEM_SETTINGS_FORMAT + MEM_FORMAT + MEM_APRS_FORMAT + \ + MEM_BACKTRACK_FORMAT + MEM_CHECKSUM_FORMAT + self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(500000, 999900000)] + rf.valid_skips = SKIPS + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 16 + rf.memory_bounds = (1, 900) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_bank_names = True + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return "\n".join([repr(self._memobj.memory[number - 1]), + repr(self._memobj.flag[number - 1])]) + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8), + yaesu_clone.YaesuChecksum(0x06CA, 0x0748), + yaesu_clone.YaesuChecksum(0x074A, 0x07C8), + yaesu_clone.YaesuChecksum(0x07CA, 0x0848), + yaesu_clone.YaesuChecksum(0x0000, 0x1FDC9)] + + @staticmethod + def _add_ff_pad(val, length): + return val.ljust(length, "\xFF")[:length] + + @classmethod + def _strip_ff_pads(cls, messages): + result = [] + for msg_text in messages: + result.append(str(msg_text).rstrip("\xFF")) + return result + + def get_memory(self, number): + flag = self._memobj.flag[number - 1] + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + mem.number = number + if not flag.used: + mem.empty = True + if not flag.valid: + mem.empty = True + return mem + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 1000 + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + self._get_tmode(mem, _mem) + mem.duplex = DUPLEX[_mem.duplex] + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + mem.mode = self._decode_mode(_mem) + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.tuning_step = STEPS[_mem.tune_step] + mem.power = self._decode_power_level(_mem) + mem.skip = flag.pskip and "P" or flag.skip and "S" or "" + + mem.name = self._decode_label(_mem) + + return mem + + def _decode_label(self, mem): + charset = ''.join(CHARSET).ljust(256, '.') + return str(mem.label).rstrip("\xFF").translate(charset) + + def _encode_label(self, mem): + label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()]) + return self._add_ff_pad(label, 16) + + def _encode_charsetbits(self, mem): + # We only speak english here in chirpville + return [0x00, 0x00] + + def _decode_power_level(self, mem): + return POWER_LEVELS[3 - mem.power] + + def _encode_power_level(self, mem): + return 3 - POWER_LEVELS.index(mem.power) + + def _decode_mode(self, mem): + return MODES[mem.mode] + + def _encode_mode(self, mem): + return MODES.index(mem.mode) + + def _get_tmode(self, mem, _mem): + mem.tmode = TMODES[_mem.tone_mode] + + def _set_tmode(self, _mem, mem): + _mem.tone_mode = TMODES.index(mem.tmode) + + def _set_mode(self, _mem, mem): + _mem.mode = self._encode_mode(mem) + + def _debank(self, mem): + bm = self.get_bank_model() + for bank in bm.get_memory_mappings(mem): + bm.remove_memory_from_mapping(mem, bank) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + flag = self._memobj.flag[mem.number - 1] + + self._debank(mem) + + if not mem.empty and not flag.valid: + self._wipe_memory(_mem) + + if mem.empty and flag.valid and not flag.used: + flag.valid = False + return + flag.used = not mem.empty + flag.valid = flag.used + + if mem.empty: + return + + if mem.freq < 30000000 or \ + (mem.freq > 88000000 and mem.freq < 108000000) or \ + mem.freq > 580000000: + flag.nosubvfo = True # Masked from VFO B + else: + flag.nosubvfo = False # Available in both VFOs + + _mem.freq = int(mem.freq / 1000) + _mem.offset = int(mem.offset / 1000) + _mem.tone = chirp_common.TONES.index(mem.rtone) + self._set_tmode(_mem, mem) + _mem.duplex = DUPLEX.index(mem.duplex) + self._set_mode(_mem, mem) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = STEPS.index(mem.tuning_step) + if mem.power: + _mem.power = self._encode_power_level(mem) + else: + _mem.power = 0 + + _mem.label = self._encode_label(mem) + charsetbits = self._encode_charsetbits(mem) + _mem.charsetbits[0], _mem.charsetbits[1] = charsetbits + + flag.skip = mem.skip == "S" + flag.pskip = mem.skip == "P" + + @classmethod + def _wipe_memory(cls, mem): + mem.set_raw("\x00" * (mem.size() / 8)) + mem.unknown1 = 0x05 + + def get_bank_model(self): + return FT1BankModel(self) + + @classmethod + def _digi_path_to_str(cls, path): + path_cmp = [] + for entry in path.entry: + callsign = str(entry.callsign).rstrip("\xFF") + if not callsign: + break + path_cmp.append("%s-%d" % (callsign, entry.ssid)) + return ",".join(path_cmp) + + @staticmethod + def _latlong_sanity(sign, l_d, l_m, l_s, is_lat): + if sign not in (0, 1): + sign = 0 + if is_lat: + d_max = 90 + else: + d_max = 180 + if l_d < 0 or l_d > d_max: + l_d = 0 + l_m = 0 + l_s = 0 + if l_m < 0 or l_m > 60: + l_m = 0 + l_s = 0 + if l_s < 0 or l_s > 60: + l_s = 0 + return sign, l_d, l_m, l_s + + @classmethod + def _latlong_to_str(cls, sign, l_d, l_m, l_s, is_lat, to_sexigesimal=True): + sign, l_d, l_m, l_s = cls._latlong_sanity(sign, l_d, l_m, l_s, is_lat) + mult = sign and -1 or 1 + if to_sexigesimal: + return "%d,%d'%d\"" % (mult * l_d, l_m, l_s) + return "%0.5f" % (mult * l_d + (l_m / 60.0) + (l_s / (60.0 * 60.0))) + + @classmethod + def _str_to_latlong(cls, lat_long, is_lat): + sign = 0 + result = [0, 0, 0] + + lat_long = lat_long.strip() + + if not lat_long: + return 1, 0, 0, 0 + + try: + # DD.MMMMM is the simple case, try that first. + val = float(lat_long) + if val < 0: + sign = 1 + val = abs(val) + result[0] = int(val) + result[1] = int(val * 60) % 60 + result[2] = int(val * 3600) % 60 + except ValueError: + # Try DD MM'SS" if DD.MMMMM failed. + match = cls._SG_RE.match(lat_long.strip()) + if match: + if match.group("sign") and (match.group("sign") in "SE-"): + sign = 1 + else: + sign = 0 + if match.group("d"): + result[0] = int(match.group("d")) + if match.group("m"): + result[1] = int(match.group("m")) + if match.group("s"): + result[2] = int(match.group("s")) + elif len(lat_long) > 4: + raise Exception("Lat/Long should be DD MM'SS\" or DD.MMMMM") + + return cls._latlong_sanity(sign, result[0], result[1], result[2], + is_lat) + + def _get_aprs_general_settings(self): + menu = RadioSettingGroup("aprs_general", "APRS General") + aprs = self._memobj.aprs + + val = RadioSettingValueString( + 0, 6, str(aprs.my_callsign.callsign).rstrip("\xFF")) + rs = RadioSetting("aprs.my_callsign.callsign", "My Callsign", val) + rs.set_apply_callback(self.apply_callsign, aprs.my_callsign) + menu.append(rs) + + val = RadioSettingValueList( + chirp_common.APRS_SSID, + chirp_common.APRS_SSID[aprs.my_callsign.ssid]) + rs = RadioSetting("aprs.my_callsign.ssid", "My SSID", val) + menu.append(rs) + + val = RadioSettingValueList(self._MY_SYMBOL, + self._MY_SYMBOL[aprs.selected_my_symbol]) + rs = RadioSetting("aprs.selected_my_symbol", "My Symbol", val) + menu.append(rs) + + symbols = list(chirp_common.APRS_SYMBOLS) + selected = aprs.custom_symbol + if aprs.custom_symbol >= len(chirp_common.APRS_SYMBOLS): + symbols.append("%d" % aprs.custom_symbol) + selected = len(symbols) - 1 + val = RadioSettingValueList(symbols, symbols[selected]) + rs = RadioSetting("aprs.custom_symbol_text", "User Selected Symbol", + val) + rs.set_apply_callback(self.apply_custom_symbol, aprs) + menu.append(rs) + + val = RadioSettingValueList( + chirp_common.APRS_POSITION_COMMENT, + chirp_common.APRS_POSITION_COMMENT[aprs.selected_position_comment]) + rs = RadioSetting("aprs.selected_position_comment", "Position Comment", + val) + menu.append(rs) + + latitude = self._latlong_to_str(aprs.latitude_sign, + aprs.latitude_degree, + aprs.latitude_minute, + aprs.latitude_second, + True, aprs.aprs_units_position_mmss) + longitude = self._latlong_to_str(aprs.longitude_sign, + aprs.longitude_degree, + aprs.longitude_minute, + aprs.longitude_second, + False, aprs.aprs_units_position_mmss) + + # TODO: Rebuild this when aprs_units_position_mmss changes. + # TODO: Rebuild this when latitude/longitude change. + # TODO: Add saved positions p1 - p10 to memory map. + position_str = list(self._POSITIONS) + # position_str[1] = "%s %s" % (latitude, longitude) + # position_str[2] = "%s %s" % (latitude, longitude) + val = RadioSettingValueList(position_str, + position_str[aprs.selected_position]) + rs = RadioSetting("aprs.selected_position", "My Position", val) + menu.append(rs) + + val = RadioSettingValueString(0, 10, latitude) + rs = RadioSetting("latitude", "Manual Latitude", val) + rs.set_apply_callback(self.apply_lat_long, aprs) + menu.append(rs) + + val = RadioSettingValueString(0, 11, longitude) + rs = RadioSetting("longitude", "Manual Longitude", val) + rs.set_apply_callback(self.apply_lat_long, aprs) + menu.append(rs) + + val = RadioSettingValueList( + self._TIME_SOURCE, self._TIME_SOURCE[aprs.set_time_manually]) + rs = RadioSetting("aprs.set_time_manually", "Time Source", val) + menu.append(rs) + + val = RadioSettingValueList(self._TZ, self._TZ[aprs.timezone]) + rs = RadioSetting("aprs.timezone", "Timezone", val) + menu.append(rs) + + val = RadioSettingValueList(self._SPEED_UNITS, + self._SPEED_UNITS[aprs.aprs_units_speed]) + rs = RadioSetting("aprs.aprs_units_speed", "APRS Speed Units", val) + menu.append(rs) + + val = RadioSettingValueList(self._SPEED_UNITS, + self._SPEED_UNITS[aprs.gps_units_speed]) + rs = RadioSetting("aprs.gps_units_speed", "GPS Speed Units", val) + menu.append(rs) + + val = RadioSettingValueList( + self._ALT_UNITS, self._ALT_UNITS[aprs.aprs_units_altitude_ft]) + rs = RadioSetting("aprs.aprs_units_altitude_ft", "APRS Altitude Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._ALT_UNITS, self._ALT_UNITS[aprs.gps_units_altitude_ft]) + rs = RadioSetting("aprs.gps_units_altitude_ft", "GPS Altitude Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._POS_UNITS, self._POS_UNITS[aprs.aprs_units_position_mmss]) + rs = RadioSetting("aprs.aprs_units_position_mmss", + "APRS Position Format", val) + menu.append(rs) + + val = RadioSettingValueList( + self._POS_UNITS, self._POS_UNITS[aprs.gps_units_position_sss]) + rs = RadioSetting("aprs.gps_units_position_sss", + "GPS Position Format", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DIST_UNITS, self._DIST_UNITS[aprs.aprs_units_distance_m]) + rs = RadioSetting("aprs.aprs_units_distance_m", "APRS Distance Units", + val) + menu.append(rs) + + val = RadioSettingValueList(self._WIND_UNITS, + self._WIND_UNITS[aprs.aprs_units_wind_mph]) + rs = RadioSetting("aprs.aprs_units_wind_mph", "APRS Wind Speed Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._RAIN_UNITS, self._RAIN_UNITS[aprs.aprs_units_rain_inch]) + rs = RadioSetting("aprs.aprs_units_rain_inch", "APRS Rain Units", val) + menu.append(rs) + + val = RadioSettingValueList( + self._TEMP_UNITS, self._TEMP_UNITS[aprs.aprs_units_temperature_f]) + rs = RadioSetting("aprs.aprs_units_temperature_f", + "APRS Temperature Units", val) + menu.append(rs) + + return menu + + def _get_aprs_msgs(self): + menu = RadioSettingGroup("aprs_msg", "APRS Messages") + aprs_msg = self._memobj.aprs_message_pkt + + for index in range(0, 60): + if aprs_msg[index].flag != 255: + astring = \ + str(aprs_msg[index].dst_callsign).partition("\xFF")[0] + + val = RadioSettingValueString( + 0, 9, chirp_common.sanitize_string(astring) + + "-%d" % aprs_msg[index].dst_callsign_ssid) + val.set_mutable(False) + rs = RadioSetting( + "aprs_msg.dst_callsign%d" % index, + "Dst Callsign %d" % index, val) + menu.append(rs) + + astring = \ + str(aprs_msg[index].path_and_body).partition("\xFF")[0] + val = RadioSettingValueString( + 0, 66, chirp_common.sanitize_string(astring)) + val.set_mutable(False) + rs = RadioSetting( + "aprs_msg.path_and_body%d" % index, "Body", val) + menu.append(rs) + + return menu + + def _get_aprs_beacons(self): + menu = RadioSettingGroup("aprs_beacons", "APRS Beacons") + aprs_beacon = self._memobj.aprs_beacon_pkt + aprs_meta = self._memobj.aprs_beacon_meta + + for index in range(0, 60): + # There is probably a more pythonesque way to do this + if int(aprs_meta[index].sender_callsign[0]) != 255: + callsign = str(aprs_meta[index].sender_callsign).rstrip("\xFF") + # LOG.debug("Callsign %s %s" % (callsign, list(callsign))) + val = RadioSettingValueString(0, 9, callsign) + val.set_mutable(False) + rs = RadioSetting( + "aprs_beacon.src_callsign%d" % index, + "SRC Callsign %d" % index, val) + menu.append(rs) + + if int(aprs_beacon[index].dst_callsign[0]) != 255: + val = RadioSettingValueString( + 0, 9, + str(aprs_beacon[index].dst_callsign).rstrip("\xFF")) + val.set_mutable(False) + rs = RadioSetting( + "aprs_beacon.dst_callsign%d" % index, + "DST Callsign %d" % index, val) + menu.append(rs) + + if int(aprs_meta[index].sender_callsign[0]) != 255: + date = "%02d/%02d/%02d" % ( + aprs_meta[index].date[0], + aprs_meta[index].date[1], + aprs_meta[index].date[2]) + val = RadioSettingValueString(0, 8, date) + val.set_mutable(False) + rs = RadioSetting("aprs_beacon.date%d" % index, "Date", val) + menu.append(rs) + + time = "%02d:%02d" % ( + aprs_meta[index].time[0], + aprs_meta[index].time[1]) + val = RadioSettingValueString(0, 5, time) + val.set_mutable(False) + rs = RadioSetting("aprs_beacon.time%d" % index, "Time", val) + menu.append(rs) + + if int(aprs_beacon[index].dst_callsign[0]) != 255: + path = str(aprs_beacon[index].path).replace("\x00", " ") + path = ''.join(c for c in path + if c in string.printable).strip() + path = str(path).replace("\xE0", "*") + # LOG.debug("path %s %s" % (path, list(path))) + val = RadioSettingValueString(0, 32, path) + val.set_mutable(False) + rs = RadioSetting( + "aprs_beacon.path%d" % index, "Digipath", val) + menu.append(rs) + + body = str(aprs_beacon[index].body).rstrip("\xFF") + checksum = body[-2:] + body = ''.join(s for s in body[:-2] + if s in string.printable).translate( + None, "\x09\x0a\x0b\x0c\x0d") + try: + val = RadioSettingValueString(0, 134, body.strip()) + except Exception as e: + LOG.error("Error in APRS beacon at index %s", index) + raise e + val.set_mutable(False) + rs = RadioSetting("aprs_beacon.body%d" % index, "Body", val) + menu.append(rs) + + return menu + + def _get_aprs_rx_settings(self): + menu = RadioSettingGroup("aprs_rx", "APRS Receive") + aprs = self._memobj.aprs + + val = RadioSettingValueList(self._RX_BAUD, self._RX_BAUD[aprs.rx_baud]) + rs = RadioSetting("aprs.rx_baud", "Modem RX", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.aprs_mute) + rs = RadioSetting("aprs.aprs_mute", "APRS Mute", val) + menu.append(rs) + + if self._has_af_dual: + val = RadioSettingValueBoolean(aprs.af_dual) + rs = RadioSetting("aprs.af_dual", "AF Dual", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.ring_msg) + rs = RadioSetting("aprs.ring_msg", "Ring on Message RX", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.ring_beacon) + rs = RadioSetting("aprs.ring_beacon", "Ring on Beacon RX", val) + menu.append(rs) + + val = RadioSettingValueList(self._FLASH, + self._FLASH[aprs.flash_msg]) + rs = RadioSetting("aprs.flash_msg", "Flash on personal message", val) + menu.append(rs) + + if self._has_vibrate: + val = RadioSettingValueList(self._FLASH, + self._FLASH[aprs.vibrate_msg]) + rs = RadioSetting("aprs.vibrate_msg", + "Vibrate on personal message", val) + menu.append(rs) + + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs.flash_bln]) + rs = RadioSetting("aprs.flash_bln", "Flash on bulletin message", val) + menu.append(rs) + + if self._has_vibrate: + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs.vibrate_bln]) + rs = RadioSetting("aprs.vibrate_bln", + "Vibrate on bulletin message", val) + menu.append(rs) + + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs.flash_grp]) + rs = RadioSetting("aprs.flash_grp", "Flash on group message", val) + menu.append(rs) + + if self._has_vibrate: + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs.vibrate_grp]) + rs = RadioSetting("aprs.vibrate_grp", + "Vibrate on group message", val) + menu.append(rs) + + filter_val = [m.padded_string for m in aprs.msg_group] + filter_val = self._strip_ff_pads(filter_val) + for index, filter_text in enumerate(filter_val): + val = RadioSettingValueString(0, 9, filter_text) + rs = RadioSetting("aprs.msg_group_%d" % index, + "Message Group %d" % (index + 1), val) + menu.append(rs) + rs.set_apply_callback(self.apply_ff_padded_string, + aprs.msg_group[index]) + # TODO: Use filter_val as the list entries and update it on edit. + val = RadioSettingValueList( + self._MSG_GROUP_NAMES, + self._MSG_GROUP_NAMES[aprs.selected_msg_group]) + rs = RadioSetting("aprs.selected_msg_group", "Selected Message Group", + val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_mic_e) + rs = RadioSetting("aprs.filter_mic_e", "Receive Mic-E Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_position) + rs = RadioSetting("aprs.filter_position", "Receive Position Beacons", + val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_weather) + rs = RadioSetting("aprs.filter_weather", "Receive Weather Beacons", + val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_object) + rs = RadioSetting("aprs.filter_object", "Receive Object Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_item) + rs = RadioSetting("aprs.filter_item", "Receive Item Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_status) + rs = RadioSetting("aprs.filter_status", "Receive Status Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_other) + rs = RadioSetting("aprs.filter_other", "Receive Other Beacons", val) + menu.append(rs) + + return menu + + def _get_aprs_tx_settings(self): + menu = RadioSettingGroup("aprs_tx", "APRS Transmit") + aprs = self._memobj.aprs + + beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon + val = RadioSettingValueList(self._BEACON_TYPE, + self._BEACON_TYPE[beacon_type]) + rs = RadioSetting("aprs.transmit", "TX Beacons", val) + rs.set_apply_callback(self.apply_beacon_type, aprs) + menu.append(rs) + + val = RadioSettingValueList(self._TX_DELAY, + self._TX_DELAY[aprs.tx_delay]) + rs = RadioSetting("aprs.tx_delay", "TX Delay", val) + menu.append(rs) + + val = RadioSettingValueList(self._BEACON_INT, + self._BEACON_INT[aprs.beacon_interval]) + rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val) + menu.append(rs) + + desc = [] + status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt] + status = self._strip_ff_pads(status) + for index, msg_text in enumerate(status): + val = RadioSettingValueString(0, 60, msg_text) + desc.append("Beacon Status Text %d" % (index + 1)) + rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1], + val) + rs.set_apply_callback(self.apply_ff_padded_string, + self._memobj.aprs_beacon_status_txt[index]) + menu.append(rs) + val = RadioSettingValueList(desc, + desc[aprs.selected_beacon_status_txt]) + rs = RadioSetting("aprs.selected_beacon_status_txt", + "Beacon Status Text", val) + menu.append(rs) + + message_macro = [m.padded_string for m in aprs.message_macro] + message_macro = self._strip_ff_pads(message_macro) + for index, msg_text in enumerate(message_macro): + val = RadioSettingValueString(0, 16, msg_text) + rs = RadioSetting("aprs.message_macro_%d" % index, + "Message Macro %d" % (index + 1), val) + rs.set_apply_callback(self.apply_ff_padded_string, + aprs.message_macro[index]) + menu.append(rs) + + path_str = list(self._DIGI_PATHS) + path_str[3] = self._digi_path_to_str(aprs.digi_path_3_6[0]) + val = RadioSettingValueString(0, 22, path_str[3]) + rs = RadioSetting("aprs.digi_path_3", "Digi Path 4 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[0]) + menu.append(rs) + + path_str[4] = self._digi_path_to_str(aprs.digi_path_3_6[1]) + val = RadioSettingValueString(0, 22, path_str[4]) + rs = RadioSetting("aprs.digi_path_4", "Digi Path 5 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[1]) + menu.append(rs) + + path_str[5] = self._digi_path_to_str(aprs.digi_path_3_6[2]) + val = RadioSettingValueString(0, 22, path_str[5]) + rs = RadioSetting("aprs.digi_path_5", "Digi Path 6 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[2]) + menu.append(rs) + + path_str[6] = self._digi_path_to_str(aprs.digi_path_3_6[3]) + val = RadioSettingValueString(0, 22, path_str[6]) + rs = RadioSetting("aprs.digi_path_6", "Digi Path 7 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[3]) + menu.append(rs) + + path_str[7] = self._digi_path_to_str(aprs.digi_path_7) + val = RadioSettingValueString(0, 88, path_str[7]) + rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7) + menu.append(rs) + + # Show friendly messages for empty slots rather than blanks. + # TODO: Rebuild this when digi_path_[34567] change. + # path_str[3] = path_str[3] or self._DIGI_PATHS[3] + # path_str[4] = path_str[4] or self._DIGI_PATHS[4] + # path_str[5] = path_str[5] or self._DIGI_PATHS[5] + # path_str[6] = path_str[6] or self._DIGI_PATHS[6] + # path_str[7] = path_str[7] or self._DIGI_PATHS[7] + path_str[3] = self._DIGI_PATHS[3] + path_str[4] = self._DIGI_PATHS[4] + path_str[5] = self._DIGI_PATHS[5] + path_str[6] = self._DIGI_PATHS[6] + path_str[7] = self._DIGI_PATHS[7] + val = RadioSettingValueList(path_str, + path_str[aprs.selected_digi_path]) + rs = RadioSetting("aprs.selected_digi_path", "Selected Digi Path", val) + menu.append(rs) + + return menu + + def _get_aprs_smartbeacon(self): + menu = RadioSettingGroup("aprs_smartbeacon", "APRS SmartBeacon") + aprs = self._memobj.aprs + + val = RadioSettingValueList( + self._SMARTBEACON_PROFILE, + self._SMARTBEACON_PROFILE[aprs.active_smartbeaconing]) + rs = RadioSetting("aprs.active_smartbeaconing", "SmartBeacon profile", + val) + menu.append(rs) + + for profile in range(3): + pfx = "type%d" % (profile + 1) + path = "aprs.smartbeaconing_profile[%d]" % profile + prof = aprs.smartbeaconing_profile[profile] + + low_val = RadioSettingValueInteger(2, 30, prof.low_speed_mph) + high_val = RadioSettingValueInteger(3, self._APRS_HIGH_SPEED_MAX, + prof.high_speed_mph) + low_val.get_max = lambda: min(30, int(high_val.get_value()) - 1) + + rs = RadioSetting("%s.low_speed_mph" % path, + "%s Low Speed (mph)" % pfx, low_val) + menu.append(rs) + + rs = RadioSetting("%s.high_speed_mph" % path, + "%s High Speed (mph)" % pfx, high_val) + menu.append(rs) + + val = RadioSettingValueInteger(1, 100, prof.slow_rate_min) + rs = RadioSetting("%s.slow_rate_min" % path, + "%s Slow rate (minutes)" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(10, 180, prof.fast_rate_sec) + rs = RadioSetting("%s.fast_rate_sec" % path, + "%s Fast rate (seconds)" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(5, 90, prof.turn_angle) + rs = RadioSetting("%s.turn_angle" % path, + "%s Turn angle (degrees)" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(1, 255, prof.turn_slop) + rs = RadioSetting("%s.turn_slop" % path, + "%s Turn slop" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(5, 180, prof.turn_time_sec) + rs = RadioSetting("%s.turn_time_sec" % path, + "%s Turn time (seconds)" % pfx, val) + menu.append(rs) + + return menu + + def _get_dtmf_settings(self): + menu = RadioSettingGroup("dtmf_settings", "DTMF") + dtmf = self._memobj.scan_settings + + val = RadioSettingValueList( + self._DTMF_MODE, + self._DTMF_MODE[dtmf.dtmf_mode]) + rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DTMF_SPEED, + self._DTMF_SPEED[dtmf.dtmf_speed]) + rs = RadioSetting( + "scan_settings.dtmf_speed", "DTMF AutoDial Speed", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DTMF_DELAY, + self._DTMF_DELAY[dtmf.dtmf_delay]) + rs = RadioSetting( + "scan_settings.dtmf_delay", "DTMF AutoDial Delay", val) + menu.append(rs) + + for i in range(10): + name = "dtmf_%02d" % i + dtmfsetting = self._memobj.dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c == 0xFF: + break + if c < len(FT1_DTMF_CHARS): + dtmfstr += FT1_DTMF_CHARS[c] + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + dtmfentry.set_charset(FT1_DTMF_CHARS + list("abcd ")) + rs = RadioSetting(name, name.upper(), dtmfentry) + rs.set_apply_callback(self.apply_dtmf, i) + menu.append(rs) + + return menu + + def _get_misc_settings(self): + menu = RadioSettingGroup("misc_settings", "Misc") + scan_settings = self._memobj.scan_settings + + val = RadioSettingValueList( + self._LCD_DIMMER, + self._LCD_DIMMER[scan_settings.lcd_dimmer]) + rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val) + menu.append(rs) + + val = RadioSettingValueList( + self._LCD_CONTRAST, + self._LCD_CONTRAST[scan_settings.lcd_contrast - 1]) + rs = RadioSetting("scan_settings.lcd_contrast", "LCD Contrast", + val) + rs.set_apply_callback(self.apply_lcd_contrast, scan_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._LAMP_KEY, + self._LAMP_KEY[scan_settings.lamp]) + rs = RadioSetting("scan_settings.lamp", "Lamp", val) + menu.append(rs) + + beep_select = self._memobj.beep_select + + val = RadioSettingValueList( + self._BEEP_SELECT, + self._BEEP_SELECT[beep_select.beep]) + rs = RadioSetting("beep_select.beep", "Beep Select", val) + menu.append(rs) + + opening_message = self._memobj.opening_message + + val = RadioSettingValueList( + self._OPENING_MESSAGE, + self._OPENING_MESSAGE[opening_message.flag]) + rs = RadioSetting("opening_message.flag", "Opening Msg Mode", + val) + menu.append(rs) + + rs = self._decode_opening_message(opening_message) + menu.append(rs) + + return menu + + def _decode_opening_message(self, opening_message): + msg = "" + for i in opening_message.message.padded_yaesu: + if i == 0xFF: + break + msg += CHARSET[i & 0x7F] + val = RadioSettingValueString(0, 16, msg) + rs = RadioSetting("opening_message.message.padded_yaesu", + "Opening Message", val) + rs.set_apply_callback(self.apply_ff_padded_yaesu, + opening_message.message) + return rs + + def backtrack_ll_validate(self, number, min, max): + if str(number).lstrip('0').strip().isdigit() and \ + int(str(number).lstrip('0')) <= max and \ + int(str(number).lstrip('0')) >= min: + return True + + return False + + def backtrack_zero_pad(self, number, l): + number = str(number).strip() + while len(number) < l: + number = '0' + number + + return str(number) + + def _get_backtrack_settings(self): + + menu = RadioSettingGroup("backtrack", "Backtrack") + + for i in range(3): + prefix = '' + if i == 0: + prefix = "Star " + if i == 1: + prefix = "L1 " + if i == 2: + prefix = "L2 " + + bt_idx = "backtrack[%d]" % i + + bt = self._memobj.backtrack[i] + + val = RadioSettingValueList( + self._BACKTRACK_STATUS, + self._BACKTRACK_STATUS[0 if bt.status == 1 else 1]) + rs = RadioSetting( + "%s.status" % bt_idx, + prefix + "status", val) + rs.set_apply_callback(self.apply_backtrack_status, bt) + menu.append(rs) + + if bt.status == 1 and int(bt.year) < 100: + val = RadioSettingValueInteger(0, 99, bt.year) + else: + val = RadioSettingValueInteger(0, 99, 0) + rs = RadioSetting( + "%s.year" % bt_idx, + prefix + "year", val) + menu.append(rs) + + if bt.status == 1 and int(bt.mon) <= 12: + val = RadioSettingValueInteger(0, 12, bt.mon) + else: + val = RadioSettingValueInteger(0, 12, 0) + rs = RadioSetting( + "%s.mon" % bt_idx, + prefix + "month", val) + menu.append(rs) + + if bt.status == 1: + val = RadioSettingValueInteger(0, 31, bt.day) + else: + val = RadioSettingValueInteger(0, 31, 0) + rs = RadioSetting( + "%s.day" % bt_idx, + prefix + "day", val) + menu.append(rs) + + if bt.status == 1: + val = RadioSettingValueInteger(0, 23, bt.hour) + else: + val = RadioSettingValueInteger(0, 23, 0) + rs = RadioSetting( + "%s.hour" % bt_idx, + prefix + "hour", val) + menu.append(rs) + + if bt.status == 1: + val = RadioSettingValueInteger(0, 59, bt.min) + else: + val = RadioSettingValueInteger(0, 59, 0) + rs = RadioSetting( + "%s.min" % bt_idx, + prefix + "min", val) + menu.append(rs) + + if bt.status == 1 and \ + (str(bt.NShemi) == 'N' or str(bt.NShemi) == 'S'): + val = RadioSettingValueString(0, 1, str(bt.NShemi)) + else: + val = RadioSettingValueString(0, 1, ' ') + rs = RadioSetting( + "%s.NShemi" % bt_idx, + prefix + "NS hemisphere", val) + rs.set_apply_callback(self.apply_NShemi, bt) + menu.append(rs) + + if bt.status == 1 and self.backtrack_ll_validate(bt.lat, 0, 90): + val = RadioSettingValueString( + 0, 3, self.backtrack_zero_pad(bt.lat, 3)) + else: + val = RadioSettingValueString(0, 3, ' ') + rs = RadioSetting("%s.lat" % bt_idx, prefix + "Latitude", val) + rs.set_apply_callback(self.apply_bt_lat, bt) + menu.append(rs) + + if bt.status == 1 and \ + self.backtrack_ll_validate(bt.lat_min, 0, 59): + val = RadioSettingValueString( + 0, 2, self.backtrack_zero_pad(bt.lat_min, 2)) + else: + val = RadioSettingValueString(0, 2, ' ') + rs = RadioSetting( + "%s.lat_min" % bt_idx, + prefix + "Latitude Minutes", val) + rs.set_apply_callback(self.apply_bt_lat_min, bt) + menu.append(rs) + + if bt.status == 1 and \ + self.backtrack_ll_validate(bt.lat_dec_sec, 0, 9999): + val = RadioSettingValueString( + 0, 4, self.backtrack_zero_pad(bt.lat_dec_sec, 4)) + else: + val = RadioSettingValueString(0, 4, ' ') + rs = RadioSetting( + "%s.lat_dec_sec" % bt_idx, + prefix + "Latitude Decimal Seconds", val) + rs.set_apply_callback(self.apply_bt_lat_dec_sec, bt) + menu.append(rs) + + if bt.status == 1 and \ + (str(bt.WEhemi) == 'W' or str(bt.WEhemi) == 'E'): + val = RadioSettingValueString( + 0, 1, str(bt.WEhemi)) + else: + val = RadioSettingValueString(0, 1, ' ') + rs = RadioSetting( + "%s.WEhemi" % bt_idx, + prefix + "WE hemisphere", val) + rs.set_apply_callback(self.apply_WEhemi, bt) + menu.append(rs) + + if bt.status == 1 and self.backtrack_ll_validate(bt.lon, 0, 180): + val = RadioSettingValueString( + 0, 3, self.backtrack_zero_pad(bt.lon, 3)) + else: + val = RadioSettingValueString(0, 3, ' ') + rs = RadioSetting("%s.lon" % bt_idx, prefix + "Longitude", val) + rs.set_apply_callback(self.apply_bt_lon, bt) + menu.append(rs) + + if bt.status == 1 and \ + self.backtrack_ll_validate(bt.lon_min, 0, 59): + val = RadioSettingValueString( + 0, 2, self.backtrack_zero_pad(bt.lon_min, 2)) + else: + val = RadioSettingValueString(0, 2, ' ') + rs = RadioSetting( + "%s.lon_min" % bt_idx, + prefix + "Longitude Minutes", val) + rs.set_apply_callback(self.apply_bt_lon_min, bt) + menu.append(rs) + + if bt.status == 1 and \ + self.backtrack_ll_validate(bt.lon_dec_sec, 0, 9999): + val = RadioSettingValueString( + 0, 4, self.backtrack_zero_pad(bt.lon_dec_sec, 4)) + else: + val = RadioSettingValueString(0, 4, ' ') + rs = RadioSetting( + "%s.lon_dec_sec" % bt_idx, + prefix + "Longitude Decimal Seconds", val) + rs.set_apply_callback(self.apply_bt_lon_dec_sec, bt) + menu.append(rs) + + return menu + + def _get_scan_settings(self): + menu = RadioSettingGroup("scan_settings", "Scan") + scan_settings = self._memobj.scan_settings + + val = RadioSettingValueList( + self._VOL_MODE, + self._VOL_MODE[scan_settings.vol_mode]) + rs = RadioSetting("scan_settings.vol_mode", "Volume Mode", val) + menu.append(rs) + + vfoa = self._memobj.vfo_info[0] + val = RadioSettingValueList( + self._VOLUME, + self._VOLUME[vfoa.volume]) + rs = RadioSetting("vfo_info[0].volume", "VFO A Volume", val) + rs.set_apply_callback(self.apply_volume, 0) + menu.append(rs) + + vfob = self._memobj.vfo_info[1] + val = RadioSettingValueList( + self._VOLUME, + self._VOLUME[vfob.volume]) + rs = RadioSetting("vfo_info[1].volume", "VFO B Volume", val) + rs.set_apply_callback(self.apply_volume, 1) + menu.append(rs) + + squelch = self._memobj.squelch + val = RadioSettingValueList( + self._SQUELCH, + self._SQUELCH[squelch.vfo_a]) + rs = RadioSetting("squelch.vfo_a", "VFO A Squelch", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SQUELCH, + self._SQUELCH[squelch.vfo_b]) + rs = RadioSetting("squelch.vfo_b", "VFO B Squelch", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SCAN_RESTART, + self._SCAN_RESTART[scan_settings.scan_restart]) + rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SCAN_RESUME, + self._SCAN_RESUME[scan_settings.scan_resume]) + rs = RadioSetting("scan_settings.scan_resume", "Scan Resume", val) + menu.append(rs) + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.busy_led]) + rs = RadioSetting("scan_settings.busy_led", "Busy LED", val) + menu.append(rs) + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.scan_lamp]) + rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val) + menu.append(rs) + + val = RadioSettingValueList( + self._TOT_TIME, + self._TOT_TIME[scan_settings.tot]) + rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val) + menu.append(rs) + + return menu + + def _get_settings(self): + top = RadioSettings(self._get_aprs_general_settings(), + self._get_aprs_rx_settings(), + self._get_aprs_tx_settings(), + self._get_aprs_smartbeacon(), + self._get_aprs_msgs(), + self._get_aprs_beacons(), + self._get_dtmf_settings(), + self._get_misc_settings(), + self._get_scan_settings(), + self._get_backtrack_settings()) + return top + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + @staticmethod + def apply_custom_symbol(setting, obj): + # Ensure new value falls within known bounds, otherwise leave it as + # it's a custom value from the radio that's outside our list. + if setting.value.get_value() in chirp_common.APRS_SYMBOLS: + setattr(obj, "custom_symbol", + chirp_common.APRS_SYMBOLS.index(setting.value.get_value())) + + @classmethod + def _apply_callsign(cls, callsign, obj, default_ssid=None): + ssid = default_ssid + dash_index = callsign.find("-") + if dash_index >= 0: + ssid = callsign[dash_index + 1:] + callsign = callsign[:dash_index] + try: + ssid = int(ssid) % 16 + except ValueError: + ssid = default_ssid + setattr(obj, "callsign", cls._add_ff_pad(callsign, 6)) + if ssid is not None: + setattr(obj, "ssid", ssid) + + def apply_beacon_type(cls, setting, obj): + beacon_type = str(setting.value.get_value()) + beacon_index = cls._BEACON_TYPE.index(beacon_type) + tx_smartbeacon = beacon_index >> 1 + tx_interval_beacon = beacon_index & 1 + if tx_interval_beacon: + setattr(obj, "tx_interval_beacon", 1) + setattr(obj, "tx_smartbeacon", 0) + elif tx_smartbeacon: + setattr(obj, "tx_interval_beacon", 0) + setattr(obj, "tx_smartbeacon", 1) + else: + setattr(obj, "tx_interval_beacon", 0) + setattr(obj, "tx_smartbeacon", 0) + + @classmethod + def apply_callsign(cls, setting, obj, default_ssid=None): + # Uppercase, strip SSID then FF pad to max string length. + callsign = setting.value.get_value().upper() + cls._apply_callsign(callsign, obj, default_ssid) + + def apply_digi_path(self, setting, obj): + # Parse and map to aprs.digi_path_4_7[0-3] or aprs.digi_path_8 + # and FF terminate. + path = str(setting.value.get_value()) + callsigns = [c.strip() for c in path.split(",")] + for index in range(len(obj.entry)): + try: + self._apply_callsign(callsigns[index], obj.entry[index], 0) + except IndexError: + self._apply_callsign("", obj.entry[index], 0) + if len(callsigns) > len(obj.entry): + raise Exception("This path only supports %d entries" % (index + 1)) + + @classmethod + def apply_ff_padded_string(cls, setting, obj): + # FF pad. + val = setting.value.get_value() + max_len = getattr(obj, "padded_string").size() / 8 + val = str(val).rstrip() + setattr(obj, "padded_string", cls._add_ff_pad(val, max_len)) + + @classmethod + def apply_lat_long(cls, setting, obj): + name = setting.get_name() + is_latitude = name.endswith("latitude") + lat_long = setting.value.get_value().strip() + sign, l_d, l_m, l_s = cls._str_to_latlong(lat_long, is_latitude) + LOG.debug("%s: %d %d %d %d" % (name, sign, l_d, l_m, l_s)) + setattr(obj, "%s_sign" % name, sign) + setattr(obj, "%s_degree" % name, l_d) + setattr(obj, "%s_minute" % name, l_m) + setattr(obj, "%s_second" % name, l_s) + + def set_settings(self, settings): + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + if element.has_apply_callback(): + LOG.debug("Using apply callback") + try: + element.run_apply_callback() + except NotImplementedError as e: + LOG.error("ft1d.set_settings: %s", e) + continue + + # Find the object containing setting. + obj = _mem + bits = element.get_name().split(".") + setting = bits[-1] + for name in bits[:-1]: + if name.endswith("]"): + name, index = name.split("[") + index = int(index[:-1]) + obj = getattr(obj, name)[index] + else: + obj = getattr(obj, name) + + try: + old_val = getattr(obj, setting) + LOG.debug("Setting %s(%r) <= %s" % ( + element.get_name(), old_val, element.value)) + setattr(obj, setting, element.value) + except AttributeError as e: + LOG.error("Setting %s is not in the memory map: %s" % + (element.get_name(), e)) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def apply_ff_padded_yaesu(cls, setting, obj): + # FF pad yaesus custom string format. + rawval = setting.value.get_value() + max_len = getattr(obj, "padded_yaesu").size() / 8 + rawval = str(rawval).rstrip() + val = [CHARSET.index(x) for x in rawval] + for x in range(len(val), max_len): + val.append(0xFF) + obj.padded_yaesu = val + + def apply_volume(cls, setting, vfo): + val = setting.value.get_value() + cls._memobj.vfo_info[(vfo * 2)].volume = val + cls._memobj.vfo_info[(vfo * 2) + 1].volume = val + + def apply_lcd_contrast(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._LCD_CONTRAST.index(rawval) + 1 + obj.lcd_contrast = val + + def apply_dtmf(cls, setting, i): + rawval = setting.value.get_value().upper().rstrip() + val = [FT1_DTMF_CHARS.index(x) for x in rawval] + for x in range(len(val), 16): + val.append(0xFF) + cls._memobj.dtmf[i].memory = val + + def apply_backtrack_status(cls, setting, obj): + status = setting.value.get_value() + + if status == 'Valid': + val = 1 + else: + val = 8 + setattr(obj, "status", val) + + def apply_NShemi(cls, setting, obj): + hemi = setting.value.get_value().upper() + + if hemi != 'N' and hemi != 'S': + hemi = ' ' + setattr(obj, "NShemi", hemi) + + def apply_WEhemi(cls, setting, obj): + hemi = setting.value.get_value().upper() + + if hemi != 'W' and hemi != 'E': + hemi = ' ' + setattr(obj, "WEhemi", hemi) + + def apply_WEhemi(cls, setting, obj): + hemi = setting.value.get_value().upper() + + if hemi != 'W' and hemi != 'E': + hemi = ' ' + setattr(obj, "WEhemi", hemi) + + def apply_bt_lat(cls, setting, obj): + val = setting.value.get_value() + val = cls.backtrack_zero_pad(val, 3) + + setattr(obj, "lat", val) + + def apply_bt_lat_min(cls, setting, obj): + val = setting.value.get_value() + val = cls.backtrack_zero_pad(val, 2) + + setattr(obj, "lat_min", val) + + def apply_bt_lat_dec_sec(cls, setting, obj): + val = setting.value.get_value() + val = cls.backtrack_zero_pad(val, 4) + + setattr(obj, "lat_dec_sec", val) + + def apply_bt_lon(cls, setting, obj): + val = setting.value.get_value() + val = cls.backtrack_zero_pad(val, 3) + + setattr(obj, "lon", val) + + def apply_bt_lon_min(cls, setting, obj): + val = setting.value.get_value() + val = cls.backtrack_zero_pad(val, 2) + + setattr(obj, "lon_min", val) + + def apply_bt_lon_dec_sec(cls, setting, obj): + val = setting.value.get_value() + val = cls.backtrack_zero_pad(val, 4) + + setattr(obj, "lon_dec_sec", val) diff --git a/chirp/drivers/ft2800.py b/chirp/drivers/ft2800.py new file mode 100644 index 0000000..9030e96 --- /dev/null +++ b/chirp/drivers/ft2800.py @@ -0,0 +1,281 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import logging + +from chirp import util, memmap, chirp_common, bitwise, directory, errors +from yaesu_clone import YaesuCloneModeRadio + +LOG = logging.getLogger(__name__) + +CHUNK_SIZE = 16 + + +def _send(s, data): + for i in range(0, len(data), CHUNK_SIZE): + chunk = data[i:i+CHUNK_SIZE] + s.write(chunk) + echo = s.read(len(chunk)) + if chunk != echo: + raise Exception("Failed to read echo chunk") + +IDBLOCK = "\x0c\x01\x41\x33\x35\x02\x00\xb8" +TRAILER = "\x0c\x02\x41\x33\x35\x00\x00\xb7" +ACK = "\x0C\x06\x00" + + +def _download(radio): + data = "" + for _i in range(0, 10): + data = radio.pipe.read(8) + if data == IDBLOCK: + break + + LOG.debug("Header:\n%s" % util.hexprint(data)) + + if len(data) != 8: + raise Exception("Failed to read header") + + _send(radio.pipe, ACK) + + data = "" + + while len(data) < radio._block_sizes[1]: + time.sleep(0.1) + chunk = radio.pipe.read(38) + LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk))) + if len(chunk) == 8: + LOG.debug("END?") + elif len(chunk) != 38: + LOG.debug("Should fail?") + break + # raise Exception("Failed to get full data block") + else: + cs = 0 + for byte in chunk[:-1]: + cs += ord(byte) + if ord(chunk[-1]) != (cs & 0xFF): + raise Exception("Block failed checksum!") + + data += chunk[5:-1] + + _send(radio.pipe, ACK) + if radio.status_fn: + status = chirp_common.Status() + status.max = radio._block_sizes[1] + status.cur = len(data) + status.msg = "Cloning from radio" + radio.status_fn(status) + + LOG.debug("Total: %i" % len(data)) + + return memmap.MemoryMap(data) + + +def _upload(radio): + for _i in range(0, 10): + data = radio.pipe.read(256) + if not data: + break + LOG.debug("What is this garbage?\n%s" % util.hexprint(data)) + + _send(radio.pipe, IDBLOCK) + time.sleep(1) + ack = radio.pipe.read(300) + LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack))) + if ack != ACK: + raise Exception("Radio did not ack ID") + + block = 0 + while block < (radio.get_memsize() / 32): + data = "\x0C\x03\x00\x00" + chr(block) + data += radio.get_mmap()[block*32:(block+1)*32] + cs = 0 + for byte in data: + cs += ord(byte) + data += chr(cs & 0xFF) + + LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data))) + + _send(radio.pipe, data) + time.sleep(0.1) + ack = radio.pipe.read(3) + if ack != ACK: + raise Exception("Radio did not ack block %i" % block) + + if radio.status_fn: + status = chirp_common.Status() + status.max = radio._block_sizes[1] + status.cur = block * 32 + status.msg = "Cloning to radio" + radio.status_fn(status) + block += 1 + + _send(radio.pipe, TRAILER) + +MEM_FORMAT = """ +struct { + bbcd freq[4]; + u8 unknown1[4]; + bbcd offset[2]; + u8 unknown2[2]; + u8 pskip:1, + skip:1, + unknown3:1, + isnarrow:1, + power:2, + duplex:2; + u8 unknown4:6, + tmode:2; + u8 tone; + u8 dtcs; +} memory[200]; + +#seekto 0x0E00; +struct { + char name[6]; +} names[200]; +""" + +MODES = ["FM", "NFM"] +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+", ""] +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=65), + chirp_common.PowerLevel("Mid", watts=25), + chirp_common.PowerLevel("Low2", watts=10), + chirp_common.PowerLevel("Low1", watts=5), + ] +CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + "()+-=*/???|_" + + +@directory.register +class FT2800Radio(YaesuCloneModeRadio): + """Yaesu FT-2800""" + VENDOR = "Yaesu" + MODEL = "FT-2800M" + + _block_sizes = [8, 7680] + _memsize = 7680 + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.memory_bounds = (0, 199) + + rf.has_ctone = False + rf.has_tuning_step = False + rf.has_dtcs_polarity = False + rf.has_bank = False + + rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0, + 20.0, 25.0, 50.0, 100.0] + rf.valid_modes = MODES + rf.valid_tmodes = TMODES + rf.valid_bands = [(137000000, 174000000)] + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = DUPLEX + rf.valid_skips = ["", "S", "P"] + rf.valid_name_length = 6 + rf.valid_characters = CHARSET + + return rf + + def sync_in(self): + self.pipe.parity = "E" + start = time.time() + try: + self._mmap = _download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Downloaded in %.2f sec" % (time.time() - start)) + self.process_mmap() + + def sync_out(self): + self.pipe.timeout = 1 + self.pipe.parity = "E" + start = time.time() + try: + _upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Uploaded in %.2f sec" % (time.time() - start)) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + mem = chirp_common.Memory() + + mem.number = number + + if _mem.get_raw()[0] == "\xFF": + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 10 + mem.offset = int(_mem.offset) * 100000 + mem.duplex = DUPLEX[_mem.duplex] + mem.tmode = TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.name = str(_nam.name).rstrip() + mem.mode = _mem.isnarrow and "NFM" or "FM" + mem.skip = _mem.pskip and "P" or _mem.skip and "S" or "" + mem.power = POWER_LEVELS[_mem.power] + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _nam = self._memobj.names[mem.number] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + if _mem.get_raw()[0] == "\xFF": + # Emtpy -> Non-empty, so initialize + _mem.set_raw("\x00" * (_mem.size() / 8)) + + _mem.freq = mem.freq / 10 + _mem.offset = mem.offset / 100000 + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.tmode = TMODES.index(mem.tmode) + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.isnarrow = MODES.index(mem.mode) + _mem.pskip = mem.skip == "P" + _mem.skip = mem.skip == "S" + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + _nam.name = mem.name.ljust(6)[:6] + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize diff --git a/chirp/drivers/ft2900.py b/chirp/drivers/ft2900.py new file mode 100644 index 0000000..a7ad33d --- /dev/null +++ b/chirp/drivers/ft2900.py @@ -0,0 +1,1256 @@ +# Copyright 2011 Dan Smith +# +# FT-2900-specific modifications by Richard Cochran, +# Initial work on settings by Chris Fosnight, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import logging + +from chirp import util, memmap, chirp_common, bitwise, directory, errors +from chirp.drivers.yaesu_clone import YaesuCloneModeRadio +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList, RadioSettingValueString, RadioSettings + +from textwrap import dedent + +LOG = logging.getLogger(__name__) + + +def _send(s, data): + s.write(data) + echo = s.read(len(data)) + if data != echo: + raise Exception("Failed to read echo") + LOG.debug("got echo\n%s\n" % util.hexprint(echo)) + +ACK = "\x06" +INITIAL_CHECKSUM = 0 + + +def _download(radio): + + blankChunk = "" + for _i in range(0, 32): + blankChunk += "\xff" + + LOG.debug("in _download\n") + + data = "" + for _i in range(0, 20): + data = radio.pipe.read(20) + LOG.debug("Header:\n%s" % util.hexprint(data)) + LOG.debug("len(header) = %s\n" % len(data)) + + if data == radio.IDBLOCK: + break + + if data != radio.IDBLOCK: + raise Exception("Failed to read header") + + _send(radio.pipe, ACK) + + # initialize data, the big var that holds all memory + data = "" + + _blockNum = 0 + + while len(data) < radio._block_sizes[1]: + _blockNum += 1 + time.sleep(0.03) + chunk = radio.pipe.read(32) + LOG.debug("Block %i " % (_blockNum)) + if chunk == blankChunk: + LOG.debug("blank chunk\n") + else: + LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk))) + if len(chunk) != 32: + LOG.debug("len chunk is %i\n" % (len(chunk))) + raise Exception("Failed to get full data block") + break + else: + data += chunk + + if radio.status_fn: + status = chirp_common.Status() + status.max = radio._block_sizes[1] + status.cur = len(data) + status.msg = "Cloning from radio" + radio.status_fn(status) + + LOG.debug("Total: %i" % len(data)) + + # radio should send us one final termination byte, containing + # checksum + chunk = radio.pipe.read(32) + if len(chunk) != 1: + LOG.debug("len(chunk) is %i\n" % len(chunk)) + raise Exception("radio sent extra unknown data") + LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk))) + + # compute checksum + cs = INITIAL_CHECKSUM + for byte in radio.IDBLOCK: + cs += ord(byte) + for byte in data: + cs += ord(byte) + LOG.debug("calculated checksum is %x\n" % (cs & 0xff)) + LOG.debug("Radio sent checksum is %x\n" % ord(chunk[0])) + + if (cs & 0xff) != ord(chunk[0]): + raise Exception("Failed checksum on read.") + + # for debugging purposes, dump the channels, in hex. + for _i in range(0, 200): + _startData = 1892 + 20 * _i + chunk = data[_startData:_startData + 20] + LOG.debug("channel %i:\n%s" % (_i, util.hexprint(chunk))) + + return memmap.MemoryMap(data) + + +def _upload(radio): + for _i in range(0, 10): + data = radio.pipe.read(256) + if not data: + break + LOG.debug("What is this garbage?\n%s" % util.hexprint(data)) + raise Exception("Radio sent unrecognized data") + + _send(radio.pipe, radio.IDBLOCK) + time.sleep(.2) + ack = radio.pipe.read(300) + LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack))) + if ack != ACK: + raise Exception("Radio did not ack ID. Check cable, verify" + " radio is not locked.\n" + " (press & Hold red \"*L\" button to unlock" + " radio if needed)") + + block = 0 + cs = INITIAL_CHECKSUM + for byte in radio.IDBLOCK: + cs += ord(byte) + + while block < (radio.get_memsize() / 32): + data = radio.get_mmap()[block * 32:(block + 1) * 32] + + LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data))) + + _send(radio.pipe, data) + time.sleep(0.03) + + for byte in data: + cs += ord(byte) + + if radio.status_fn: + status = chirp_common.Status() + status.max = radio._block_sizes[1] + status.cur = block * 32 + status.msg = "Cloning to radio" + radio.status_fn(status) + block += 1 + + _send(radio.pipe, chr(cs & 0xFF)) + +MEM_FORMAT = """ +#seekto 0x0080; +struct { + u8 apo; + u8 arts_beep; + u8 bell; + u8 dimmer; + u8 cw_id_string[16]; + u8 cw_trng; + u8 x95; + u8 x96; + u8 x97; + u8 int_cd; + u8 int_set; + u8 x9A; + u8 x9B; + u8 lock; + u8 x9D; + u8 mic_gain; + u8 open_msg; + u8 openMsg_Text[6]; + u8 rf_sql; + u8 unk:6, + pag_abk:1, + unk:1; + u8 pag_cdr_1; + u8 pag_cdr_2; + u8 pag_cdt_1; + u8 pag_cdt_2; + u8 prog_p1; + u8 xAD; + u8 prog_p2; + u8 xAF; + u8 prog_p3; + u8 xB1; + u8 prog_p4; + u8 xB3; + u8 resume; + u8 tot; + u8 unk:1, + cw_id:1, + unk:1, + ts_speed:1, + ars:1, + unk:2, + dtmf_mode:1; + u8 unk:1, + ts_mut:1 + wires_auto:1, + busy_lockout:1, + edge_beep:1, + unk:3; + u8 unk:2, + s_search:1, + unk:2, + cw_trng_units:1, + unk:2; + u8 dtmf_speed:1, + unk:2, + arts_interval:1, + unk:1, + inverted_dcs:1, + unk:1, + mw_mode:1; + u8 unk:2, + wires_mode:1, + wx_alert:1, + unk:1, + wx_vol_max:1, + revert:1, + unk:1; + u8 vfo_scan; + u8 scan_mode; + u8 dtmf_delay; + u8 beep; + u8 xBF; +} settings; + +#seekto 0x00d0; + u8 passwd[4]; + u8 mbs; + +#seekto 0x00c0; +struct { + u16 in_use; +} bank_used[8]; + +#seekto 0x00ef; + u8 currentTone; + +#seekto 0x00f0; + u8 curChannelMem[20]; + +#seekto 0x1e0; +struct { + u8 dtmf_string[16]; +} dtmf_strings[10]; + +#seekto 0x0127; + u8 curChannelNum; + +#seekto 0x012a; + u8 banksoff1; + +#seekto 0x15f; + u8 checksum1; + +#seekto 0x16f; + u8 curentTone2; + +#seekto 0x1aa; + u16 banksoff2; + +#seekto 0x1df; + u8 checksum2; + +#seekto 0x0360; +struct{ + u8 name[6]; +} bank_names[8]; + + +#seekto 0x03c4; +struct{ + u16 channels[50]; +} banks[8]; + +#seekto 0x06e4; +struct { + u8 even_pskip:1, + even_skip:1, + even_valid:1, + even_masked:1, + odd_pskip:1, + odd_skip:1, + odd_valid:1, + odd_masked:1; +} flags[225]; + +#seekto 0x0764; +struct { + u8 unknown0:2, + isnarrow:1, + unknown1:5; + u8 unknown2:2, + duplex:2, + unknown3:1, + step:3; + bbcd freq[3]; + u8 power:2, + unknown4:3, + tmode:3; + u8 name[6]; + bbcd offset[3]; + u8 ctonesplitflag:1, + ctone:7; + u8 rx_dtcssplitflag:1, + rx_dtcs:7; + u8 unknown5; + u8 rtonesplitflag:1, + rtone:7; + u8 dtcssplitflag:1, + dtcs:7; +} memory[200]; + +""" + +MODES = ["FM", "NFM"] +TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"] +CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone", + "Tone->Tone", "DTCS->DTCS"] +DUPLEX = ["", "-", "+", "split"] +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=75), + chirp_common.PowerLevel("Low3", watts=30), + chirp_common.PowerLevel("Low2", watts=10), + chirp_common.PowerLevel("Low1", watts=5), + ] + +CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?C[] _" +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] + + +def _decode_tone(radiotone): + try: + chirptone = chirp_common.TONES[radiotone] + except IndexError: + chirptone = 100 + LOG.debug("found invalid radio tone: %i\n" % radiotone) + return chirptone + + +def _decode_dtcs(radiodtcs): + try: + chirpdtcs = chirp_common.DTCS_CODES[radiodtcs] + except IndexError: + chirpdtcs = 23 + LOG.debug("found invalid radio dtcs code: %i\n" % radiodtcs) + return chirpdtcs + + +def _decode_name(mem): + name = "" + for i in mem: + if (i & 0x7F) == 0x7F: + break + try: + name += CHARSET[i & 0x7F] + except IndexError: + LOG.debug("Unknown char index: %x " % (i)) + name = name.strip() + return name + + +def _encode_name(mem): + if(mem.strip() == ""): + return [0xff] * 6 + + name = [None] * 6 + for i in range(0, 6): + try: + name[i] = CHARSET.index(mem[i]) + except IndexError: + name[i] = CHARSET.index(" ") + + name[0] = name[0] | 0x80 + return name + + +def _wipe_memory(mem): + mem.set_raw("\xff" * (mem.size() / 8)) + + +class FT2900Bank(chirp_common.NamedBank): + + def get_name(self): + _bank = self._model._radio._memobj.bank_names[self.index] + name = "" + for i in _bank.name: + if i == 0xff: + break + name += CHARSET[i & 0x7f] + + return name.rstrip() + + def set_name(self, name): + name = name.upper().ljust(6)[:6] + _bank = self._model._radio._memobj.bank_names[self.index] + _bank.name = [CHARSET.index(x) for x in name.ljust(6)[:6]] + + +class FT2900BankModel(chirp_common.BankModel): + + def get_num_mappings(self): + return 8 + + def get_mappings(self): + banks = self._radio._memobj.banks + bank_mappings = [] + for index, _bank in enumerate(banks): + bank = FT2900Bank(self, "%i" % index, "b%i" % (index + 1)) + bank.index = index + bank_mappings.append(bank) + + return bank_mappings + + def _get_channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xffff: + return set() + + _members = self._radio._memobj.banks[bank.index] + return set([int(ch) for ch in _members.channels if ch != 0xffff]) + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.banks[bank.index] + if len(channels_in_bank) > len(_members.channels): + raise Exception("More than %i entries in bank %d" % + (len(_members.channels), bank.index)) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channels[index] = channel_number + empty = index + 1 + for index in range(empty, len(_members.channels)): + _members.channels[index] = 0xffff + + _bank_used = self._radio._memobj.bank_used[bank.index] + if empty == 0: + _bank_used.in_use = 0xffff + else: + _bank_used.in_use = empty - 1 + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + # tells radio that banks are active + self._radio._memobj.banksoff1 = bank.index + self._radio._memobj.banksoff2 = bank.index + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._get_channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._get_channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +@directory.register +class FT2900Radio(YaesuCloneModeRadio): + + """Yaesu FT-2900""" + VENDOR = "Yaesu" + MODEL = "FT-2900R/1900R" + IDBLOCK = "\x56\x43\x32\x33\x00\x02\x46\x01\x01\x01" + BAUD_RATE = 19200 + + _memsize = 8000 + _block_sizes = [8, 8000] + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.memory_bounds = (0, 199) + + rf.can_odd_split = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = True + rf.has_dtcs_polarity = False + rf.has_bank = True + rf.has_bank_names = True + rf.has_settings = True + + rf.valid_tuning_steps = STEPS + rf.valid_modes = MODES + rf.valid_tmodes = TMODES + rf.valid_cross_modes = CROSS_MODES + rf.valid_bands = [(136000000, 174000000)] + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = DUPLEX + rf.valid_skips = ["", "S", "P"] + rf.valid_name_length = 6 + rf.valid_characters = CHARSET + + return rf + + def sync_in(self): + start = time.time() + try: + self._mmap = _download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Downloaded in %.2f sec" % (time.time() - start)) + self.process_mmap() + + def sync_out(self): + self.pipe.timeout = 1 + start = time.time() + try: + _upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Uploaded in %.2f sec" % (time.time() - start)) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[(number) / 2] + + nibble = ((number) % 2) and "even" or "odd" + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + pskip = _flag["%s_pskip" % nibble] + skip = _flag["%s_skip" % nibble] + + mem = chirp_common.Memory() + + mem.number = number + + if _mem.get_raw()[0] == "\xFF" or not valid or not used: + mem.empty = True + return mem + + mem.tuning_step = STEPS[_mem.step] + mem.freq = int(_mem.freq) * 1000 + + # compensate for 12.5 kHz tuning steps, add 500 Hz if needed + if(mem.tuning_step == 12.5): + lastdigit = int(_mem.freq) % 10 + if (lastdigit == 2 or lastdigit == 7): + mem.freq += 500 + + mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000) + mem.duplex = DUPLEX[_mem.duplex] + if _mem.tmode < TMODES.index("Cross"): + mem.tmode = TMODES[_mem.tmode] + mem.cross_mode = CROSS_MODES[0] + else: + mem.tmode = "Cross" + mem.cross_mode = CROSS_MODES[_mem.tmode - TMODES.index("Cross")] + + mem.rtone = _decode_tone(_mem.rtone) + mem.ctone = _decode_tone(_mem.ctone) + + # check for unequal ctone/rtone in TSQL mode. map it as a + # cross tone mode + if mem.rtone != mem.ctone and (mem.tmode == "TSQL" or + mem.tmode == "Tone"): + mem.tmode = "Cross" + mem.cross_mode = "Tone->Tone" + + mem.dtcs = _decode_dtcs(_mem.dtcs) + mem.rx_dtcs = _decode_dtcs(_mem.rx_dtcs) + + # check for unequal dtcs/rx_dtcs in DTCS mode. map it as a + # cross tone mode + if mem.dtcs != mem.rx_dtcs and mem.tmode == "DTCS": + mem.tmode = "Cross" + mem.cross_mode = "DTCS->DTCS" + + if (int(_mem.name[0]) & 0x80) != 0: + mem.name = _decode_name(_mem.name) + + mem.mode = _mem.isnarrow and "NFM" or "FM" + mem.skip = pskip and "P" or skip and "S" or "" + mem.power = POWER_LEVELS[3 - _mem.power] + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _flag = self._memobj.flags[(mem.number) / 2] + + nibble = ((mem.number) % 2) and "even" or "odd" + + valid = _flag["%s_valid" % nibble] + used = _flag["%s_masked" % nibble] + + if not valid: + _wipe_memory(_mem) + + if mem.empty and valid and not used: + _flag["%s_valid" % nibble] = False + return + + _flag["%s_masked" % nibble] = not mem.empty + + if mem.empty: + return + + _flag["%s_valid" % nibble] = True + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 1000 + _mem.duplex = DUPLEX.index(mem.duplex) + + # clear all the split tone flags -- we'll set them as needed below + _mem.ctonesplitflag = 0 + _mem.rx_dtcssplitflag = 0 + _mem.rtonesplitflag = 0 + _mem.dtcssplitflag = 0 + + if mem.tmode != "Cross": + _mem.tmode = TMODES.index(mem.tmode) + # for the non-cross modes, use ONE tone for both send + # and receive but figure out where to get it from. + if mem.tmode == "TSQL" or mem.tmode == "TSQL-R": + _mem.rtone = chirp_common.TONES.index(mem.ctone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + else: + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.rtone) + + # and one tone for dtcs, but this is always the sending one + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.rx_dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + + else: + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.rx_dtcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs) + if mem.cross_mode == "Tone->Tone": + # tone->tone cross mode is treated as + # TSQL, but with separate tones for + # send and receive + _mem.tmode = TMODES.index("TSQL") + _mem.rtonesplitflag = 1 + elif mem.cross_mode == "DTCS->DTCS": + # DTCS->DTCS cross mode is treated as + # DTCS, but with separate codes for + # send and receive + _mem.tmode = TMODES.index("DTCS") + _mem.dtcssplitflag = 1 + else: + _mem.tmode = TMODES.index("Cross") + \ + CROSS_MODES.index(mem.cross_mode) + + _mem.isnarrow = MODES.index(mem.mode) + _mem.step = STEPS.index(mem.tuning_step) + _flag["%s_pskip" % nibble] = mem.skip == "P" + _flag["%s_skip" % nibble] = mem.skip == "S" + if mem.power: + _mem.power = 3 - POWER_LEVELS.index(mem.power) + else: + _mem.power = 3 + + _mem.name = _encode_name(mem.name) + + # set all unknown areas of the memory map to 0 + _mem.unknown0 = 0 + _mem.unknown1 = 0 + _mem.unknown2 = 0 + _mem.unknown3 = 0 + _mem.unknown4 = 0 + _mem.unknown5 = 0 + + LOG.debug("encoded mem\n%s\n" % (util.hexprint(_mem.get_raw()[0:20]))) + + def get_settings(self): + _settings = self._memobj.settings + _dtmf_strings = self._memobj.dtmf_strings + _passwd = self._memobj.passwd + + repeater = RadioSettingGroup("repeater", "Repeater Settings") + ctcss = RadioSettingGroup("ctcss", "CTCSS/DCS/EPCS Settings") + arts = RadioSettingGroup("arts", "ARTS Settings") + mbls = RadioSettingGroup("banks", "Memory Settings") + scan = RadioSettingGroup("scan", "Scan Settings") + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + wires = RadioSettingGroup("wires", "WiRES(tm) Settings") + switch = RadioSettingGroup("switch", "Switch/Knob Settings") + disp = RadioSettingGroup("disp", "Display Settings") + misc = RadioSettingGroup("misc", "Miscellaneous Settings") + + setmode = RadioSettings(repeater, ctcss, arts, mbls, scan, + dtmf, wires, switch, disp, misc) + + # numbers and names of settings refer to the way they're + # presented in the set menu, as well as the list starting on + # page 74 of the manual + + # 1 APO + opts = ["Off", "30 Min", "1 Hour", "3 Hour", "5 Hour", "8 Hour"] + misc.append( + RadioSetting( + "apo", "Automatic Power Off", + RadioSettingValueList(opts, opts[_settings.apo]))) + + # 2 AR.BEP + opts = ["Off", "In Range", "Always"] + arts.append( + RadioSetting( + "arts_beep", "ARTS Beep", + RadioSettingValueList(opts, opts[_settings.arts_beep]))) + + # 3 AR.INT + opts = ["15 Sec", "25 Sec"] + arts.append( + RadioSetting( + "arts_interval", "ARTS Polling Interval", + RadioSettingValueList(opts, opts[_settings.arts_interval]))) + + # 4 ARS + opts = ["Off", "On"] + repeater.append( + RadioSetting( + "ars", "Automatic Repeater Shift", + RadioSettingValueList(opts, opts[_settings.ars]))) + + # 5 BCLO + opts = ["Off", "On"] + misc.append(RadioSetting( + "busy_lockout", "Busy Channel Lock-Out", + RadioSettingValueList(opts, opts[_settings.busy_lockout]))) + + # 6 BEEP + opts = ["Off", "Key+Scan", "Key"] + switch.append(RadioSetting( + "beep", "Enable the Beeper", + RadioSettingValueList(opts, opts[_settings.beep]))) + + # 7 BELL + opts = ["Off", "1", "3", "5", "8", "Continuous"] + ctcss.append(RadioSetting("bell", "Bell Repetitions", + RadioSettingValueList(opts, opts[ + _settings.bell]))) + + # 8 BNK.LNK + for i in range(0, 8): + opts = ["Off", "On"] + mbs = (self._memobj.mbs >> i) & 1 + rs = RadioSetting("mbs%i" % i, "Bank %s Scan" % (i + 1), + RadioSettingValueList(opts, opts[mbs])) + + def apply_mbs(s, index): + if int(s.value): + self._memobj.mbs |= (1 << index) + else: + self._memobj.mbs &= ~(1 << index) + rs.set_apply_callback(apply_mbs, i) + mbls.append(rs) + + # 9 BNK.NM - A per-bank attribute, nothing to do here. + + # 10 CLK.SFT - A per-channel attribute, nothing to do here. + + # 11 CW.ID + opts = ["Off", "On"] + arts.append(RadioSetting("cw_id", "CW ID Enable", + RadioSettingValueList(opts, opts[ + _settings.cw_id]))) + + cw_id_text = "" + for i in _settings.cw_id_string: + try: + cw_id_text += CHARSET[i & 0x7F] + except IndexError: + if i != 0xff: + LOG.debug("unknown char index in cw id: %x " % (i)) + + val = RadioSettingValueString(0, 16, cw_id_text, True) + val.set_charset(CHARSET + "abcdefghijklmnopqrstuvwxyz") + rs = RadioSetting("cw_id_string", "CW Identifier Text", val) + + def apply_cw_id(s): + str = s.value.get_value().upper().rstrip() + mval = "" + mval = [chr(CHARSET.index(x)) for x in str] + for x in range(len(mval), 16): + mval.append(chr(0xff)) + for x in range(0, 16): + _settings.cw_id_string[x] = ord(mval[x]) + rs.set_apply_callback(apply_cw_id) + arts.append(rs) + + # 12 CWTRNG + opts = ["Off", "4WPM", "5WPM", "6WPM", "7WPM", "8WPM", "9WPM", + "10WPM", "11WPM", "12WPM", "13WPM", "15WPM", "17WPM", + "20WPM", "24WPM", "30WPM", "40WPM"] + misc.append(RadioSetting("cw_trng", "CW Training", + RadioSettingValueList(opts, opts[ + _settings.cw_trng]))) + + # todo: make the setting of the units here affect the display + # of the speed. Not critical, but would be slick. + opts = ["CPM", "WPM"] + misc.append(RadioSetting("cw_trng_units", "CW Training Units", + RadioSettingValueList(opts, + opts[_settings. + cw_trng_units]))) + + # 13 DC VLT - a read-only status, so nothing to do here + + # 14 DCS CD - A per-channel attribute, nothing to do here + + # 15 DCS.RV + opts = ["Disabled", "Enabled"] + ctcss.append(RadioSetting( + "inverted_dcs", + "\"Inverted\" DCS Code Decoding", + RadioSettingValueList(opts, + opts[_settings.inverted_dcs]))) + + # 16 DIMMER + opts = ["Off"] + ["Level %d" % (x) for x in range(1, 11)] + disp.append(RadioSetting("dimmer", "Dimmer", + RadioSettingValueList(opts, + opts[_settings + .dimmer]))) + + # 17 DT.A/M + opts = ["Manual", "Auto"] + dtmf.append(RadioSetting("dtmf_mode", "DTMF Autodialer", + RadioSettingValueList(opts, + opts[_settings + .dtmf_mode]))) + + # 18 DT.DLY + opts = ["50 ms", "250 ms", "450 ms", "750 ms", "1000 ms"] + dtmf.append(RadioSetting("dtmf_delay", "DTMF Autodialer Delay Time", + RadioSettingValueList(opts, + opts[_settings + .dtmf_delay]))) + + # 19 DT.SET + for memslot in range(0, 10): + dtmf_memory = "" + for i in _dtmf_strings[memslot].dtmf_string: + if i != 0xFF: + try: + dtmf_memory += CHARSET[i] + except IndexError: + LOG.debug("unknown char index in dtmf: %x " % (i)) + + val = RadioSettingValueString(0, 16, dtmf_memory, True) + val.set_charset(CHARSET + "abcdef") + rs = RadioSetting("dtmf_string_%d" % memslot, + "DTMF Memory %d" % memslot, val) + + def apply_dtmf(s, i): + LOG.debug("applying dtmf for %x\n" % i) + str = s.value.get_value().upper().rstrip() + LOG.debug("str is %s\n" % str) + mval = "" + mval = [chr(CHARSET.index(x)) for x in str] + for x in range(len(mval), 16): + mval.append(chr(0xff)) + for x in range(0, 16): + _dtmf_strings[i].dtmf_string[x] = ord(mval[x]) + rs.set_apply_callback(apply_dtmf, memslot) + dtmf.append(rs) + + # 20 DT.SPD + opts = ["50 ms", "100 ms"] + dtmf.append(RadioSetting("dtmf_speed", + "DTMF Autodialer Sending Speed", + RadioSettingValueList(opts, + opts[_settings. + dtmf_speed]))) + + # 21 EDG.BEP + opts = ["Off", "On"] + mbls.append(RadioSetting("edge_beep", "Band Edge Beeper", + RadioSettingValueList(opts, + opts[_settings. + edge_beep]))) + + # 22 INT.CD + opts = ["DTMF %X" % (x) for x in range(0, 16)] + wires.append(RadioSetting("int_cd", "Access Number for WiRES(TM)", + RadioSettingValueList(opts, opts[ + _settings.int_cd]))) + + # 23 ING MD + opts = ["Sister Radio Group", "Friends Radio Group"] + wires.append(RadioSetting("wires_mode", + "Internet Link Connection Mode", + RadioSettingValueList(opts, + opts[_settings. + wires_mode]))) + + # 24 INT.A/M + opts = ["Manual", "Auto"] + wires.append(RadioSetting("wires_auto", "Internet Link Autodialer", + RadioSettingValueList(opts, + opts[_settings + .wires_auto]))) + # 25 INT.SET + opts = ["F%d" % (x) for x in range(0, 10)] + + wires.append(RadioSetting("int_set", "Memory Register for " + "non-WiRES Internet", + RadioSettingValueList(opts, + opts[_settings + .int_set]))) + + # 26 LOCK + opts = ["Key", "Dial", "Key + Dial", "PTT", + "Key + PTT", "Dial + PTT", "All"] + switch.append(RadioSetting("lock", "Control Locking", + RadioSettingValueList(opts, + opts[_settings + .lock]))) + + # 27 MCGAIN + opts = ["Level %d" % (x) for x in range(1, 10)] + misc.append(RadioSetting("mic_gain", "Microphone Gain", + RadioSettingValueList(opts, + opts[_settings + .mic_gain]))) + + # 28 MEM.SCN + opts = ["Tag 1", "Tag 2", "All Channels"] + rs = RadioSetting("scan_mode", "Memory Scan Mode", + RadioSettingValueList(opts, + opts[_settings + .scan_mode - 1])) + # this setting is unusual in that it starts at 1 instead of 0. + # that is, index 1 corresponds to "Tag 1", and index 0 is invalid. + # so we create a custom callback to handle this. + + def apply_scan_mode(s): + myopts = ["Tag 1", "Tag 2", "All Channels"] + _settings.scan_mode = myopts.index(s.value.get_value()) + 1 + rs.set_apply_callback(apply_scan_mode) + mbls.append(rs) + + # 29 MW MD + opts = ["Lower", "Next"] + mbls.append(RadioSetting("mw_mode", "Memory Write Mode", + RadioSettingValueList(opts, + opts[_settings + .mw_mode]))) + + # 30 NM SET - This is per channel, so nothing to do here + + # 31 OPN.MSG + opts = ["Off", "DC Supply Voltage", "Text Message"] + disp.append(RadioSetting("open_msg", "Opening Message Type", + RadioSettingValueList(opts, + opts[_settings. + open_msg]))) + + openmsg = "" + for i in _settings.openMsg_Text: + try: + openmsg += CHARSET[i & 0x7F] + except IndexError: + if i != 0xff: + LOG.debug("unknown char index in openmsg: %x " % (i)) + + val = RadioSettingValueString(0, 6, openmsg, True) + val.set_charset(CHARSET + "abcdefghijklmnopqrstuvwxyz") + rs = RadioSetting("openMsg_Text", "Opening Message Text", val) + + def apply_openmsg(s): + str = s.value.get_value().upper().rstrip() + mval = "" + mval = [chr(CHARSET.index(x)) for x in str] + for x in range(len(mval), 6): + mval.append(chr(0xff)) + for x in range(0, 6): + _settings.openMsg_Text[x] = ord(mval[x]) + rs.set_apply_callback(apply_openmsg) + disp.append(rs) + + # 32 PAGER - a per-channel attribute + + # 33 PAG.ABK + opts = ["Off", "On"] + ctcss.append(RadioSetting("pag_abk", "Paging Answer Back", + RadioSettingValueList(opts, + opts[_settings + .pag_abk]))) + + # 34 PAG.CDR + opts = ["%2.2d" % (x) for x in range(1, 50)] + ctcss.append(RadioSetting("pag_cdr_1", "Receive Page Code 1", + RadioSettingValueList(opts, + opts[_settings + .pag_cdr_1]))) + + ctcss.append(RadioSetting("pag_cdr_2", "Receive Page Code 2", + RadioSettingValueList(opts, + opts[_settings + .pag_cdr_2]))) + + # 35 PAG.CDT + opts = ["%2.2d" % (x) for x in range(1, 50)] + ctcss.append(RadioSetting("pag_cdt_1", "Transmit Page Code 1", + RadioSettingValueList(opts, + opts[_settings + .pag_cdt_1]))) + + ctcss.append(RadioSetting("pag_cdt_2", "Transmit Page Code 2", + RadioSettingValueList(opts, + opts[_settings + .pag_cdt_2]))) + + # Common Button Options + button_opts = ["Squelch Off", "Weather", "Smart Search", + "Tone Scan", "Scan", "T Call", "ARTS"] + + # 36 PRG P1 + opts = button_opts + ["DC Volts"] + switch.append(RadioSetting( + "prog_p1", "P1 Button", + RadioSettingValueList(opts, opts[_settings.prog_p1]))) + + # 37 PRG P2 + opts = button_opts + ["Dimmer"] + switch.append(RadioSetting( + "prog_p2", "P2 Button", + RadioSettingValueList(opts, opts[_settings.prog_p2]))) + + # 38 PRG P3 + opts = button_opts + ["Mic Gain"] + switch.append(RadioSetting( + "prog_p3", "P3 Button", + RadioSettingValueList(opts, opts[_settings.prog_p3]))) + + # 39 PRG P4 + opts = button_opts + ["Skip"] + switch.append(RadioSetting( + "prog_p4", "P4 Button", + RadioSettingValueList(opts, opts[_settings.prog_p4]))) + + # 40 PSWD + password = "" + for i in _passwd: + if i != 0xFF: + try: + password += CHARSET[i] + except IndexError: + LOG.debug("unknown char index in password: %x " % (i)) + + val = RadioSettingValueString(0, 4, password, True) + val.set_charset(CHARSET[0:15] + "abcdef ") + rs = RadioSetting("passwd", "Password", val) + + def apply_password(s): + str = s.value.get_value().upper().rstrip() + mval = "" + mval = [chr(CHARSET.index(x)) for x in str] + for x in range(len(mval), 4): + mval.append(chr(0xff)) + for x in range(0, 4): + _passwd[x] = ord(mval[x]) + rs.set_apply_callback(apply_password) + misc.append(rs) + + # 41 RESUME + opts = ["3 Sec", "5 Sec", "10 Sec", "Busy", "Hold"] + scan.append(RadioSetting("resume", "Scan Resume Mode", + RadioSettingValueList(opts, opts[ + _settings.resume]))) + + # 42 RF.SQL + opts = ["Off"] + ["S-%d" % (x) for x in range(1, 10)] + misc.append(RadioSetting("rf_sql", "RF Squelch Threshold", + RadioSettingValueList(opts, opts[ + _settings.rf_sql]))) + + # 43 RPT - per channel attribute, nothing to do here + + # 44 RVRT + opts = ["Off", "On"] + misc.append(RadioSetting("revert", "Priority Revert", + RadioSettingValueList(opts, opts[ + _settings.revert]))) + + # 45 S.SRCH + opts = ["Single", "Continuous"] + misc.append(RadioSetting("s_search", "Smart Search Sweep Mode", + RadioSettingValueList(opts, opts[ + _settings.s_search]))) + + # 46 SHIFT - per channel setting, nothing to do here + + # 47 SKIP = per channel setting, nothing to do here + + # 48 SPLIT - per channel attribute, nothing to do here + + # 49 SQL.TYP - per channel attribute, nothing to do here + + # 50 STEP - per channel attribute, nothing to do here + + # 51 TEMP - read-only status, nothing to do here + + # 52 TN FRQ - per channel attribute, nothing to do here + + # 53 TOT + opts = ["Off", "1 Min", "3 Min", "5 Min", "10 Min"] + misc.append(RadioSetting("tot", "Timeout Timer", + RadioSettingValueList(opts, + opts[_settings.tot]))) + + # 54 TS MUT + opts = ["Off", "On"] + ctcss.append(RadioSetting("ts_mut", "Tone Search Mute", + RadioSettingValueList(opts, + opts[_settings + .ts_mut]))) + + # 55 TS SPEED + opts = ["Fast", "Slow"] + ctcss.append(RadioSetting("ts_speed", "Tone Search Scanner Speed", + RadioSettingValueList(opts, + opts[_settings + .ts_speed]))) + + # 56 VFO.SCN + opts = ["+/- 1MHz", "+/- 2MHz", "+/-5MHz", "All"] + scan.append(RadioSetting("vfo_scan", "VFO Scanner Width", + RadioSettingValueList(opts, + opts[_settings + .vfo_scan]))) + + # 57 WX.ALT + opts = ["Off", "On"] + misc.append(RadioSetting("wx_alert", "Weather Alert Scan", + RadioSettingValueList(opts, opts[ + _settings.wx_alert]))) + + # 58 WX.VOL + opts = ["Normal", "Maximum"] + misc.append(RadioSetting("wx_vol_max", "Weather Alert Volume", + RadioSettingValueList(opts, opts[ + _settings.wx_vol_max]))) + + # 59 W/N DV - this is a per-channel attribute, nothing to do here + + return setmode + + def set_settings(self, uisettings): + _settings = self._memobj.settings + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + + try: + name = element.get_name() + value = element.value + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + obj = getattr(_settings, name) + setattr(_settings, name, value) + + LOG.debug("Setting %s: %s" % (name, value)) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def get_bank_model(self): + return FT2900BankModel(self) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn Radio off. + 2. Connect data cable. + 3. While holding "A/N LOW" button, turn radio on. + 4. After clicking OK, press "SET MHz" to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn Radio off. + 2. Connect data cable. + 3. While holding "A/N LOW" button, turn radio on. + 4. Press "MW D/MR" to receive image. + 5. Make sure display says "-WAIT-" (see note below if not) + 6. Click OK to dismiss this dialog and start transfer. + + Note: if you don't see "-WAIT-" at step 5, try cycling + power and pressing and holding red "*L" button to unlock + radio, then start back at step 1.""")) + return rp + + +# the FT2900E is the European version of the radio, almost identical +# to the R (USA) version, except for the model number and ID Block. We +# create and register a class for it, with only the needed overrides +# NOTE: Disabled until detection is fixed +# @directory.register +class FT2900ERadio(FT2900Radio): + + """Yaesu FT-2900E""" + MODEL = "FT-2900E/1900E" + VARIANT = "E" + IDBLOCK = "\x56\x43\x32\x33\x00\x02\x41\x02\x01\x01" diff --git a/chirp/drivers/ft2d.py b/chirp/drivers/ft2d.py new file mode 100644 index 0000000..6c15f16 --- /dev/null +++ b/chirp/drivers/ft2d.py @@ -0,0 +1,166 @@ +# Copyright 2010 Dan Smith +# Portions Copyright 2017 Wade Simmons +# Copyright 2017 Declan Rieb +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from textwrap import dedent + +from chirp.drivers import yaesu_clone, ft1d +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSetting, RadioSettings +from chirp.settings import RadioSettingValueString + +# Differences from Yaesu FT1D +# 999 memories, but 901-999 are only for skipping VFO frequencies +# Text in memory and memory bank structures is ASCII encoded +# Expanded modes +# Slightly different clone-mode instructions + +LOG = logging.getLogger(__name__) + +TMODES = ["", "Tone", "TSQL", "DTCS", "RTone", "JRfrq", "PRSQL", "Pager"] + +class FT2Bank(chirp_common.NamedBank): # Like FT1D except for name in ASCII + def get_name(self): + _bank = self._model._radio._memobj.bank_info[self.index] + name = "" + for i in _bank.name: + if i == 0xff: + break + name += chr(i & 0xFF) + return name.rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_info[self.index] + _bank.name = [ord(x) for x in name.ljust(16, chr(0xFF))[:16]] + +class FT2BankModel(ft1d.FT1BankModel): #just need this one to launch FT2Bank + """A FT1D bank model""" + def __init__(self, radio, name='Banks'): + super(FT2BankModel, self).__init__(radio, name) + + _banks = self._radio._memobj.bank_info + self._bank_mappings = [] + for index, _bank in enumerate(_banks): + bank = FT2Bank(self, "%i" % index, "BANK-%i" % index) + bank.index = index + self._bank_mappings.append(bank) + +@directory.register +class FT2D(ft1d.FT1Radio): + """Yaesu FT-2D""" + BAUD_RATE = 38400 + VENDOR = "Yaesu" + MODEL = "FT2D" # Yaesu doesn't use a hyphen in its documents + VARIANT = "R" + + _model = "AH60M" # Get this from chirp .img file after saving once + _has_vibrate = True + _mem_params = (0x94a, # Location of DTMF storage + 999, # size of memories array + 999, # size of flags array + 0xFECA, # APRS beacon metadata address. + 60, # Number of beacons stored. + 0x1064A, # APRS beacon content address. + 134, # Length of beacon data stored. + 60) # Number of beacons stored. + _APRS_HIGH_SPEED_MAX = 90 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA terminal. + 3. Press and hold [DISP] key while turning on radio + ("CLONE" will appear on the display). + 4. After clicking OK here in chirp, + press the [Send] screen button.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA terminal. + 3. Press and hold in [DISP] key while turning on radio + ("CLONE" will appear on radio LCD). + 4. Press [RECEIVE] screen button + ("-WAIT-" will appear on radio LCD). + 5. Finally, press OK button below.""")) + return rp + + def get_features(self): # AFAICT only TMODES & memory bounds are different + rf = super(FT2D, self).get_features() + rf.valid_tmodes = list(TMODES) + rf.memory_bounds = (1, 999) + return rf + + def get_bank_model(self): # here only to launch the bank model + return FT2BankModel(self) + + def get_memory(self, number): + mem = super(FT2D, self).get_memory(number) + flag = self._memobj.flag[number - 1] + if number >= 901 and number <= 999: # for FT2D; enforces skip + mem.skip = "S" + flag.skip = True + return mem + + def _decode_label(self, mem): + return str(mem.label).rstrip("\xFF").decode('ascii', 'replace') + + def _encode_label(self, mem): + label = mem.name.rstrip().encode('ascii', 'ignore') + return self._add_ff_pad(label, 16) + + def set_memory(self, mem): + flag = self._memobj.flag[mem.number - 1] + if mem.number >= 901 and mem.number <= 999: # for FT2D; enforces skip + flag.skip = True + mem.skip = "S" + super(FT2D, self).set_memory(mem) + + def _decode_opening_message(self, opening_message): + msg = "" + for i in opening_message.message.padded_yaesu: + if i == 0xFF: + break + msg += chr(int(i)) + val = RadioSettingValueString(0, 16, msg) + rs = RadioSetting("opening_message.message.padded_yaesu", + "Opening Message", val) + rs.set_apply_callback(self._apply_opening_message, + opening_message.message.padded_yaesu) + return rs + + def _apply_opening_message(self, setting, obj): + data = self._add_ff_pad(setting.value.get_value().rstrip(), 16) + val = [] + for i in data: + val.append(ord(i)) + self._memobj.opening_message.message.padded_yaesu = val + +@directory.register +class FT2Dv2(FT2D): + """Yaesu FT-2D v2 firwmare""" + VARIANT = "Rv2" + + _model = "AH60G" + +@directory.register +class FT3D(FT2D): + """Yaesu FT-3D""" + MODEL = "FT3D" + VARIANT = "R" + + _model = "AH72M" diff --git a/chirp/drivers/ft4.py b/chirp/drivers/ft4.py new file mode 100644 index 0000000..0b7f319 --- /dev/null +++ b/chirp/drivers/ft4.py @@ -0,0 +1,1300 @@ +# Copyright 2019 Dan Clemmensen +# Derives loosely from two sources released under GPLv2: +# ./template.py, Copyright 2012 Dan Smith +# ./ft60.py, Copyright 2011 Dan Smith +# Edited 2020 Bernhard Hailer AE6YN +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +CHIRP driver for Yaesu radios that use the SCU-35 cable. This includes at +least the FT-4X, FT-4V, FT-65, and FT-25. This driver will not work with older +Yaesu models. +""" +import logging +import struct +import copy +from chirp import chirp_common, directory, memmap, bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList, RadioSettingValueString, RadioSettings + +LOG = logging.getLogger(__name__) + + +# Layout of Radio memory image. +# This module and the serial protocol treat the FT-4 memory as 16-byte blocks. +# There in nothing magic about 16-byte blocks, but it simplifies the +# description. There are 17 groups of blocks, each with a different purpose +# and format. Five groups consist of channel memories, or "mems" in CHIRP. +# A "mem" describes a radio channel, and all "mems" have the same internal +# format. Three of the groups consist of bitmaps, which all have the same +# internal mapping. also groups for Name, misc, DTMF digits, and prog, +# plus some unused groups. The MEM_FORMAT describes the radio image memory. +# MEM_FORMAT is parsed in module ../bitwise.py. Syntax is similar but not +# identical to C data and structure definitions. + +# Define the structures for each type of group here, but do not associate them +# with actual memory addresses yet +MEM_FORMAT = """ +struct slot { + u8 tx_pwr; //0, 1, 2 == lo, medium, high + bbcd freq[4]; // Hz/10 but must end in 00 + u8 tx_ctcss; //see ctcss table, but radio code = CHIRP code+1. 0==off + u8 rx_ctcss; //see ctcss table, but radio code = CHIRP code+1. 0==off + u8 tx_dcs; //see dcs table, but radio code = CHIRP code+1. 0==off + u8 rx_dcs; //see dcs table, but radio code = CHIRP code+1. 0==off + u8 duplex; //(auto,offset). (0,2,4,5)= (+,-,0, auto) + ul16 offset; //little-endian binary * scaler, +- per duplex + //scaler is 25 kHz for FT-4, 50 kHz for FT-65. + u8 tx_width; //0=wide, 1=narrow + u8 step; //STEPS (0-9)=(auto,5,6.25,10,12.5,15,20,25,50,100) kHz + u8 sql_type; //(0-6)==(off,r-tone,t-tone,tsql,rev tn,dcs,pager) + u8 unused; +}; +// one bit per channel. 220 bits (200 mem+ 2*10 PMS) padded to fill +//exactly 2 blocks +struct bitmap { +u8 b8[28]; +u8 unused[4]; +}; +//name struct occupies half a block (8 bytes) +//the code restricts the actual len to 6 for an FT-4 +struct name { + u8 chrs[8]; //[0-9,A-z,a-z, -] padded with spaces +}; +//txfreq struct occupies 4 bytes (1/4 slot) +struct txfreq { + bbcd freq[4]; +}; + +//miscellaneous params. One 4-block group. +//"SMI": "Set Mode Index" of the FT-4 radio keypad function to set parameter. +//"SMI numbers on the FT-65 are different but the names in mem are the same. +struct misc { + u8 apo; //SMI 01. 0==off, (1-24) is the number of half-hours. + u8 arts_beep; //SMI 02. 0==off, 1==inrange, 2==always + u8 arts_intv; //SMI 03. 0==25 seconds, 1==15 seconds + u8 battsave; //SMI 32. 0==off, (1-5)==(200,300,500,1000,2000) ms + u8 bclo; //SMI 04. 0==off, 1==on + u8 beep; //SMI 05. (0-2)==(key+scan,key, off) + u8 bell; //SMI 06. (0-5)==(0,1,3,5,8,continuous) bells + u8 cw_id[6]; //SMI 08. callsign (A_Z,0-9) (pad with space if <6) + u8 unknown1[3]; + // addr= 2010 + u8 dtmf_mode; //SMI 12. 0==manual, 1==auto + u8 dtmf_delay; //SMI 11. (0-4)==(50,250,450,750,1000) ms + u8 dtmf_speed; //SMI 13. (0,1)=(50,100) ms + u8 edg_beep; //SMI 14. 0==off, 1==on + u8 key_lock; //SMI 18. (0-2)==(key,ptt,key+ptt) + u8 lamp; //SMI 15. (0-4)==(5,10,30,continuous,off) secKEY + u8 tx_led; //SMI 17. 0==off, 1==on + u8 bsy_led; //SMI 16. 0==off, 1==on + u8 moni_tcall; //SMI 19. (0-4)==(mon,1750,2100,1000,1450) tcall Hz. + u8 pri_rvt; //SMI 23. 0==off, 1==on + u8 scan_resume; //SMI 34. (0-2)==(busy,hold,time) + u8 rf_squelch; //SMI 28. 0==off, 8==full, (1-7)==(S1-S7) + u8 scan_lamp; //SMI 33 0==off,1==on + u8 unknown2; + u8 use_cwid; //SMI 7. 0==no, 1==yes + u8 compander; // compander on FT_65 + // addr 2020 + u8 unknown3; + u8 tx_save; //SMI 41. 0==off, 1==on (addr==2021) + u8 vfo_spl; //SMI 42. 0==off, 1==on + u8 vox; //SMI 43. 0==off, 1==on + u8 wfm_rcv; //SMI 44. 0==off, 1==on + u8 unknown4; + u8 wx_alert; //SMI 46. 0==off, 1==0n + u8 tot; //SMI 39. 0-off, (1-30)== (1-30) minutes + u8 pager_tx1; //SMI 21. (0-49)=(1-50) epcs code (i.e., value is code-1) + u8 pager_tx2; //SMI 21 same + u8 pager_rx1; //SMI 23 same + u8 pager_rx2; //SMI 23 same + u8 pager_ack; //SMI 22 same + u8 unknown5[3]; //possibly sql_setting and pgm_vfo_scan on FT-65? + // addr 2030 + u8 use_passwd; //SMI 26 0==no, 1==yes + u8 passwd[4]; //SMI 27 ASCII (0-9) + u8 unused2[11]; // pad out to a block boundary +}; + +struct dtmfset { + u8 digit[16]; //ASCII (*,#,-,0-9,A-D). (dash-filled) +}; + +// area to be filled with 0xff, or just ignored +struct notused { + u8 unused[16]; +}; +// areas we are still analyzing +struct unknown { + u8 notknown[16]; +}; + +struct progc { + u8 usage; //P key is (0-2) == unused, use P channel, use parm + u8 submode:2, //if parm!=0 submode 0-3 of mode + parm:6; //if usage == 2: if 0, use m-channel, else mode +}; +""" +# Actual memory layout. 0x215 blocks, in 20 groups. +MEM_FORMAT += """ +#seekto 0x0000; +struct unknown radiotype; //0000 probably a radio type ID but not sure +struct slot memory[200]; //0010 channel memory array +struct slot pms[20]; //0c90 10 PMS (L,U) slot pairs +struct slot vfo[5]; //0dd0 VFO (A UHF, A VHF, B FM, B UHF, B VHF) +struct slot home[3]; //0e20 Home (FM, VHF, UHF) +struct bitmap enable; //0e50 +struct bitmap scan; //0e70 +struct notused notused0; //0e90 +struct bitmap bankmask[10]; //0ea0 +struct notused notused1[2]; //0fe0 +struct name names[220]; //1000 220 names in 110 blocks +struct notused notused2[2]; //16e0 +struct txfreq txfreqs[220]; //1700 220 freqs in 55 blocks +struct notused notused3[89]; //1a20 +struct misc settings; //2000 4-block collection of misc params +struct notused notused4[2]; //2040 +struct dtmfset dtmf[9]; //2060 sets 1-9 +struct notused notused5; //20f0 +//struct progs progkeys; //2100 not a struct. bitwise.py refuses +struct progc progctl[4]; //2100 8 bytes. 1 2-byte struct per P key +u8 pmemndx[4]; //2108 4 bytes, 1 per P key +u8 notused6[4]; //210c fill out the block +struct slot prog[4]; //2110 P key "channel" array +//---------------- end of FT-4 mem? +""" +# The remaining mem is (apparently) not available on the FT4 but is +# reported to be available on the FT-65. Not implemented here yet. +# Possibly, memory-mapped control registers that allow for "live-mode" +# operation instead of "clone-mode" operation. +# 2150 27ff (unused?) +# 2800 285f 6 MRU operation? +# 2860 2fff (unused?) +# 3000 310f 17 (version info, etc?) +# ----------END of memory map + + +# Begin Serial transfer utilities for the SCU-35 cable. + +# The serial transfer protocol was implemented here after snooping the wire. +# After it was implemented, we noticed that it is identical to the protocol +# implemented in th9000.py. A non-echo version is implemented in anytone_ht.py. +# +# The pipe.read and pipe.write functions use bytes, not strings. The serial +# transfer utilities operate only to move data between the memory object and +# the serial port. The code runs on either Python 2 or Python3, so some +# constructs could be better optimized for one or the other, but not both. + + +def get_mmap_data(radio): + """ + horrible kludge needed until we convert entirely to Python 3 OR we add a + slightly less horrible kludge to the Py2 or Py3 versions of memmap.py. + The minimal change have Py3 code return a bytestring instead of a string. + This is the only function in this module that must explicitly test for the + data string type. It is used only in the do_upload function. + returns the memobj data as a byte-like object. + """ + data = radio.get_mmap().get_packed() + if isinstance(data, bytes): + return data + return bytearray(radio.get_mmap()._data) + + +def checkSum8(data): + """ + Calculate the 8 bit checksum of buffer + Input: buffer - bytes + returns: integer + """ + return sum(x for x in bytearray(data)) & 0xFF + + +def variable_len_resp(pipe): + """ + when length of expected reply is not known, read byte at a time + until the ack character is found. + """ + response = b"" + i = 0 + toolong = 256 # arbitrary + while True: + b = pipe.read(1) + if b == b'\x06': + break + else: + response += b + i += 1 + if i > toolong: + LOG.debug("Response too long. got" + util.hexprint(response)) + raise errors.RadioError("Response from radio too long.") + return(response) + + +def sendcmd(pipe, cmd, response_len): + """ + send a command bytelist to radio,receive and return the resulting bytelist. + Input: pipe - serial port object to use + cmd - bytes to send + response_len - number of bytes of expected response, + not including the ACK. (if None, read until ack) + This cable is "two-wire": The TxD and RxD are "or'ed" so we receive + whatever we send and then whatever response the radio sends. We check the + echo and strip it, returning only the radio's response. + We also check and strip the ACK character at the end of the response. + """ + pipe.write(cmd) + echo = pipe.read(len(cmd)) + if echo != cmd: + msg = "Bad echo. Sent:" + util.hexprint(cmd) + ", " + msg += "Received:" + util.hexprint(echo) + LOG.debug(msg) + raise errors.RadioError( + "Incorrect echo on serial port. Radio off? Bad cable?") + if response_len is None: + return variable_len_resp(pipe) + if response_len > 0: + response = pipe.read(response_len) + else: + response = b"" + ack = pipe.read(1) + if ack != b'\x06': + LOG.debug("missing ack: expected 0x06, got" + util.hexprint(ack)) + raise errors.RadioError("Incorrect ACK on serial port.") + return response + + +def enter_clonemode(radio): + """ + Send the PROGRAM command and check the response. Retry if + needed. After 3 tries, send an "END" and try some more if + it is acknowledged. + """ + for use_end in range(0, 3): + for i in range(0, 3): + try: + if b"QX" == sendcmd(radio.pipe, b"PROGRAM", 2): + return + except: + continue + sendcmd(radio.pipe, b"END", 0) + raise errors.RadioError("expected QX from radio.") + + +def startcomms(radio, way): + """ + For either upload or download, put the radio into PROGRAM mode + and check the radio's ID. In this preliminary version of the driver, + the exact nature of the ID has been inferred from a single test case. + set up the progress bar + send "PROGRAM" to command the radio into clone mode + read the initial string (version?) + """ + progressbar = chirp_common.Status() + progressbar.msg = "Cloning " + way + " radio" + progressbar.max = radio.numblocks + enter_clonemode(radio) + id_response = sendcmd(radio.pipe, b'\x02', None) + if id_response != radio.id_str: + substr0 = radio.id_str[:radio.id_str.find('\x00')] + if id_response[:id_response.find('\x00')] != substr0: + msg = "ID mismatch. Expected" + util.hexprint(radio.id_str) + msg += ", Received:" + util.hexprint(id_response) + LOG.warning(msg) + raise errors.RadioError("Incorrect ID read from radio.") + else: + msg = "ID suspect. Expected" + util.hexprint(radio.id_str) + msg += ", Received:" + util.hexprint(id_response) + LOG.warning(msg) + return progressbar + + +def getblock(pipe, addr, image): + """ + read a single 16-byte block from the radio. + send the command and check the response + places the response into the correct offset in the supplied bytearray + returns True if successful, False if error. + """ + cmd = struct.pack(">cHb", b"R", addr, 16) + response = sendcmd(pipe, cmd, 21) + if (response[0] != b"W"[0]) or (response[1:4] != cmd[1:4]): + msg = "Bad response. Sent:" + util.hexprint(cmd) + ", " + msg += b"Received:" + util.hexprint(response) + LOG.debug(msg) + return False + if checkSum8(response[1:20]) != bytearray(response)[20]: + LOG.debug(b"Bad checksum: " + util.hexprint(response)) + return False + image[addr:addr+16] = response[4:20] + return True + + +def do_download(radio): + """ + Read memory from the radio. + call startcomms to go into program mode and check version + create an mmap + read the memory blocks and place the data into the mmap + send "END" + """ + image = bytearray(radio.get_memsize()) + pipe = radio.pipe # Get the serial port connection + progressbar = startcomms(radio, "from") + for blocknum in range(radio.numblocks): + for i in range(0, 3): + if getblock(pipe, 16 * blocknum, image): + break + if i == 2: + raise errors.RadioError( + "read block from radio failed 3 times") + progressbar.cur = blocknum + radio.status_fn(progressbar) + sendcmd(pipe, b"END", 0) + return memmap.MemoryMap(bytes(image)) + + +def putblock(pipe, addr, data): + """ + write a single 16-byte block to the radio + send the command and check the response + """ + chkstr = struct.pack(">Hb", addr, 16) + data + msg = b'W' + chkstr + struct.pack('B', checkSum8(chkstr)) + b'\x06' + sendcmd(pipe, msg, 0) + + +def do_upload(radio): + """ + Write memory image to radio + call startcomms to go into program mode and check version + write the memory blocks. Skip the first block + send "END" + """ + pipe = radio.pipe # Get the serial port connection + progressbar = startcomms(radio, "to") + data = get_mmap_data(radio) + for _i in range(1, radio.numblocks): + putblock(pipe, 16*_i, data[16*_i:16*(_i+1)]) + progressbar.cur = _i + radio.status_fn(progressbar) + sendcmd(pipe, b"END", 0) + return +# End serial transfer utilities + + +def bit_loc(bitnum): + """ + return the ndx and mask for a bit location + """ + return (bitnum // 8, 1 << (bitnum & 7)) + + +def store_bit(bankmem, bitnum, val): + """ + store a bit in a bankmem. Store 0 or 1 for False or True + """ + ndx, mask = bit_loc(bitnum) + if val: + bankmem.b8[ndx] |= mask + else: + bankmem.b8[ndx] &= ~mask + return + + +def retrieve_bit(bankmem, bitnum): + """ + return True or False for a bit in a bankmem + """ + ndx, mask = bit_loc(bitnum) + return (bankmem.b8[ndx] & mask) != 0 + + +# A bank is a bitmap of 220 bits. 200 mem slots and 2*10 PMS slots. +# There are 10 banks. +class YaesuSC35GenericBankModel(chirp_common.BankModel): + + def get_num_mappings(self): + return 10 + + def get_mappings(self): + banks = [] + for i in range(1, 1 + self.get_num_mappings()): + bank = chirp_common.Bank(self, "%i" % i, "Bank %i" % i) + bank.index = i - 1 + banks.append(bank) + return banks + + def add_memory_to_mapping(self, memory, bank): + bankmem = self._radio._memobj.bankmask[bank.index] + store_bit(bankmem, memory.number-1, True) + + def remove_memory_from_mapping(self, memory, bank): + bankmem = self._radio._memobj.bankmask[bank.index] + if not retrieve_bit(bankmem, memory.number-1): + raise Exception("Memory %i is not in bank %s." % + (memory.number, bank)) + store_bit(bankmem, memory.number-1, False) + + # return a list of slots in a bank + def get_mapping_memories(self, bank): + memories = [] + for i in range(*self._radio.get_features().memory_bounds): + if retrieve_bit(self._radio._memobj.bankmask[bank.index], i - 1): + memories.append(self._radio.get_memory(i)) + return memories + + # return a list of banks a slot is a member of + def get_memory_mappings(self, memory): + memndx = memory.number - 1 + banks = [] + for bank in self.get_mappings(): + if retrieve_bit(self._radio._memobj.bankmask[bank.index], memndx): + banks.append(bank) + return banks + +# the values in these lists must also be in the canonical UI list +# we can re-arrange the order, and we don't need to have all +# the values, but we cannot add our own values here. +DUPLEX = ["+", "", "-", "", "off", "", "split"] # (0,2,4,5)= (+,-,0, auto) +# the radio implements duplex "auto" as 5. we map to "" It appears to be +# a convienience function in the radio that affects the offset, but I do not +# understand it. + +SKIPS = ["", "S"] + +POWER_LEVELS = [ + chirp_common.PowerLevel("High", watts=5.0), # high must be first (0) + chirp_common.PowerLevel("Mid", watts=2.5), + chirp_common.PowerLevel("Low", watts=0.5)] + +# these steps encode to 0-9 on all radios, but encoding #2 is disallowed +# on the US versions (FT-4XR) +STEP_CODE = [0, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +US_LEGAL_STEPS = list(STEP_CODE) # copy to pass to UI on US radios +US_LEGAL_STEPS.remove(6.25) # euro radios just use STEP_CODE + +# Map the radio image sql_type (0-6) to the CHIRP mem values. +# Yaesu "TSQL" and "DCS" each map to different CHIRP values depending on the +# radio values of the tx and rx tone codes. The table is a list of rows, one +# per Yaesu sql_type (0-5). The code does not use this table when the sql_type +# is 6 (PAGER). Each row is a tuple. Its first member is a list of +# [tmode,cross] or [tmode, cross, suppress]. "Suppress" is used only when +# encoding UI-->radio. When decoding radio-->UI, two of the sql_types each +# result in 5 possibible UI decodings depending on the tx and rx codes, and the +# list in each of these rows has five members. These two row tuples each have +# two additional members to specify which of the radio fields to examine. +# The map from CHIRP UI to radio image types is also built from this table. +RADIO_TMODES = [ + ([["", None], ], ), # sql_type= 0. off + ([["Cross", "->Tone"], ], ), # sql_type= 1. R-TONE + ([["Tone", None], ], ), # sql_type= 2. T-TONE + ([ # sql_type= 3. TSQL: + ["", None], # tx==0, rx==0 : invalid + ["TSQL", None], # tx==0 + ["Tone", None], # rx==0 + ["Cross", "Tone->Tone"], # tx!=rx + ["TSQL", None] # tx==rx + ], "tx_ctcss", "rx_ctcss"), # tx and rx fields to check + ([["TSQL-R", None], ], ), # sql_type= 4. REV TN + ([ # sql_type= 5.DCS: + ["", None], # tx==0, rx==0 : invalid + ["Cross", "->DTCS", "tx_dcs"], # tx==0. suppress tx + ["Cross", "DTCS->", "rx_dcs"], # rx==0. suppress rx + ["Cross", "DTCS->DTCS"], # tx!=rx + ["DTCS", None] # tx==rx + ], "tx_dcs", "rx_dcs"), # tx and rx fields to check + # # sql_type= 6. PAGER is a CHIRP "extra" + ] + +# Find all legal values for the tmode and cross fields for the UI. +# We build two dictionaries to do the lookups when encoding. +# The reversed range is a kludge: by happenstance, earlier duplicates +# in the above table are the preferred mapping, they override the +# later ones when we process the table backwards. +TONE_DICT = {} # encode sql_type. +CROSS_DICT = {} # encode sql_type. + +for sql_type in reversed(range(0, len(RADIO_TMODES))): + sql_type_row = RADIO_TMODES[sql_type] + for decode_row in sql_type_row[0]: + suppress = None + if len(decode_row) == 3: + suppress = decode_row[2] + TONE_DICT[decode_row[0]] = (sql_type, suppress) + if decode_row[1]: + CROSS_DICT[decode_row[1]] = (sql_type, suppress) + +# The keys are added to the "VALID" lists using code that puts those lists +# in the same order as the UI's default order instead of the random dict +# order or the arbitrary build order. +VALID_TONE_MODES = [] # list for UI. +VALID_CROSS_MODES = [] # list for UI. +for name in chirp_common.TONE_MODES: + if name in TONE_DICT: + VALID_TONE_MODES += [name] +for name in chirp_common.CROSS_MODES: + if name in CROSS_DICT: + VALID_CROSS_MODES += [name] + + +DTMF_CHARS = "0123456789ABCD*#- " +CW_ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ " +PASSWD_CHARS = "0123456789" +CHARSET = CW_ID_CHARS + "abcdefghijklmnopqrstuvwxyz*+-/@" +PMSNAMES = ["%s%02d" % (c, i) for i in range(1, 11) for c in ('L', 'U')] + +# Four separate arrays of special channel mems. +# Each special has unique constrants: band, name yes/no, and pms L/U +# The FT-65 class replaces the "prog" entry in this list. +# The name field must be the name of a slot array in MEM_FORMAT +SPECIALS_FT4 = [ + ("pms", PMSNAMES), + ("vfo", ["VFO A UHF", "VFO A VHF", "VFO B FM", "VFO B VHF", "VFO B UHF"]), + ("home", ["HOME FM", "HOME VHF", "HOME UHF"]), + ("prog", ["P1", "P2"]) + ] +SPECIALS_FT65 = SPECIALS_FT4 +FT65_PROGS = ("prog", ["P1", "P2", "P3", "P4"]) +SPECIALS_FT65[-1] = FT65_PROGS # replace the last entry (P key names) + + +VALID_BANDS_DUAL = [ + (65000000, 108000000), # broadcast FM, receive only + (136000000, 174000000), # VHF + (400000000, 480000000) # UHF + ] + +VALID_BANDS_VHF = [ + (65000000, 108000000), # broadcast FM, receive only + (136000000, 174000000), # VHF + ] + +# bands for the five VFO and three home channel memories +BAND_ASSIGNMENTS_DUALBAND = [2, 1, 0, 1, 2, 0, 1, 2] # all locations used +BAND_ASSIGNMENTS_MONO_VHF = [1, 1, 0, 1, 1, 0, 1, 1] # UHF locations unused + + +# None, and 50 Tones. Use this explicit array because the +# one in chirp_common could change and no longer describe our radio +TONE_MAP = [ + None, 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, + 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, + 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, + 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, + 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, + 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, + 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, + 225.7, 229.1, 233.6, 241.8, 250.3, 254.1 + ] + +# None, and 104 DTCS Codes. Use this explicit array because the +# one in chirp_common could change and no longer describe our radio +DTCS_MAP = [ + None, 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, + 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, + 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, + 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, + 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, + 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, + 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, + 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, + 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, + 731, 732, 734, 743, 754 + ] + +# The legal PAGER codes are the same as the CTCSS codes, but we +# pass them to the UI as a list of strings +EPCS_CODES = [format(flt) for flt in [0] + TONE_MAP[1:]] + + +# allow a child class to add a param to its class +# description list. used when a specific radio class has +# a param that is not in the generic list. +def add_paramdesc(desc_list, group, param): + for description in desc_list: + groupname, title, parms = description + if group == groupname: + description[2].append(param) + return + + +class YaesuSC35GenericRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """ + Base class for all Yaesu radios using the SCU-35 programming cable + and its protocol. Classes for sub families extend this class and + are found towards the end of this file. + """ + VENDOR = "Yaesu" + MODEL = "SCU-35Generic" # No radio directly uses the base class + BAUD_RATE = 9600 + MAX_MEM_SLOT = 200 + NEEDS_COMPAT_SERIAL = False + + # These settings are common to all radios in this family. + _valid_chars = chirp_common.CHARSET_ASCII + numblocks = 0x215 # number of 16-byte blocks in the radio + _memsize = 16 * numblocks # used by CHIRP file loader to guess radio type + MAX_MEM_SLOT = 200 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ( + 'Tested only by the developer and only on a single radio.\n' + 'Proceed at your own risk!' + ) + + rp.pre_download = "".join([ + "1. Connect programming cable to MIC jack.\n", + "2. Press OK." + ] + ) + rp.pre_upload = rp.pre_download + return rp + + # identify the features that can be manipulated on this radio. + # mentioned here only when different from defaults in chirp_common.py + def get_features(self): + + rf = chirp_common.RadioFeatures() + specials = [name for s in self.class_specials for name in s[1]] + rf.valid_special_chans = specials + rf.memory_bounds = (1, self.MAX_MEM_SLOT) + rf.valid_duplexes = DUPLEX + rf.valid_tmodes = VALID_TONE_MODES + rf.valid_cross_modes = VALID_CROSS_MODES + rf.valid_power_levels = POWER_LEVELS + rf.valid_tuning_steps = self.legal_steps + rf.valid_skips = SKIPS + rf.valid_characters = CHARSET + rf.valid_name_length = self.namelen + rf.valid_modes = ["FM", "NFM"] + rf.valid_bands = self.valid_bands + rf.can_odd_split = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = False # REV TN reverses the tone, not the dcs + rf.has_cross = True + rf.has_settings = True + + return rf + + def get_bank_model(self): + return YaesuSC35GenericBankModel(self) + + # read and parse the radio memory + def sync_in(self): + try: + self._mmap = do_download(self) + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + # write the memory image to the radio + def sync_out(self): + try: + do_upload(self) + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + # There are about 40 settings and most are handled generically in + # get_settings. get_settings invokes these handlers for the few + # that are more complicated. + + # callback for setting byte arrays: DTMF[0-9], passwd, and CW_ID + def apply_str_to_bytearray(self, element, obj): + lng = len(obj) + strng = (element.value.get_value() + " ")[:lng] + bytes = bytearray(strng, "ascii") + for x in range(0, lng): # memobj cannot iterate, so byte-by-byte + obj[x] = bytes[x] + return + + # add a string value to the RadioSettings + def get_string_setting(self, obj, valid_chars, desc1, desc2, group): + content = '' + maxlen = len(obj) + for x in range(0, maxlen): + content += chr(obj[x]) + val = RadioSettingValueString(0, maxlen, content, True, valid_chars) + rs = RadioSetting(desc1, desc2, val) + rs.set_apply_callback(self.apply_str_to_bytearray, obj) + group.append(rs) + + # called when found in the group_descriptions table to handle string value + def get_strset(self, group, parm): + # parm =(paramname, paramtitle,(handler,[handler params])). + objname, title, fparms = parm + myparms = fparms[1] + obj = getattr(self._memobj.settings, objname) + self.get_string_setting(obj, myparms[0], objname, title, group) + + # called when found in the group_descriptions table for DTMF strings + def get_dtmfs(self, group, parm): + objname, title, fparms = parm + for i in range(1, 10): + dtmf_digits = self._memobj.dtmf[i - 1].digit + self.get_string_setting( + dtmf_digits, DTMF_CHARS, + "dtmf_%i" % i, "DTMF Autodialer Memory %i" % i, group) + + def apply_P(self, element, pnum, memobj): + memobj.progctl[pnum].usage = element.value + + def apply_Pmode(self, element, pnum, memobj): + memobj.progctl[pnum].parm = element.value + + def apply_Psubmode(self, element, pnum, memobj): + memobj.progctl[pnum].submode = element.value + + def apply_Pmem(self, element, pnum, memobj): + memobj.pmemndx[pnum] = element.value + + MEMLIST = ["%d" % i for i in range(1, MAX_MEM_SLOT + 1)] + PMSNAMES + USAGE_LIST = ["unused", "P channel", "mode or M channel"] + + # called when found in the group_descriptions table + # returns the settings for the programmable keys (P1-P4) + def get_progs(self, group, parm): + _progctl = self._memobj.progctl + _progndx = self._memobj.pmemndx + + def get_prog(i, val_list, valndx, sname, longname, f_apply): + k = str(i + 1) + val = val_list[valndx] + valuelist = RadioSettingValueList(val_list, val) + rs = RadioSetting(sname + k, longname + k, valuelist) + rs.set_apply_callback(f_apply, i, self._memobj) + group.append(rs) + for i in range(0, self.Pkeys): + get_prog(i, self.USAGE_LIST, _progctl[i].usage, + "P", "Programmable key ", self.apply_P) + get_prog(i, self.SETMODES, _progctl[i].parm, "modeP", + "mode for Programmable key ", self.apply_Pmode) + get_prog(i, ["0", "1", "2", "3"], _progctl[i].submode, "submodeP", + "submode for Programmable key ", self.apply_Psubmode) + get_prog(i, self.MEMLIST, _progndx[i], "memP", + "mem for Programmable key ", self.apply_Pmem) + # ------------ End of special settings handlers. + + # list of group description tuples: [groupame,group title, [param list]]. + # A param is a tuple: + # for a simple param: (paramname, paramtitle,[valuename list]) + # for a handler param: (paramname, paramtitle,( handler,[handler params])) + # This is a class variable. subclasses msut create a variable named + # class_group_descs. The FT-4 classes simply equate this, but the + # FT-65 classes must copy and modify this. + group_descriptions = [ + ("misc", "Miscellaneous Settings", [ # misc + ("apo", "Automatic Power Off", + ["OFF"] + ["%0.1f" % (x * 0.5) for x in range(1, 24 + 1)]), + ("bclo", "Busy Channel Lock-Out", ["OFF", "ON"]), + ("beep", "Enable the Beeper", ["OFF", "KEY", "KEY+SC"]), + ("bsy_led", "Busy LED", ["ON", "OFF"]), + ("edg_beep", "Band Edge Beeper", ["OFF", "ON"]), + ("vox", "VOX", ["OFF", "ON"]), + ("rf_squelch", "RF Squelch Threshold", + ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6", "S-7", "S-FULL"]), + ("tot", "Timeout Timer", + ["OFF"] + ["%dMIN" % (x) for x in range(1, 30 + 1)]), + ("tx_led", "TX LED", ["OFF", "ON"]), + ("use_cwid", "use CW ID", ["NO", "YES"]), + ("cw_id", "CW ID Callsign", (get_strset, [CW_ID_CHARS])), # handler + ("vfo_spl", "VFO Split", ["OFF", "ON"]), + ("wfm_rcv", "Enable Broadband FM", ["OFF", "ON"]), + ("passwd", "Password", (get_strset, [PASSWD_CHARS])) # handler + ]), + ("arts", "ARTS Settings", [ # arts + ("arts_beep", "ARTS BEEP", ["OFF", "INRANG", "ALWAYS"]), + ("arts_intv", "ARTS Polling Interval", ["25 SEC", "15 SEC"]) + ]), + ("ctcss", "CTCSS/DCS/DTMF Settings", [ # ctcss + ("bell", "Bell Repetitions", ["OFF", "1T", "3T", "5T", "8T", "CONT"]), + ("dtmf_mode", "DTMF Mode", ["Manual", "Auto"]), + ("dtmf_delay", "DTMF Autodialer Delay Time", + ["50 MS", "100 MS", "250 MS", "450 MS", "750 MS", "1000 MS"]), + ("dtmf_speed", "DTMF Autodialer Sending Speed", ["50 MS", "100 MS"]), + ("dtmf", "DTMF Autodialer Memory ", (get_dtmfs, [])) # handler + ]), + ("switch", "Switch/Knob Settings", [ # switch + ("lamp", "Lamp Mode", ["5SEC", "10SEC", "30SEC", "KEY", "OFF"]), + ("moni_tcall", "MONI Switch Function", + ["MONI", "1750", "2100", "1000", "1450"]), + ("key_lock", "Lock Function", ["KEY", "PTT", "KEY+PTT"]), + ("Pkeys", "Pkey fields", (get_progs, [])) + ]), + ("scan", "Scan Settings", [ # scan + ("scan_resume", "Scan Resume Mode", ["BUSY", "HOLD", "TIME"]), + ("pri_rvt", "Priority Revert", ["OFF", "ON"]), + ("scan_lamp", "Scan Lamp", ["OFF", "ON"]), + ("wx_alert", "Weather Alert Scan", ["OFF", "ON"]) + ]), + ("power", "Power Saver Settings", [ # power + ("battsave", "Receive Mode Battery Save Interval", + ["OFF", "200 MS", "300 MS", "500 MS", "1 S", "2 S"]), + ("tx_save", "Transmitter Battery Saver", ["OFF", "ON"]) + ]), + ("eai", "EAI/EPCS Settings", [ # eai + ("pager_tx1", "TX pager frequency 1", EPCS_CODES), + ("pager_tx2", "TX pager frequency 2", EPCS_CODES), + ("pager_rx1", "RX pager frequency 1", EPCS_CODES), + ("pager_rx2", "RX pager frequency 2", EPCS_CODES), + ("pager_ack", "Pager answerback", ["NO", "YES"]) + ]) + ] + # ----------------end of group_descriptions + + # returns the current values of all the settings in the radio memory image, + # in the form of a RadioSettings list. Uses the class_group_descs + # list to create the groups and params. Valuelist scalars are handled + # inline. More complex params are built by calling the special handlers. + def get_settings(self): + _settings = self._memobj.settings + groups = RadioSettings() + for description in self.class_group_descs: + groupname, title, parms = description + group = RadioSettingGroup(groupname, title) + groups.append(group) + for parm in parms: + param, title, opts = parm + try: + if isinstance(opts, list): + # setting is a single value from the list + objval = getattr(_settings, param) + value = opts[objval] + valuelist = RadioSettingValueList(opts, value) + group.append(RadioSetting(param, title, valuelist)) + else: + # setting needs special handling. opts[0] is a + # function name + opts[0](self, group, parm) + except Exception as e: + LOG.debug( + "%s: cannot set %s to %s" % (e, param, repr(objval)) + ) + return groups + # end of get_settings + + # modify settings values in the radio memory image + def set_settings(self, uisettings): + _settings = self._memobj.settings + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + + try: + name = element.get_name() + value = element.value + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + setattr(_settings, name, value) + + LOG.debug("Setting %s: %s" % (name, value)) + except: + LOG.debug(element.get_name()) + raise + + # maps a boolean pair (tx==0,rx==0) to the numbers 0-3 + LOOKUP = [[True, True], [True, False], [False, True], [False, False]] + + def decode_sql(self, mem, chan): + """ + examine the radio channel fields and determine the correct + CHIRP CSV values for tmode, cross_mode, and sql_override + """ + mem.extra = RadioSettingGroup("Extra", "extra") + extra_modes = ["(None)", "PAGER"] + value = extra_modes[chan.sql_type == 6] + valuelist = RadioSettingValueList(extra_modes, value) + rs = RadioSetting("sql_override", "Squelch override", valuelist) + mem.extra.append(rs) + if chan.sql_type == 6: + return + sql_map = RADIO_TMODES[chan.sql_type] + ndx = 0 + if len(sql_map[0]) > 1: + # the sql_type is TSQL or DCS, so there are multiple UI mappings + x = getattr(chan, sql_map[1]) + r = getattr(chan, sql_map[2]) + ndx = self.LOOKUP.index([x == 0, r == 0]) + if ndx == 3 and x == r: + ndx = 4 + mem.tmode = sql_map[0][ndx][0] + cross = sql_map[0][ndx][1] + if cross: + mem.cross_mode = cross + if chan.rx_ctcss: + mem.ctone = TONE_MAP[chan.rx_ctcss] + if chan.tx_ctcss: + mem.rtone = TONE_MAP[chan.tx_ctcss] + if chan.tx_dcs: + mem.dtcs = DTCS_MAP[chan.tx_dcs] + if chan.rx_dcs: + mem.rx_dtcs = DTCS_MAP[chan.rx_dcs] + + def encode_sql(self, mem, chan): + """ + examine CHIRP's mem.tmode and mem.cross_mode and set the values + for the radio sql_type, dcs codes, and ctcss codes. We set all four + codes, and then zero out a code if needed when Tone or DCS is one-way + """ + chan.tx_ctcss = TONE_MAP.index(mem.rtone) + chan.tx_dcs = DTCS_MAP.index(mem.dtcs) + chan.rx_ctcss = TONE_MAP.index(mem.ctone) + chan.rx_dcs = DTCS_MAP.index(mem.rx_dtcs) + if mem.tmode == "TSQL": + chan.tx_ctcss = chan.rx_ctcss # CHIRP uses ctone for TSQL + if mem.tmode == "DTCS": + chan.tx_dcs = chan.rx_dcs # CHIRP uses rx_dtcs for DTCS + # select the correct internal dictionary and key + mode_dict, key = [ + (TONE_DICT, mem.tmode), + (CROSS_DICT, mem.cross_mode) + ][mem.tmode == "Cross"] + # now look up that key in that dictionary. + chan.sql_type, suppress = mode_dict[key] + if suppress: + setattr(chan, suppress, 0) + for setting in mem.extra: + if (setting.get_name() == 'sql_override'): + value = str(setting.value) + if value == "PAGER": + chan.sql_type = 6 + return + + # given a CHIRP memory ref, get the radio memobj for it. + # A memref is either a number or the name of a special + # CHIRP will sometimes use numbers (>MAX_SLOTS) for specials + # returns the obj and several attributes + def slotloc(self, memref): + array = None + num = memref + sname = memref + if isinstance(memref, str): # named special? + num = self.MAX_MEM_SLOT + 1 + for x in self.class_specials: + try: + ndx = x[1].index(memref) + array = x[0] + break + except: + num += len(x[1]) + if array is None: + LOG.debug("unknown Special %s", memref) + raise + num += ndx + elif memref > self.MAX_MEM_SLOT: # numbered special? + ndx = memref - (self.MAX_MEM_SLOT + 1) + for x in self.class_specials: + if ndx < len(x[1]): + array = x[0] + sname = x[1][ndx] + break + ndx -= len(x[1]) + if array is None: + LOG.debug("memref number %d out of range", memref) + raise + else: # regular memory slot + array = "memory" + ndx = memref - 1 + memloc = getattr(self._memobj, array)[ndx] + return (memloc, ndx, num, array, sname) + # end of slotloc + + # return the raw info for a memory channel + def get_raw_memory(self, memref): + memloc, ndx, num, regtype, sname = self.slotloc(memref) + if regtype == "memory": + return repr(memloc) + else: + return repr(memloc) + repr(self._memobj.names[ndx]) + + # return the info for a memory channel In CHIRP canonical form + def get_memory(self, memref): + + def clean_name(obj): # helper func to tidy up the name + name = '' + for x in range(0, self.namelen): + y = obj[x] + if y == 0: + break + if y == 0x7F: # when programmed from VFO + y = 0x20 + name += chr(y) + return name.rstrip() + + mem = chirp_common.Memory() + _mem, ndx, num, regtype, sname = self.slotloc(memref) + mem.number = num + + # First, we need to know whether a channel is enabled, + # then we can process any channel parameters. + # It was found (at least on an FT-25) that channels might be + # uninitialized and memory is just completely filled with 0xFF. + + if regtype == "pms": + mem.extd_number = sname + if regtype in ["memory", "pms"]: + ndx = num - 1 + mem.name = clean_name(self._memobj.names[ndx].chrs) + mem.empty = not retrieve_bit(self._memobj.enable, ndx) + mem.skip = SKIPS[retrieve_bit(self._memobj.scan, ndx)] + else: + mem.empty = False + mem.extd_number = sname + mem.immutable = ["number", "extd_number", "name", "skip"] + + # So, now if channel is not empty, we can do the evaluation of + # all parameters. Otherwise we set them to defaults. + + if mem.empty: + mem.freq = 0 + mem.offset = 0 + mem.duplex = "off" + mem.power = POWER_LEVELS[0] # "High" + mem.mode = "FM" + mem.tuning_step = 0 + else: + mem.freq = int(_mem.freq) * 10 + txfreq = int(self._memobj.txfreqs[ndx].freq) * 10 + if (txfreq != 0) and (txfreq != mem.freq): + mem.duplex = "split" + mem.offset = txfreq + else: + mem.offset = int(_mem.offset) * self.freq_offset_scale + mem.duplex = DUPLEX[_mem.duplex] + self.decode_sql(mem, _mem) + mem.power = POWER_LEVELS[2 - _mem.tx_pwr] + mem.mode = ["FM", "NFM"][_mem.tx_width] + mem.tuning_step = STEP_CODE[_mem.step] + return mem + + def enforce_band(self, memloc, freq, mem_num, sname): + """ + vfo and home channels are each restricted to a particular band. + If the frequency is not in the band, use the lower bound + Raise an exception to cause UI to pop up an error message + """ + first_vfo_num = self.MAX_MEM_SLOT + len(PMSNAMES) + 1 + band = BAND_ASSIGNMENTS[mem_num - first_vfo_num] + frange = self.valid_bands[band] + if freq >= frange[0] and freq <= frange[1]: + memloc.freq = freq / 10 + return freq + memloc.freq = frange[0] / 10 + raise Exception("freq out of range for %s" % sname) + + # modify a radio channel in memobj based on info in CHIRP canonical form + def set_memory(self, mem): + _mem, ndx, num, regtype, sname = self.slotloc(mem.number) + assert(_mem) + if mem.empty: + if regtype in ["memory", "pms"]: + store_bit(self._memobj.enable, ndx, False) + return + + txfreq = mem.freq / 10 # really. RX freq is used for TX base + _mem.freq = txfreq + self.encode_sql(mem, _mem) + if mem.power: + _mem.tx_pwr = 2 - POWER_LEVELS.index(mem.power) + else: + _mem.tx_pwr = 0 # set to "High" if CHIRP canonical value is None + _mem.tx_width = mem.mode == "NFM" + _mem.step = STEP_CODE.index(mem.tuning_step) + + _mem.offset = mem.offset / self.freq_offset_scale + duplex = mem.duplex + if regtype in ["memory", "pms"]: + ndx = num - 1 + store_bit(self._memobj.enable, ndx, True) + store_bit(self._memobj.scan, ndx, SKIPS.index(mem.skip)) + nametrim = (mem.name + " ")[:8] + self._memobj.names[ndx].chrs = bytearray(nametrim, "ascii") + if mem.duplex == "split": + txfreq = mem.offset / 10 + duplex = "off" # radio ignores when tx != rx + self._memobj.txfreqs[num-1].freq = txfreq + _mem.duplex = DUPLEX.index(duplex) + if regtype in ["vfo", "home"]: + self.enforce_band(_mem, mem.freq, num, sname) + + return + + +class YaesuFT4GenericRadio(YaesuSC35GenericRadio): + """ + FT-4 sub family class. Classes for individual radios extend + these classes and are found at the end of this file. + """ + class_specials = SPECIALS_FT4 + Pkeys = 2 # number of programmable keys + namelen = 6 # length of the mem name display on the front-panel + freq_offset_scale = 25000 + class_group_descs = YaesuSC35GenericRadio.group_descriptions + # names for the setmode function for the programmable keys. Mode zero means + # that the key is programmed for a memory not a setmode. + SETMODES = [ + "mem", "apo", "ar bep", "ar int", "beclo", # 00-04 + "beep", "bell", "cw id", "cw wrt", "dc vlt", # 05-09 + "dcs cod", "dt dly", "dt set", "dtc spd", "edg.bep", # 10-14 + "lamp", "led.bsy", "led.tx", "lock", "m/t-cl", # 15-19 + "mem.del", "mem.tag", "pag.abk", "pag.cdr", "pag.cdt", # 20-24 + "pri.rvt", "pswd", "pswdwt", "rf sql", "rpt.ars", # 25-29 + "rpt.frq", "rpt.sft", "rxsave", "scn.lmp", "scn.rsm", # 30-34 + "skip", "sql.typ", "step", "tn frq", "tot", # 35-39 + "tx pwr", "tx save", "vfo.spl", "vox", "wfm.rcv", # 40-44 + "w/n.dev", "wx.alert" # 45-46 + ] + + +class YaesuFT65GenericRadio(YaesuSC35GenericRadio): + """ + FT-65 sub family class. Classes for individual radios extend + these classes and are found at the end of this file. + """ + class_specials = SPECIALS_FT65 + Pkeys = 4 # number of programmable keys + namelen = 8 # length of the mem name display on the front-panel + freq_offset_scale = 50000 + # we need a deep copy here because we are adding deeper than the top level. + class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions) + add_paramdesc( + class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"])) + # names for the setmode function for the programmable keys. Mode zero means + # that the key is programmed for a memory not a setmode. + SETMODES = [ + "mem", "apo", "arts", "battsave", "b-ch.l/o", # 00-04 + "beep", "bell", "compander", "ctcss", "cw id", # 05-09 + "dc volt", "dcs code", "dtmf set", "dtmf wrt", "edg bep", # 10-14 + "key lock", "lamp", "ledbsy", "mem del", "mon/t-cl", # 15-19 + "name tag", "pager", "password", "pri.rvt", "repeater", # 20-24 + "resume", "rf.sql", "scn.lamp", "skip", "sql type", # 25-29 + "step", "tot", "tx pwr", "tx save", "vfo.spl", # 30-34 + "vox", "wfm.rcv", "wide/nar", "wx alert", "scramble" # 35-39 + ] + + +# Classes for each individual radio. + + +@directory.register +class YaesuFT4XRRadio(YaesuFT4GenericRadio): + """ + FT-4X dual band, US version + """ + MODEL = "FT-4XR" + id_str = b'IFT-35R\x00\x00V100\x00\x00' + valid_bands = VALID_BANDS_DUAL + legal_steps = US_LEGAL_STEPS + BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND + + +@directory.register +class YaesuFT4XERadio(YaesuFT4GenericRadio): + """ + FT-4X dual band, EU version + """ + MODEL = "FT-4XE" + id_str = b'IFT-35R\x00\x00V100\x00\x00' + valid_bands = VALID_BANDS_DUAL + legal_steps = STEP_CODE + BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND + + +@directory.register +class YaesuFT4VRRadio(YaesuFT4GenericRadio): + """ + FT-4V VHF, US version + """ + MODEL = "FT-4VR" + id_str = b'IFT-15R\x00\x00V100\x00\x00' + valid_bands = VALID_BANDS_VHF + legal_steps = US_LEGAL_STEPS + BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF + + +# No image available yet +# @directory.register +# class YaesuFT4VERadio(YaesuFT4GenericRadio): +# """ +# FT-4V VHF, EU version +# """ +# MODEL = "FT-4VE" +# id_str = b'IFT-15R\x00\x00V100\x00\x00' +# valid_bands = VALID_BANDS_VHF +# legal_steps = STEP_CODE +# BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF + + +@directory.register +class YaesuFT65RRadio(YaesuFT65GenericRadio): + """ + FT-65 dual band, US version + """ + MODEL = "FT-65R" + id_str = b'IH-420\x00\x00\x00V100\x00\x00' + valid_bands = VALID_BANDS_DUAL + legal_steps = US_LEGAL_STEPS + BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND + + +@directory.register +class YaesuFT65ERadio(YaesuFT65GenericRadio): + """ + FT-65 dual band, EU version + """ + MODEL = "FT-65E" + id_str = b'IH-420\x00\x00\x00V100\x00\x00' + valid_bands = VALID_BANDS_DUAL + legal_steps = STEP_CODE + BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND + + +@directory.register +class YaesuFT25RRadio(YaesuFT65GenericRadio): + """ + FT-25 VHF, US version + """ + MODEL = "FT-25R" + id_str = b'IFT-25R\x00\x00V100\x00\x00' + valid_bands = VALID_BANDS_VHF + legal_steps = US_LEGAL_STEPS + BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF + + +# No image available yet +# @directory.register +# class YaesuFT25ERadio(YaesuFT65GenericRadio): +# """ +# FT-25 VHF, EU version +# """ +# MODEL = "FT-25E" +# id_str = b'IFT-25R\x00\x00V100\x00\x00' +# valid_bands = VALID_BANDS_VHF +# legal_steps = STEP_CODE +# BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF diff --git a/chirp/drivers/ft450d.py b/chirp/drivers/ft450d.py new file mode 100644 index 0000000..6664f77 --- /dev/null +++ b/chirp/drivers/ft450d.py @@ -0,0 +1,1494 @@ +# Copyright 2018 by Rick DeWitt (aa0rd@yahoo.com> +# Thanks to Filippi Marco for Yaesu processes +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""FT450D Yaesu Radio Driver""" + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, util, memmap, errors, directory, bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings +import time +import logging +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +CMD_ACK = 0x06 +# TBD: Enable some form of generated UI field, for the memory tags +# That field wiould not be stored in img file, but generated in get_memory +MEM_GRP_LBL = False # To ignore Comment channel-tags for now +EX_MODES = ["USER-L", "USER-U", "LSB+CW", "USB+CW", "RTTY-L", "RTTY-U", "N/A"] +for i in EX_MODES: + chirp_common.MODES.append(i) +T_STEPS = sorted(list(chirp_common.TUNING_STEPS)) +T_STEPS.remove(30.0) +T_STEPS.remove(100.0) +T_STEPS.remove(125.0) +T_STEPS.remove(200.0) + +@directory.register +class FT450DRadio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FT-450D""" + BAUD_RATE = 38400 + COM_BITS = 8 # number of data bits + COM_PRTY = 'N' # parity checking + COM_STOP = 1 # stop bits + MODEL = "FT-450D" + + DUPLEX = ["", "-", "+"] + MODES = ["LSB", "USB", "CW", "AM", "FM", "RTTY-L", + "USER-L", "USER-U", "NFM", "CWR"] + TMODES = ["", "Tone", "TSQL"] + STEPSFM = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] + STEPSAM = [2.5, 5.0, 9.0, 10.0, 12.5, 25.0] + STEPSSSB = [1.0, 2.5, 5.0] + VALID_BANDS = [(100000, 33000000), (33000000, 56000000)] + FUNC_LIST = ['MONI', 'N/A', 'PBAK', 'PLAY1', 'PLAY2', 'PLAY3', 'QSPLIT', + 'SPOT', 'SQLOFF', 'SWR', 'TXW', 'VCC', 'VOICE2', 'VM1MONI', + 'VM1REC', 'VM1TX', 'VM2MONI', 'VM2REC', 'VM2TX', 'DOWN', 'FAST', + 'UP', 'DSP', 'IPO/ATT', 'NB', 'AGC' , 'MODEDN', 'MODEUP', + 'DSP/SEL', 'KEYER', 'CLAR' , 'BANDDN', 'BANDUP', 'A=B', 'A/B', + 'LOCK', 'TUNE', 'VOICE', 'MW', 'V/M', 'HOME', 'RCL', 'VOX', 'STO', + 'STEP', 'SPLIT', 'PMS', 'SCAN', 'MENU', 'DIMMER', 'MTR'] + CHARSET = list(chirp_common.CHARSET_ASCII) + CHARSET.remove("\\") + + MEM_SIZE = 15017 + # block 9 (135 Bytes long) is to be repeated 101 times + _block_lengths = [4, 84, 135, 162, 135, 162, 151, 130, 135, 127, 189, 103] + + MEM_FORMAT = """ + struct mem_struct { // 27 bytes per channel + u8 tag_on_off:2, // @ Byte 0 1=Off, 2=On + unk0:2, + mode:4; + u8 duplex:2, // @ byte 1 + att:1, + ipo:1, + unka1:1, + tunerbad:1, // ?? Possible tuner failed + unk1b:1, // @@@??? + uprband:1; + u8 cnturpk:1, // @ Byte 2 Peak (clr), Null (set) + cnturmd:3, // Contour filter mode + cnturgn:1, // Contour filter gain Low/high + mode2:3; // When mode is data(5) + u8 ssb_step:2, // @ Byte 3 + am_step:3, + fm_step:3; + u8 tunerok:1, // @ Byte 4 ?? Poss tuned ok + cnturon:1, + unk4b:1, + dnr_on:1 + notch:1, + unk4c:1, + tmode:2; // Tone/Cross/etc as Off/Enc/Enc+Dec + u8 unk5a:4, // @ byte 5 + dnr_val:4; + u8 cw_width:2, // # byte 6, Notch width indexes + fm_width:2, + am_width:2, + sb_width:2; + i8 notch_pos; // @ Byte 7 Signed: - 0 + + u8 tone; // @ Byte 8 + u8 unk9; // @ Byte 9 Always set to 0 + u8 unkA; // @ Byte A + u8 unkB; // @ Byte B + u32 freq; // @ C-F + u32 offset; // @ 10-13 + u8 name[7]; // @ 14-1A + }; + + #seekto 0x04; + struct { + u8 set04; // ?Checksum / Clone counter? + u8 set05; // Current VFO? + u8 set06; + u8 fast:1, + lock:1, // Inverted: 1 = Off + nb:1, + agc:5; + u8 set08a:3, + keyer:1, + set08b:2, + mtr_mode:2; + u8 set09; + u8 set0A; + u8 set0B:2, + clk_sft:1, + cont:5; // 1:1 + u8 beepvol_sgn:1, // @x0C: set : Link @0x41, clear: fix @ 0x40 + set0Ca:3, + clar_btn:1, // 0 = Dial, 1= SEL + cwstone_sgn:1, // Set: Lnk @ x42, clear: Fixed at 0x43 + beepton:2; // Index 0-3 + u8 set0Da:1, + cw_key:1, + set0Db:3, + dialstp_mode:1, + dialstp:2; + u8 set0E:1, + keyhold:1, + lockmod:1, + set0ea:1, + amfmdial:1, // 0= Enabled. 1 = Disabled + cwpitch:3; // 0-based index + u8 sql_rfg:1 + set0F:2, + cwweigt:5; // Index 1:2.5=0 -> 1:4.5=20 + u8 cw_dly; // @x10 ms = val * 10 + u8 set11; + u8 cwspeed; // val 4-60 is wpm, *5 is cpm + u8 vox_gain; // val 1:1 + u8 set14:2, + emergen:1, + vox_dly:5; // ms = val * 100 + u8 set15a:1, + stby_beep:1 + set15b:1 + mem_grp:1, + apo:4; + u8 tot; // Byte x16, 1:1 + u8 micscan:1, + set17:5, + micgain:2; + u8 cwpaddl:1, // @x18 0=Key, 1=Mic + set18:7; + u8 set19; + u8 set1A; + u8 set1B; + u8 set1C; + u8 dig_vox; // 1:1 + u8 set1E; + i16 d_disp; // @ x1F,x20 signed 16bit + u8 pnl_cs; // 0-based index + u8 pm_up; + u8 pm_fst; + u8 pm_dwn; + u8 set25; + u8 set26; + u8 set27; + u8 set28; + u8 beacon_time; // 1:1 + u8 set2A; + u8 cat_rts:1, // @x2b: Enable=0, Disable=1 + peakhold:1, + set2B:4, + cat_tot:2; // Index 0-3 + u8 set2CA:2, + rtyrpol:1, + rtytpol:1 + rty_sft:2, + rty_ton:1, + set2CC:1; + u8 dig_vox; // 1:1 + u8 ext_mnu:1, + m_tune:1, + set2E:2, + scn_res:4; + u8 cw_auto:1, // Off=0, On=1 + cwtrain:2, // Index + set2F:1, + cw_qsk:2, // Index + cw_bfo:2; // Index + u8 mic_eq; // @x30 1:1 + u8 set31:5, + catrate:3; // Index 0-4 + u8 set32; + u8 dimmer:4, + set33:4; + u8 set34; + u8 set35; + u8 set36; + u8 set37; + u8 set38a:1, + rfpower:7; // 1:1 + u8 set39a:2, + tuner:3, // Index 0-4 + seldial:3; // Index 0-5 + u8 set3A; + u8 set3B; + u8 set3C; + i8 qspl_f; // Signed + u8 set3E; + u8 set3F; + u8 beepvol_fix; // 1:1 + i8 beepvol_lnk; // SIGNED 2's compl byte + u8 cwstone_fix; + i8 cwstone_lnk; // signed byte + u8 set44:2, + mym_data:1, // My Mode: Data, set = OFF + mym_fm:1, + mym_am:1, + mym_cw:1, + mym_usb:1, + mym_lsb:1; + u8 myb_24:1, // My Band: 24Mhz set = OFF + myb_21:1, + myb_18:1, + myb_14:1, + myb_10:1, + myb_7:1, + myb_3_5:1, + myb_1_8:1; + u8 set46:6, + myb_28:1, + myb_50:1; + u8 set47; + u8 set48; + u8 set49; + u8 set4A; + u8 set4B; + u8 set4C; + u8 set4D; + u8 set4E; + u8 set4F; + u8 set50; + u8 set51; + u8 set52; + u8 set53; + u8 set54; + u8 set55; + u8 set56a:3, + split:1, + set56b:4; + u8 set57; + } settings; + + #seekto 0x58; + struct mem_struct vfoa[11]; // The current cfgs for each vfo 'band' + struct mem_struct vfob[11]; + struct mem_struct home[2]; // The 2 Home cfgs (HF and 6m) + struct mem_struct qmb; // The Quick Memory Bank STO/RCL + struct mem_struct mtqmb; // Last QMB-MemTune cfg (not displayed) + struct mem_struct mtune; // Last MemTune cfg (not displayed) + + #seekto 0x343; // chan status + u8 visible[63]; // 1 bit per channel + u8 pmsvisible; // @ 0x382 + + #seekto 0x383; + u8 filled[63]; + u8 pmsfilled; // @ 0x3c2 + + #seekto 0x3C3; + struct mem_struct memory[500]; + struct mem_struct pms[4]; // Programed Scan limits @ x387F + + #seekto 0x3906; + struct { + char t1[40]; // CW Beacon Text + char t2[40]; + char t3[40]; + } beacontext; // to 0x397E + + #seekto 0x3985; + struct mem_struct m60[5]; // to 0x3A0B + + #seekto 0x03a45; + struct mem_struct current; + + """ + _CALLSIGN_CHARSET = [chr(x) for x in range(ord("0"), ord("9") + 1) + + range(ord("A"), ord("Z") + 1) + [ord(" ")]] + _CALLSIGN_CHARSET_REV = dict(zip(_CALLSIGN_CHARSET, + range(0, len(_CALLSIGN_CHARSET)))) + + # WARNING Indecis are hard wired in get/set_memory code !!! + # Channels print in + increasing index order (PMS first) + SPECIAL_MEMORIES = { + "VFOa-1.8M": -27, + "VFOa-3.5M": -26, + "VFOa-7M": -25, + "VFOa-10M": -24, + "VFOa-14M": -23, + "VFOa-18M": -22, + "VFOa-21M": -21, + "VFOa-24M": -20, + "VFOa-28M": -19, + "VFOa-50M": -18, + "VFOa-HF": -17, + "VFOb-1.8M": -16, + "VFOb-3.5M": -15, + "VFOb-7M": -14, + "VFOb-10M": -13, + "VFOb-14M": -12, + "VFOb-18M": -11, + "VFOb-21M": - 10, + "VFOb-24M": -9, + "VFOb-28M": -8, + "VFOb-50M": -7, + "VFOb-HF": -6, + "HOME-HF": -5, + "HOME-50M": -4, + "QMB": -3, + "QMB-MTune": -2, + "Mem-Tune": -1, + } + FIRST_VFOB_INDEX = -6 + LAST_VFOB_INDEX = -16 + FIRST_VFOA_INDEX = -17 + LAST_VFOA_INDEX = -27 + + SPECIAL_PMS = { + "PMS1-L": -36, + "PMS1-U": -35, + "PMS2-L": -34, + "PMS2-U": -33, + } + LAST_PMS_INDEX = -36 + SPECIAL_MEMORIES.update(SPECIAL_PMS) + + SPECIAL_60M = { + "60m-Ch1": -32, + "60m-Ch2": -31, + "60m-Ch3": -30, + "60m-Ch4": -29, + "60m-Ch5": -28, + } + LAST_60M_INDEX = -32 + SPECIAL_MEMORIES.update(SPECIAL_60M) + + SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(), + SPECIAL_MEMORIES.keys())) + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = _(dedent(""" + The FT-450 radio driver loads the 'Special Channels' tab + with the PMS scanning range memories (group 11), 60meter + channels (group 12), the QMB (STO/RCL) memory, the HF and + 50m HOME memories and all the A and B VFO memories. + There are VFO memories for the last frequency dialed in + each band. The last mem-tune config is also stored. + These Special Channels allow limited field editting. + This driver also populates the 'Other' tab in the channel + memory Properties window. This tab contains values for + those channel memory settings that don't fall under the + standard Chirp display columns. + """)) + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. After clicking OK here, press the [C.S.] key to + send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. Click OK here. + ("Receiving" will appear on the LCD).""")) + return rp + + def _read(self, block, blocknum): + # be very patient at first block + if blocknum == 0: + attempts = 60 + else: + attempts = 5 + for _i in range(0, attempts): + data = self.pipe.read(block + 2) # Blocknum, data,checksum + if data: + break + time.sleep(0.5) + if len(data) == block + 2 and data[0] == chr(blocknum): + checksum = yaesu_clone.YaesuChecksum(1, block) + if checksum.get_existing(data) != \ + checksum.get_calculated(data): + raise Exception("Checksum Failed [%02X<>%02X] block %02X" % + (checksum.get_existing(data), + checksum.get_calculated(data), blocknum)) + # Remove the block number and checksum + data = data[1:block + 1] + else: # Use this info to decode a new Yaesu model + raise Exception("Unable to read block %i expected %i got %i" + % (blocknum, block + 2, len(data))) + return data + + def _clone_in(self): + # Be very patient with the radio + self.pipe.timeout = 2 + self.pipe.baudrate = self.BAUD_RATE + self.pipe.bytesize = self.COM_BITS + self.pipe.parity = self.COM_PRTY + self.pipe.stopbits = self.COM_STOP + self.pipe.rtscts = False + + start = time.time() + + data = "" + blocks = 0 + status = chirp_common.Status() + status.msg = _("Cloning from radio") + nblocks = len(self._block_lengths) + 100 # Block 8 repeats + status.max = nblocks + for block in self._block_lengths: + if blocks == 8: + # repeated read of block 8 same size (chan memory area) + repeat = 101 + else: + repeat = 1 + for _i in range(0, repeat): + data += self._read(block, blocks) + self.pipe.write(chr(CMD_ACK)) + blocks += 1 + status.cur = blocks + self.status_fn(status) + data += self.MODEL # Append ID + return memmap.MemoryMap(data) + + def _clone_out(self): + self.pipe.baudrate = self.BAUD_RATE + self.pipe.bytesize = self.COM_BITS + self.pipe.parity = self.COM_PRTY + self.pipe.stopbits = self.COM_STOP + self.pipe.rtscts = False + delay = 0.5 + start = time.time() + blocks = 0 + pos = 0 + status = chirp_common.Status() + status.msg = _("Cloning to radio") + status.max = len(self._block_lengths) + 100 + for block in self._block_lengths: + if blocks == 8: + repeat = 101 + else: + repeat = 1 + for _i in range(0, repeat): + time.sleep(0.01) + checksum = yaesu_clone.YaesuChecksum(pos, pos + block - 1) + self.pipe.write(chr(blocks)) + self.pipe.write(self.get_mmap()[pos:pos + block]) + self.pipe.write(chr(checksum.get_calculated(self.get_mmap()))) + buf = self.pipe.read(1) + if not buf or buf[0] != chr(CMD_ACK): + time.sleep(delay) + buf = self.pipe.read(1) + if not buf or buf[0] != chr(CMD_ACK): + raise Exception(_("Radio did not ack block %i") % blocks) + pos += block + blocks += 1 + status.cur = blocks + self.status_fn(status) + + + def sync_in(self): + try: + self._mmap = self._clone_in() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" + % e) + self.process_mmap() + + def sync_out(self): + try: + self._clone_out() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" + % e) + + def process_mmap(self): + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_dtcs= False + if MEM_GRP_LBL: + rf.has_comment = True # Used for Mem-Grp number + rf.valid_modes = list(set(self.MODES)) + rf.valid_tmodes = list(self.TMODES) + rf.valid_duplexes = list(self.DUPLEX) + rf.valid_tuning_steps = list(T_STEPS) + rf.valid_bands = self.VALID_BANDS + rf.valid_power_levels = [] + rf.valid_characters = "".join(self.CHARSET) + rf.valid_name_length = 7 + rf.valid_skips = [] + rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys()) + rf.memory_bounds = (1, 500) + rf.has_ctone = True + rf.has_settings = True + rf.has_cross = True + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tmode(self, mem, _mem): + mem.tmode = self.TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.tone] + mem.ctone = mem.rtone + + def _set_duplex(self, mem, _mem): + _mem.duplex = self.DUPLEX.index(mem.duplex) + + def get_memory(self, number): + if isinstance(number, str): + return self._get_special(number) + elif number < 0: + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special(self.SPECIAL_MEMORIES_REV[number]) + else: + return self._get_normal(number) + + def set_memory(self, memory): + if memory.number < 0: + return self._set_special(memory) + else: + return self._set_normal(memory) + + def _get_special(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_MEMORIES[number] + mem.extd_number = number + + if mem.number in range(self.FIRST_VFOA_INDEX, + self.LAST_VFOA_INDEX - 1, -1): + _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number] + immutable = ["number", "extd_number", "name", "power"] + elif mem.number in range(self.FIRST_VFOB_INDEX, + self.LAST_VFOB_INDEX - 1, -1): + _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number] + immutable = ["number", "extd_number", "name", "power"] + elif mem.number in range(-4, -6, -1): # 2 Home Chans + _mem = self._memobj.home[5 + mem.number] + immutable = ["number", "extd_number", "name", "power"] + elif mem.number == -3: + _mem = self._memobj.qmb + immutable = ["number", "extd_number", "name", "power"] + elif mem.number == -2: + _mem = self._memobj.mtqmb + immutable = ["number", "extd_number", "name", "power"] + elif mem.number == -1: + _mem = self._memobj.mtune + immutable = ["number", "extd_number", "name", "power"] + elif mem.number in self.SPECIAL_PMS.values(): + bitindex = (-self.LAST_PMS_INDEX) + mem.number + used = (self._memobj.pmsvisible >> bitindex) & 0x01 + valid = (self._memobj.pmsfilled >> bitindex) & 0x01 + if not used: + mem.empty = True + if not valid: + mem.empty = True + return mem + mx = (-self.LAST_PMS_INDEX) + mem.number + _mem = self._memobj.pms[mx] + mx = mx + 1 + if MEM_GRP_LBL: + mem.comment = "M-11-%02i" % mx + immutable = ["number", "rtone", "ctone", "extd_number", + "tmode", "cross_mode", + "power", "duplex", "offset"] + elif mem.number in self.SPECIAL_60M.values(): + mx = (-self.LAST_60M_INDEX) + mem.number + _mem = self._memobj.m60[mx] + mx = mx + 1 + if MEM_GRP_LBL: + mem.comment = "M-12-%02i" % mx + immutable = ["number", "rtone", "ctone", "extd_number", + "tmode", "cross_mode", + "frequency", "power", "duplex", "offset"] + else: + raise Exception("Sorry, you can't edit that special" + " memory channel %i." % mem.number) + + mem = self._get_memory(mem, _mem) + mem.immutable = immutable + + return mem + + def _set_special(self, mem): + if mem.empty and mem.number not in self.SPECIAL_PMS.values(): + # can't delete special memories! + raise Exception("Sorry, special memory can't be deleted") + + cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[mem.number]) + + if mem.number in range(self.FIRST_VFOA_INDEX, + self.LAST_VFOA_INDEX - 1, -1): + _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number] + elif mem.number in range(self.FIRST_VFOB_INDEX, + self.LAST_VFOB_INDEX - 1, -1): + _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number] + elif mem.number in range(-4, -6, -1): + _mem = self._memobj.home[5 + mem.number] + elif mem.number == -3: + _mem = self._memobj.qmb + elif mem.number == -2: + _mem = self._memobj.mtqmb + elif mem.number == -1: + _mem = self._memobj.mtune + elif mem.number in self.SPECIAL_PMS.values(): + bitindex = (-self.LAST_PMS_INDEX) + mem.number + wasused = (self._memobj.pmsvisible >> bitindex) & 0x01 + wasvalid = (self._memobj.pmsfilled >> bitindex) & 0x01 + if mem.empty: + if wasvalid and not wasused: + # pylint get confused by &= operator + self._memobj.pmsfilled = self._memobj.pmsfilled & \ + ~ (1 << bitindex) + # pylint get confused by &= operator + self._memobj.pmsvisible = self._memobj.pmsvisible & \ + ~ (1 << bitindex) + return + # pylint get confused by |= operator + self._memobj.pmsvisible = self._memobj.pmsvisible | 1 << bitindex + self._memobj.pmsfilled = self._memobj.pmsfilled | 1 << bitindex + _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number] + else: + raise Exception("Sorry, you can't edit" + " that special memory.") + + for key in cur_mem.immutable: + if key != "extd_number": + if cur_mem.__dict__[key] != mem.__dict__[key]: + raise errors.RadioError("Editing field `%s' " % key + + "is not supported on this channel") + + self._set_memory(mem, _mem) + + def _get_normal(self, number): + _mem = self._memobj.memory[number - 1] + used = (self._memobj.visible[(number - 1) / 8] >> (number - 1) % 8) \ + & 0x01 + valid = (self._memobj.filled[(number - 1) / 8] >> (number - 1) % 8) \ + & 0x01 + + mem = chirp_common.Memory() + mem.number = number + if not used: + mem.empty = True + if not valid or _mem.freq == 0xffffffff: + return mem + if MEM_GRP_LBL: + mgrp = int((number - 1) / 50) + mem.comment = "M-%02i-%02i" % (mgrp + 1, number - (mgrp * 50)) + return self._get_memory(mem, _mem) + + def _set_normal(self, mem): + _mem = self._memobj.memory[mem.number - 1] + wasused = (self._memobj.visible[(mem.number - 1) / 8] >> + (mem.number - 1) % 8) & 0x01 + wasvalid = (self._memobj.filled[(mem.number - 1) / 8] >> + (mem.number - 1) % 8) & 0x01 + + if mem.empty: + if mem.number == 1: + raise Exception("Sorry, can't delete first memory") + if wasvalid and not wasused: + self._memobj.filled[(mem.number - 1) / 8] &= \ + ~(1 << (mem.number - 1) % 8) + _mem.set_raw("\xFF" * (_mem.size() / 8)) # clean up + self._memobj.visible[(mem.number - 1) / 8] &= \ + ~(1 << (mem.number - 1) % 8) + return + if not wasvalid: + _mem.set_raw("\x00" * (_mem.size() / 8)) # clean up + + self._memobj.visible[(mem.number - 1) / 8] |= 1 << (mem.number - 1) \ + % 8 + self._memobj.filled[(mem.number - 1) / 8] |= 1 << (mem.number - 1) \ + % 8 + self._set_memory(mem, _mem) + + def _get_memory(self, mem, _mem): + mem.freq = int(_mem.freq) + mem.offset = int(_mem.offset) + mem.duplex = self.DUPLEX[_mem.duplex] + # Mode gets tricky with dual (USB+DATA) options + vx = _mem.mode + if vx == 4: # FM or NFM + if _mem.mode2 == 2: + vx = 4 # FM + else: + vx = 8 # NFM + if vx == 10: # CWR + vx = 9 + if vx == 5: # Data/Dual mode + if _mem.mode2 == 0: # RTTY-L + vx = 5 + if _mem.mode2 == 1: # USER-L + vx = 6 + if _mem.mode2 == 2: # USER-U + vx = 7 + mem.mode = self.MODES[vx] + if mem.mode == "FM" or mem.mode == "NFM": + mem.tuning_step = self.STEPSFM[_mem.fm_step] + elif mem.mode == "AM": + mem.tuning_step = self.STEPSAM[_mem.am_step] + elif mem.mode[:2] == "CW": + mem.tuning_step = self.STEPSSSB[_mem.ssb_step] + else: + try: + mem.tuning_step = self.STEPSSSB[_mem.ssb_step] + except IndexError: + pass + self._get_tmode(mem, _mem) + + if _mem.tag_on_off == 2: + for i in _mem.name: + if i == 0xFF: + break + if chr(i) in self.CHARSET: + mem.name += chr(i) + else: + # radio has some graphical chars that are not supported + # we replace those with a * + LOG.info("Replacing char %x with *" % i) + mem.name += "*" + mem.name = mem.name.rstrip() + else: + mem.name = "" + + mem.extra = RadioSettingGroup("extra", "Extra") + + rs = RadioSetting("ipo", "IPO", + RadioSettingValueBoolean(bool(_mem.ipo))) + rs.set_doc("Bypass preamp") + mem.extra.append(rs) + + rs = RadioSetting("att", "ATT", + RadioSettingValueBoolean(bool(_mem.att))) + rs.set_doc("10dB front end attenuator") + mem.extra.append(rs) + + rs = RadioSetting("cnturon", "Contour Filter", + RadioSettingValueBoolean(_mem.cnturon )) + rs.set_doc("Contour filter on/off") + mem.extra.append(rs) + + options = ["Peak", "Null"] + rs = RadioSetting("cnturpk", "Contour Filter Mode", + RadioSettingValueList(options, + options[_mem.cnturpk])) + mem.extra.append(rs) + + options = ["Low", "High"] + rs = RadioSetting("cnturgn", "Contour Filter Gain", + RadioSettingValueList(options, + options[_mem.cnturgn])) + rs.set_doc("Filter gain/attenuation") + mem.extra.append(rs) + + options = ["-2", "-1", "Center", "+1", "+2"] + rs = RadioSetting("cnturmd", "Contour Filter Notch", + RadioSettingValueList(options, + options[_mem.cnturmd])) + rs.set_doc("Filter notch offset") + mem.extra.append(rs) + + rs = RadioSetting("notch", "Notch Filter", + RadioSettingValueBoolean(_mem.notch )) + rs.set_doc("IF bandpass filter") + mem.extra.append(rs) + + vx = 1 + options = ["<-", "Center", "+>"] + if _mem.notch_pos < 0: + vx = 0 + if _mem.notch_pos > 0: + vx = 2 + rs = RadioSetting("notch_pos", "Notch Position", + RadioSettingValueList(options, options[vx])) + rs.set_doc("IF bandpass filter shift") + mem.extra.append(rs) + + vx = 0 + if mem.mode[1:] == "SB": + options = ["1.8kHz", "2.4kHz", "3.0kHz"] + vx = _mem.sb_width + stx = "sb_width" + elif mem.mode[:1] == "CW": + options = ["300Hz", "500 kHz", "2.4kHz"] + vx = _mem.cw_width + stx = "cw_width" + elif mem.mode[:4] == "USER" or mem.mode[:4] == "RTTY": + options = ["300Hz", "2.4kHz", "3.0kHz"] + vx = _mem.sb_width + stx = "sb_width" + elif mem.mode == "AM": + options = ["3.0kHz", "6.0kHz", "9.0 kHz"] + vx = _mem.am_width + stx = "am_width" + else: + options = ["2.5kHz", "5.0kHz"] + vx = _mem.fm_width + stx = "fm_width" + rs = RadioSetting(stx, "IF Bandpass Filter Width", + RadioSettingValueList(options, options[vx])) + rs.set_doc("DSP IF bandpass Notch width (Hz)") + mem.extra.append(rs) + + rs = RadioSetting("dnr_on", "DSP Noise Reduction", + RadioSettingValueBoolean(bool(_mem.dnr_on))) + rs.set_doc("Digital noise processing") + mem.extra.append(rs) + + options = ["Off", "1", "2", "3", "4", "5", "6", "7", + "8", "9", "10", "11"] + rs = RadioSetting("dnr_val", "DSP Noise Reduction Alg", + RadioSettingValueList(options, + options[ _mem.dnr_val])) + rs.set_doc("Digital noise reduction algorithm number (1-11)") + mem.extra.append(rs) + + return mem # end get_memory + + def _set_memory(self, mem, _mem): + if len(mem.name) > 0: + _mem.tag_on_off = 2 + else: + _mem.tag_on_off = 1 + self._set_duplex(mem, _mem) + _mem.mode2 = 0 + if mem.mode == "USER-L": + _mem.mode = 5 + _mem.mode2 = 1 + elif mem.mode == "USER-U": + _mem.mode = 5 + _mem.mode2 = 2 + elif mem.mode == "RTTY-L": + _mem.mode = 5 + _mem.mode2 = 0 + elif mem.mode == "CWR": + _mem.mode = 10 + _mem.mode2 = 0 + elif mem.mode == "CW": + _mem.mode = 2 + _mem.mode2 = 0 + elif mem.mode == "NFM": + _mem.mode = 4 + _mem.mode2 = 1 + elif mem.mode == "FM": + _mem.mode = 4 + _mem.mode2 = 2 + else: # LSB, USB, AM + _mem.mode = self.MODES.index(mem.mode) + _mem.mode2 = 0 + try: + _mem.ssb_step = self.STEPSSSB.index(mem.tuning_step) + except ValueError: + pass + try: + _mem.am_step = self.STEPSAM.index(mem.tuning_step) + except ValueError: + pass + try: + _mem.fm_step = self.STEPSFM.index(mem.tuning_step) + except ValueError: + pass + _mem.freq = mem.freq + _mem.uprband = 0 + if mem.freq >= 33000000: + _mem.uprband = 1 + _mem.offset = mem.offset + _mem.tmode = self.TMODES.index(mem.tmode) + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.tunerok = 0 # Dont know what these two do... + _mem.tunerbad = 0 + + for i in range(0, 7): + _mem.name[i] = ord(mem.name.ljust(7)[i]) + + for setting in mem.extra: + if setting.get_name() == "notch_pos": + vx = 0 # Overide list string with signed value + stx = str(setting.value) + if stx == "<-": + vx = -13 + if stx == "+>": + vx = 12 + setattr(_mem, "notch_pos", vx) + elif setting.get_name() == "dnr_val": + stx = str(setting.value) # Convert string to int + vx = 0 + if stx != "Off": + vx = int(stx) + else: + setattr(_mem, "dnr_on", 0) + setattr(_mem, setting.get_name(), vx) + else: + setattr(_mem, setting.get_name(), setting.value) + + + @classmethod + def match_model(cls, filedata, filename): + """Match the opened/downloaded image to the correct version""" + if len(filedata) == cls.MEM_SIZE + 7: # +7 bytes of model name + rid = filedata[cls.MEM_SIZE:cls.MEM_SIZE + 7] + if rid.startswith(cls.MODEL): + return True + else: + return False + + def _invert_me(self, setting, obj, atrb): + """Callback: from inverted logic 1-bit booleans""" + invb = not setting.value + setattr(obj, atrb, invb) + return + + def _chars2str(self, cary, knt): + """Convert raw memory char array to a string: NOT a callback.""" + stx = "" + for char in cary[:knt]: + stx += chr(char) + return stx + + def _my_str2ary(self, setting, obj, atrba, knt): + """Callback: convert string to fixed-length char array..""" + ary = "" + for j in range(0, knt, 1): + chx = ord(str(setting.value)[j]) + if chx < 32 or chx > 125: # strip non-printing + ary += " " + else: + ary += str(setting.value)[j] + setattr(obj, atrba, ary) + return + + def get_settings(self): + _settings = self._memobj.settings + _beacon = self._memobj.beacontext + gen = RadioSettingGroup("gen", "General") + cw = RadioSettingGroup("cw", "CW") + pnlcfg = RadioSettingGroup("pnlcfg", "Panel buttons") + pnlset = RadioSettingGroup("pnlset", "Panel settings") + voxdat = RadioSettingGroup("voxdat", "VOX and Data") + mic = RadioSettingGroup("mic", "Microphone") + mybands = RadioSettingGroup("mybands", "My Bands") + mymodes = RadioSettingGroup("mymodes", "My Modes") + + top = RadioSettings(gen, cw, pnlcfg, pnlset, voxdat, mic, + mymodes, mybands) + + self._do_general_settings(gen) + self._do_cw_settings(cw) + self._do_panel_buttons(pnlcfg) + self._do_panel_settings(pnlset) + self._do_vox_settings(voxdat) + self._do_mic_settings(mic) + self._do_mymodes_settings(mymodes) + self._do_mybands_settings(mybands) + + return top + + def _do_general_settings(self, tab): + _settings = self._memobj.settings + + rs = RadioSetting("ext_mnu", "Extended menu", + RadioSettingValueBoolean(_settings.ext_mnu)) + rs.set_doc("Enables access to extended settings in the radio") + tab.append(rs) + + rs = RadioSetting("apo", "APO time (Hrs)", + RadioSettingValueInteger(1, 12, _settings.apo)) + tab.append(rs) + + options = ["%i" % i for i in range(0, 21)] + options[0] = "Off" + rs = RadioSetting("tot", "TX 'TOT' time-out (mins)", + RadioSettingValueList(options, + options[_settings.tot])) + tab.append(rs) + + bx = not _settings.cat_rts # Convert from Enable=0 + rs = RadioSetting("cat_rts", "CAT RTS flow control", + RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "cat_rts") + tab.append(rs) + + options = ["0", "100ms", "1000ms", "3000ms"] + rs = RadioSetting("cat_tot", "CAT Timeout", + RadioSettingValueList(options, + options[_settings.cat_tot])) + tab.append(rs) + + options = ["4800", "9600", "19200", "38400", "Data"] + rs = RadioSetting("catrate", "CAT rate", + RadioSettingValueList(options, + options[_settings.catrate])) + tab.append(rs) + + rs = RadioSetting("mem_grp", "Mem groups", + RadioSettingValueBoolean(_settings.mem_grp)) + tab.append(rs) + + rs = RadioSetting("scn_res", "Resume scan (secs)", + RadioSettingValueInteger(0, 10, _settings.scn_res)) + tab.append(rs) + + rs = RadioSetting("clk_sft", "CPU clock shift", + RadioSettingValueBoolean(_settings.clk_sft)) + tab.append(rs) + + rs = RadioSetting("split", "TX/RX Frequency Split", + RadioSettingValueBoolean(_settings.split)) + tab.append(rs) + + rs = RadioSetting("qspl_f", "Quick-Split freq offset (KHz)", + RadioSettingValueInteger(-20, 20, _settings.qspl_f)) + tab.append(rs) + + rs = RadioSetting("emergen", "Alaska Emergency Mem 5167.5KHz", + RadioSettingValueBoolean(_settings.emergen)) + tab.append(rs) + + rs = RadioSetting("stby_beep", "PTT release 'Standby' beep", + RadioSettingValueBoolean(_settings.stby_beep)) + tab.append(rs) + + options = ["ATAS", "EXT ATU", "INT ATU", "INTRATU", "F-TRANS"] + rs = RadioSetting("tuner", "Antenna Tuner", + RadioSettingValueList(options, + options[_settings.tuner])) + tab.append(rs) + + rs = RadioSetting("rfpower", "RF power (watts)", + RadioSettingValueInteger(5, 100, _settings.rfpower)) + tab.append(rs) # End of _do_general_settings + + + def _do_cw_settings(self, cw): # - - - CW - - - + _settings = self._memobj.settings + _beacon = self._memobj.beacontext + + rs = RadioSetting("cw_dly", "CW break-in delay (ms * 10)", + RadioSettingValueInteger(0, 300, _settings.cw_dly)) + cw.append(rs) + + options = ["%i Hz" % i for i in range(400, 801, 100)] + rs = RadioSetting("cwpitch", "CW pitch", + RadioSettingValueList(options, + options[_settings.cwpitch])) + cw.append(rs) + + rs = RadioSetting("cwspeed", "CW speed (wpm)", + RadioSettingValueInteger(4, 60, _settings.cwspeed)) + rs.set_doc("Cpm is Wpm * 5") + cw.append(rs) + + options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)] + rs = RadioSetting("cwweigt", "CW weight", + RadioSettingValueList(options, + options[_settings.cwweigt])) + cw.append(rs) + + options = ["15ms", "20ms", "25ms", "30ms"] + rs = RadioSetting("cw_qsk", "CW delay before TX in QSK mode", + RadioSettingValueList(options, + options[_settings.cw_qsk])) + cw.append(rs) + + rs = RadioSetting("cwstone_sgn", "CW sidetone volume Linked", + RadioSettingValueBoolean(_settings.cwstone_sgn)) + rs.set_doc("If set; volume is relative to AF Gain knob.") + cw.append(rs) + + rs = RadioSetting("cwstone_lnk", "CW sidetone linked volume", + RadioSettingValueInteger(-50, 50, + _settings.cwstone_lnk)) + cw.append(rs) + + rs = RadioSetting("cwstone_fix", "CW sidetone fixed volume", + RadioSettingValueInteger(0, 100, + _settings.cwstone_fix)) + cw.append(rs) + + options = [ "Numeric", "Alpha", "Mixed"] + rs = RadioSetting("cwtrain", "CW Training mode", + RadioSettingValueList(options, + options[_settings.cwtrain])) + cw.append(rs) + + rs = RadioSetting("cw_auto", "CW key jack- auto CW mode", + RadioSettingValueBoolean(_settings.cw_auto)) + rs.set_doc("Enable for CW mode auto-set when keyer pluuged in.") + cw.append(rs) + + options = ["Normal", "Reverse"] + rs = RadioSetting("cw_key", "CW paddle wiring", + RadioSettingValueList(options, + options[_settings.cw_key])) + cw.append(rs) + + rs = RadioSetting("beacon_time", "CW beacon Tx interval (secs)", + RadioSettingValueInteger(0, 255, + _settings.beacon_time)) + cw.append(rs) + + tmp = self._chars2str(_beacon.t1, 40) + rs=RadioSetting("t1", "CW Beacon Line 1", + RadioSettingValueString(0, 40, tmp)) + rs.set_apply_callback(self._my_str2ary, _beacon, "t1", 40) + cw.append(rs) + + tmp = self._chars2str(_beacon.t2, 40) + rs=RadioSetting("t2", "CW Beacon Line 2", + RadioSettingValueString(0, 40, tmp)) + rs.set_apply_callback(self._my_str2ary, _beacon, "t2", 40) + cw.append(rs) + + tmp = self._chars2str(_beacon.t3, 40) + rs=RadioSetting("t3", "CW Beacon Line 3", + RadioSettingValueString(0, 40, tmp)) + rs.set_apply_callback(self._my_str2ary, _beacon, "t3", 40) + cw.append(rs) # END _do_cw_settings + + + def _do_panel_settings(self, pnlset): # - - - Panel settings + _settings = self._memobj.settings + + bx = not _settings.amfmdial # Convert from Enable=0 + rs = RadioSetting("amfmdial", "AM&FM Dial", + RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "amfmdial") + pnlset.append(rs) + + options = ["440Hz", "880Hz", "1760Hz"] + rs = RadioSetting("beepton", "Beep frequency", + RadioSettingValueList(options, + options[_settings.beepton])) + pnlset.append(rs) + + rs = RadioSetting("beepvol_sgn", "Beep volume Linked", + RadioSettingValueBoolean(_settings.beepvol_sgn)) + rs.set_doc("If set; volume is relative to AF Gain knob.") + pnlset.append(rs) + + rs = RadioSetting("beepvol_lnk", "Linked beep volume", + RadioSettingValueInteger(-50, 50, + _settings.beepvol_lnk)) + rs.set_doc("Relative to AF-Gain setting.") + pnlset.append(rs) + + rs = RadioSetting("beepvol_fix", "Fixed beep volume", + RadioSettingValueInteger(0, 100, + _settings.beepvol_fix)) + rs.set_doc("When Linked setting is unchecked.") + pnlset.append(rs) + + rs = RadioSetting("cont", "LCD Contrast", + RadioSettingValueInteger(1, 24, _settings.cont )) + rs.set_doc("This setting does not appear to do anything...") + pnlset.append(rs) + + rs = RadioSetting("dimmer", "LCD Dimmer", + RadioSettingValueInteger(1, 8, _settings.dimmer )) + pnlset.append(rs) + + options = ["RF-Gain", "Squelch"] + rs = RadioSetting("sql_rfg", "Squelch/RF-Gain", + RadioSettingValueList(options, + options[_settings.sql_rfg])) + pnlset.append(rs) + + options = ["Frequencies", "Panel", "All"] + rs = RadioSetting("lockmod", "Lock Mode", + RadioSettingValueList(options, + options[_settings.lockmod])) + pnlset.append(rs) + + options = ["Dial", "SEL"] + rs = RadioSetting("clar_btn", "CLAR button control", + RadioSettingValueList(options, + options[_settings.clar_btn])) + pnlset.append(rs) + + if _settings.dialstp_mode == 0: # AM/FM + options = ["SSB/CW:1Hz", "SSB/CW:10Hz", "SSB/CW:20Hz"] + else: + options = ["AM/FM:100Hz", "AM/FM:200Hz"] + rs = RadioSetting("dialstp", "Dial tuning step", + RadioSettingValueList(options, + options[_settings.dialstp])) + pnlset.append(rs) + + options = ["0.5secs", "1.0secs", "1.5secs", "2.0secs"] + rs = RadioSetting("keyhold", "Buttons hold-to-activate time", + RadioSettingValueList(options, + options[_settings.keyhold])) + pnlset.append(rs) + + rs = RadioSetting("m_tune", "Memory tune", + RadioSettingValueBoolean(_settings.m_tune)) + pnlset.append(rs) + + rs = RadioSetting("peakhold", "S-Meter display hold (1sec)", + RadioSettingValueBoolean(_settings.peakhold)) + pnlset.append(rs) + + options = ["CW Sidetone", "CW Speed", "100KHz step", "1MHz Step", + "Mic Gain", "RF Power"] + rs = RadioSetting("seldial", "SEL dial 2nd function (push)", + RadioSettingValueList(options, + options[_settings.seldial])) + pnlset.append(rs) + # End _do_panel_settings + + def _do_panel_buttons(self, pnlcfg): #- - - Current Panel Config + _settings = self._memobj.settings + + rs = RadioSetting("pnl_cs", "C.S. Function", + RadioSettingValueList(self.FUNC_LIST, + self.FUNC_LIST[_settings.pnl_cs])) + pnlcfg.append(rs) + + rs = RadioSetting("nb", "Noise blanker", + RadioSettingValueBoolean(_settings.nb)) + pnlcfg.append(rs) + + options = ["Auto", "Fast", "Slow", "Auto/Fast", "Auto/Slow", "?5?"] + rs = RadioSetting("agc", "AGC", + RadioSettingValueList(options, + options[_settings.agc])) + pnlcfg.append(rs) + + rs = RadioSetting("keyer", "Keyer", + RadioSettingValueBoolean(_settings.keyer)) + pnlcfg.append(rs) + + rs = RadioSetting("fast", "Fast step", + RadioSettingValueBoolean(_settings.fast)) + pnlcfg.append(rs) + + rs = RadioSetting("lock", "Lock (per Lock Mode)", + RadioSettingValueBoolean(_settings.lock)) + pnlcfg.append(rs) + + options = ["PO", "ALC", "SWR"] + rs = RadioSetting("mtr_mode", "S-Meter mode", + RadioSettingValueList(options, + options[_settings.mtr_mode])) + pnlcfg.append(rs) + # End _do_panel_Buttons + + def _do_vox_settings(self, voxdat): # - - VOX and DATA Settings + _settings = self._memobj.settings + + rs = RadioSetting("vox_dly", "VOX delay (x 100 ms)", + RadioSettingValueInteger(1, 30, _settings.vox_dly)) + voxdat.append(rs) + + rs = RadioSetting("vox_gain", "VOX Gain", + RadioSettingValueInteger(0, 100, + _settings.vox_gain)) + voxdat.append(rs) + + rs = RadioSetting("dig_vox", "Digital VOX Gain", + RadioSettingValueInteger(0, 100, + _settings.dig_vox)) + voxdat.append(rs) + + rs = RadioSetting("d_disp", "User-L/U freq offset (Hz)", + RadioSettingValueInteger(-3000, 30000, + _settings.d_disp, 10)) + voxdat.append(rs) + + options = ["170Hz", "200Hz", "425Hz", "850Hz"] + rs = RadioSetting("rty_sft", "RTTY FSK Freq Shift", + RadioSettingValueList(options, + options[_settings.rty_sft])) + voxdat.append(rs) + + options = ["1275Hz", "2125Hz"] + rs = RadioSetting("rty_ton", "RTTY FSK Mark tone", + RadioSettingValueList(options, + options[_settings.rty_ton])) + voxdat.append(rs) + + options = ["Normal", "Reverse"] + rs = RadioSetting("rtyrpol", "RTTY Mark/Space RX polarity", + RadioSettingValueList(options, + options[_settings.rtyrpol])) + voxdat.append(rs) + + rs = RadioSetting("rtytpol", "RTTY Mark/Space TX polarity", + RadioSettingValueList(options, + options[_settings.rtytpol])) + voxdat.append(rs) + # End _do_vox_settings + + def _do_mic_settings(self, mic): # - - MIC Settings + _settings = self._memobj.settings + + rs = RadioSetting("mic_eq", "Mic Equalizer", + RadioSettingValueInteger(0, 9, _settings.mic_eq)) + mic.append(rs) + + options = ["Low", "Normal", "High"] + rs = RadioSetting("micgain", "Mic Gain", + RadioSettingValueList(options, + options[_settings.micgain])) + mic.append(rs) + + rs = RadioSetting("micscan", "Mic scan enabled", + RadioSettingValueBoolean(_settings.micscan)) + rs.set_doc("Enables channel scanning via mic up/down buttons.") + mic.append(rs) + + rs = RadioSetting("pm_dwn", "Mic Down button function", + RadioSettingValueList(self.FUNC_LIST, + self.FUNC_LIST[_settings.pm_dwn])) + mic.append(rs) + + rs = RadioSetting("pm_fst", "Mic Fast button function", + RadioSettingValueList(self.FUNC_LIST, + self.FUNC_LIST[_settings.pm_fst])) + mic.append(rs) + + rs = RadioSetting("pm_up", "Mic Up button function", + RadioSettingValueList(self.FUNC_LIST, + self.FUNC_LIST[_settings.pm_up])) + mic.append(rs) + # End _do_mic_settings + + def _do_mymodes_settings(self, mymodes): # - - MYMODES + _settings = self._memobj.settings # Inverted Logic requires callback + + bx = not _settings.mym_lsb + rs = RadioSetting("mym_lsb", "LSB", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "mym_lsb") + mymodes.append(rs) + + bx = not _settings.mym_usb + rs = RadioSetting("mym_usb", "USB", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "mym_usb") + mymodes.append(rs) + + bx = not _settings.mym_cw + rs = RadioSetting("mym_cw", "CW", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "mym_cw") + mymodes.append(rs) + + bx = not _settings.mym_am + rs = RadioSetting("mym_am", "AM", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "mym_am") + mymodes.append(rs) + + bx = not _settings.mym_fm + rs = RadioSetting("mym_fm", "FM", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "mym_fm") + mymodes.append(rs) + + bx = not _settings.mym_data + rs = RadioSetting("mym_data", "DATA", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "mym_data") + mymodes.append(rs) + # End _do_mymodes_settings + + def _do_mybands_settings(self, mybands): # - - MYBANDS Settings + _settings = self._memobj.settings # Inverted Logic requires callback + + bx = not _settings.myb_1_8 + rs = RadioSetting("myb_1_8", "1.8 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_1_8") + mybands.append(rs) + + bx = not _settings.myb_3_5 + rs = RadioSetting("myb_3_5", "3.5 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_3_5") + mybands.append(rs) + + bx = not _settings.myb_7 + rs = RadioSetting("myb_7", "7 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_7") + mybands.append(rs) + + bx = not _settings.myb_10 + rs = RadioSetting("myb_10", "10 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_10") + mybands.append(rs) + + bx = not _settings.myb_14 + rs = RadioSetting("myb_14", "14 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_14") + mybands.append(rs) + + bx = not _settings.myb_18 + rs = RadioSetting("myb_18", "18 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_18") + mybands.append(rs) + + bx = not _settings.myb_21 + rs = RadioSetting("myb_21", "21 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_21") + mybands.append(rs) + + bx = not _settings.myb_24 + rs = RadioSetting("myb_24", "24 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_24") + mybands.append(rs) + + bx = not _settings.myb_28 + rs = RadioSetting("myb_28", "28 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_28") + mybands.append(rs) + + bx = not _settings.myb_50 + rs = RadioSetting("myb_50", "50 MHz", RadioSettingValueBoolean(bx)) + rs.set_apply_callback(self._invert_me, _settings, "myb_50") + mybands.append(rs) + # End _do_mybands_settings + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[: -1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/ft50.py b/chirp/drivers/ft50.py new file mode 100644 index 0000000..9f28110 --- /dev/null +++ b/chirp/drivers/ft50.py @@ -0,0 +1,641 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import time +import logging +import re + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, errors, bitwise, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct flag_struct { + u8 unknown1f:5, + skip:1, + mask:1, + used:1; +}; + +struct mem_struct { + u8 showname:1, + unknown1:3, + unknown2:2, + unknown3:2; + u8 ishighpower:1, + power:2, + unknown4:1, + tuning_step:4; + u8 codememno:4, + codeorpage:2, + duplex:2; + u8 tmode:2, + tone:6; + u8 unknown5:1, + dtcs:7; + u8 unknown6:6, + mode:2; + bbcd freq[3]; + bbcd offset[3]; + u8 name[4]; +}; + +#seekto 0x000C; +struct { + u8 extendedrx_flg; // Seems to be set to 03 when extended rx is enabled + u8 extendedrx; // Seems to be set to 01 when extended rx is enabled +} extendedrx_struct; // UNFINISHED!! + +#seekto 0x001A; +struct flag_struct flag[100]; + +#seekto 0x079C; +struct flag_struct flag_repeat[100]; + +#seekto 0x00AA; +struct mem_struct memory[100]; +struct mem_struct special[11]; + +#seekto 0x08C7; +struct { + u8 sub_display; + u8 unknown1s; + u8 apo; + u8 timeout; + u8 lock; + u8 rxsave; + u8 lamp; + u8 bell; + u8 cwid[16]; + u8 unknown2s; + u8 artsmode; + u8 artsbeep; + u8 unknown3s; + u8 unknown4s; + struct { + u8 header[3]; + u8 mem_num; + u8 digits[16]; + } autodial[8]; + struct { + u8 header[3]; + u8 mem_num; + u8 digits[32]; + } autodial9_ro; + bbcd pagingcodec_ro[2]; + bbcd pagingcodep[2]; + struct { + bbcd digits[2]; + } pagingcode[6]; + u8 code_dec_c_en:1, + code_dec_p_en:1, + code_dec_1_en:1, + code_dec_2_en:1, + code_dec_3_en:1, + code_dec_4_en:1, + code_dec_5_en:1, + code_dec_6_en:1; + u8 pagingspeed; + u8 pagingdelay; + u8 pagingbell; + u8 paginganswer; + + #seekto 0x0E30; + u8 squelch; // squelch + u8 unknown0c; + u8 rptl:1, // repeater input tracking + amod:1, // auto mode + scnl:1, // scan lamp + resm:1, // scan resume mode 0=5sec, 1=carr + ars:1, // automatic repeater shift + keybeep:1, // keypad beep + lck:1, // lock + unknown1c:1; + u8 lgt:1, + pageamsg:1, + unknown2c:1, + bclo:1, // Busy channel lock out + unknown3c:2, + cwid_en:1, // CWID off/on + tsav:1; // TX save + u8 unknown4c:4, + artssped:1, // ARTS/SPED: 0=15s, 1=25s + unknown5c:1, + rvhm:1, // RVHM: 0=home, 1=rev + mon:1; // MON: 0=mon, 1=tcal +} settings; + +#seekto 0x080E; +struct mem_struct vfo_mem[10]; + + +""" + +# 10 VFO memories: A145, A220, A380, A430, A800, +# B145, B220, B380, B430, B800 + +DUPLEX = ["", "-", "+"] +MODES = ["FM", "AM", "WFM"] +SKIP_VALUES = ["", "S"] +TMODES = ["", "Tone", "TSQL", "DTCS"] +TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] +TONES = list(chirp_common.OLD_TONES) + +# CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ()+-=*/???|0123456789" +# the = displays as an underscored dash on radio +# the first ? is an uppercase delta - \xA7 +# the second ? is an uppercase gamma - \xD1 +# the thrid ? is an uppercase sigma - \xCF +NUMERIC_CHARSET = list("0123456789") +CHARSET = [str(x) for x in range(0, 10)] + \ + [chr(x) for x in range(ord("A"), ord("Z")+1)] + \ + list(" ()+-=*/" + ("\x00" * 3) + "|") + NUMERIC_CHARSET +DTMFCHARSET = NUMERIC_CHARSET + list("ABCD*#") + +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.0), + chirp_common.PowerLevel("L3", watts=2.5), + chirp_common.PowerLevel("L2", watts=1.0), + chirp_common.PowerLevel("L1", watts=0.1)] +SPECIALS = ["L1", "U1", "L2", "U2", "L3", "U3", "L4", "U4", "L5", "U5", "UNK"] + + +@directory.register +class FT50Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FT-50""" + BAUD_RATE = 9600 + VENDOR = "Yaesu" + MODEL = "FT-50" + + _model = "" + _memsize = 3723 + _block_lengths = [10, 16, 112, 16, 16, 1776, 1776, 1] + # _block_delay = 0.15 + _block_size = 8 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold [PTT] & Knob while turning the + radio on. +4. After clicking OK, press the [PTT] switch to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold [PTT] & Knob while turning the + radio on. +4. Press the [MONI] switch ("WAIT" will appear on the LCD). +5. Press OK.""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 100) + rf.valid_duplexes = DUPLEX + rf.valid_tmodes = TMODES + rf.valid_power_levels = POWER_LEVELS + rf.valid_tuning_steps = TUNING_STEPS + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 4 + rf.valid_modes = MODES + # Specials not yet implementd + # rf.valid_special_chans = SPECIALS + rf.valid_bands = [(76000000, 200000000), + (300000000, 540000000), + (590000000, 999000000)] + # rf.can_odd_split = True + rf.has_ctone = False + rf.has_bank = False + rf.has_settings = True + rf.has_dtcs_polarity = False + + return rf + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0xE89)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def get_memory(self, number): + mem = chirp_common.Memory() + _mem = self._memobj.memory[number-1] + _flg = self._memobj.flag[number-1] + mem.number = number + + # if not _flg.visible: + # mem.empty = True + if not _flg.used: + mem.empty = True + return mem + + for i in _mem.name: + mem.name += CHARSET[i & 0x7F] + mem.name = mem.name.rstrip() + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.duplex = DUPLEX[_mem.duplex] + mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000) + mem.rtone = mem.ctone = TONES[_mem.tone] + mem.tmode = TMODES[_mem.tmode] + mem.mode = MODES[_mem.mode] + mem.tuning_step = TUNING_STEPS[_mem.tuning_step] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + # Power is stored as 2 bits to describe the 3 low power levels + # High power is determined by a different bit. + if not _mem.ishighpower: + mem.power = POWER_LEVELS[3 - _mem.power] + else: + mem.power = POWER_LEVELS[0] + mem.skip = SKIP_VALUES[_flg.skip] + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + _flg = self._memobj.flag[mem.number-1] + _flg_repeat = self._memobj.flag_repeat[mem.number-1] + + if mem.empty: + _flg.used = False + return + + if (len(mem.name) == 0): + _mem.name = [0x24] * 4 + _mem.showname = 0 + else: + _mem.showname = 1 + for i in range(0, 4): + _mem.name[i] = CHARSET.index(mem.name.ljust(4)[i]) + + _mem.freq = int(mem.freq / 1000) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.offset = int(mem.offset / 1000) + _mem.mode = MODES.index(mem.mode) + _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step) + if mem.power: + if (mem.power == POWER_LEVELS[0]): + # low power level is not changed when high power is selected + _mem.ishighpower = 0x01 + if (_mem.power == 3): + # Set low power to L3 (0x02) if it is + # set to 3 (new object default) + LOG.debug("SETTING DEFAULT?") + _mem.power = 0x02 + else: + _mem.ishighpower = 0x00 + _mem.power = 3 - POWER_LEVELS.index(mem.power) + else: + _mem.ishighpower = 0x01 + _mem.power = 0x02 + _mem.tmode = TMODES.index(mem.tmode) + try: + _mem.tone = TONES.index(mem.rtone) + except ValueError: + raise errors.UnsupportedToneError( + ("This radio does not support tone %s" % mem.rtone)) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + + _flg.skip = SKIP_VALUES.index(mem.skip) + + # initialize new channel to safe defaults + if not mem.empty and not _flg.used: + _flg.used = True + _flg.mask = True # Mask = True to be visible on radio + _mem.unknown1 = 0x00 + _mem.unknown2 = 0x00 + _mem.unknown3 = 0x00 + _mem.unknown4 = 0x00 + _mem.unknown5 = 0x00 + _mem.unknown6 = 0x00 + _mem.codememno = 0x02 # Not implemented in chirp + _mem.codeorpage = 0x00 # Not implemented in chirp + + # Duplicate flags to repeated part in memory + _flg_repeat.skip = _flg.skip + _flg_repeat.mask = _flg.mask + _flg_repeat.used = _flg.used + + def _decode_cwid(self, inarr): + LOG.debug("@_decode_chars, type: %s" % type(inarr)) + LOG.debug(inarr) + outstr = "" + for i in inarr: + if i == 0xFF: + break + outstr += CHARSET[i & 0x7F] + LOG.debug(outstr) + return outstr.rstrip() + + def _encode_cwid(self, instr, length=16): + LOG.debug("@_encode_chars, type: %s" % type(instr)) + LOG.debug(instr) + outarr = [] + instr = str(instr) + for i in range(0, length): + if i < len(instr): + outarr.append(CHARSET.index(instr[i])) + else: + outarr.append(0xFF) + return outarr + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + dtmf = RadioSettingGroup("dtmf", "DTMF Code & Paging") + arts = RadioSettingGroup("arts", "ARTS") + autodial = RadioSettingGroup("autodial", "AutoDial") + top = RadioSettings(basic, autodial, arts, dtmf) + + rs = RadioSetting( + "squelch", "Squelch", + RadioSettingValueInteger(0, 15, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting( + "keybeep", "Keypad Beep", + RadioSettingValueBoolean(_settings.keybeep)) + basic.append(rs) + + rs = RadioSetting( + "scnl", "Scan Lamp", + RadioSettingValueBoolean(_settings.scnl)) + basic.append(rs) + + options = ["off", "30m", "1h", "3h", "5h", "8h"] + rs = RadioSetting( + "apo", "APO time (hrs)", + RadioSettingValueList(options, options[_settings.apo])) + basic.append(rs) + + options = ["off", "1m", "2.5m", "5m", "10m"] + rs = RadioSetting( + "timeout", "Time Out Timer", + RadioSettingValueList(options, options[_settings.timeout])) + basic.append(rs) + + options = ["key", "dial", "key+dial", "ptt", + "key+ptt", "dial+ptt", "all"] + rs = RadioSetting( + "lock", "Lock mode", + RadioSettingValueList(options, options[_settings.lock])) + basic.append(rs) + + options = ["off", "0.2", "0.3", "0.5", "1.0", "2.0"] + rs = RadioSetting( + "rxsave", "RX Save (sec)", + RadioSettingValueList(options, options[_settings.rxsave])) + basic.append(rs) + + options = ["5sec", "key", "tgl"] + rs = RadioSetting( + "lamp", "Lamp mode", + RadioSettingValueList(options, options[_settings.lamp])) + basic.append(rs) + + options = ["off", "1", "3", "5", "8", "rpt"] + rs = RadioSetting( + "bell", "Bell Repetitions", + RadioSettingValueList(options, options[_settings.bell])) + basic.append(rs) + + rs = RadioSetting( + "cwid_en", "CWID Enable", + RadioSettingValueBoolean(_settings.cwid_en)) + arts.append(rs) + + cwid = RadioSettingValueString( + 0, 16, self._decode_cwid(_settings.cwid.get_value())) + cwid.set_charset(CHARSET) + rs = RadioSetting("cwid", "CWID", cwid) + arts.append(rs) + + options = ["off", "rx", "tx", "trx"] + rs = RadioSetting( + "artsmode", "ARTS Mode", + RadioSettingValueList( + options, options[_settings.artsmode])) + arts.append(rs) + + options = ["off", "in range", "always"] + rs = RadioSetting( + "artsbeep", "ARTS Beep", + RadioSettingValueList(options, options[_settings.artsbeep])) + arts.append(rs) + + for i in range(0, 8): + dialsettings = _settings.autodial[i] + dialstr = "" + for c in dialsettings.digits: + if c < len(DTMFCHARSET): + dialstr += DTMFCHARSET[c] + dialentry = RadioSettingValueString(0, 16, dialstr) + dialentry.set_charset(DTMFCHARSET + list(" ")) + rs = RadioSetting("autodial" + str(i+1), + "AutoDial " + str(i+1), dialentry) + autodial.append(rs) + + dialstr = "" + for c in _settings.autodial9_ro.digits: + if c < len(DTMFCHARSET): + dialstr += DTMFCHARSET[c] + dialentry = RadioSettingValueString(0, 32, dialstr) + dialentry.set_mutable(False) + rs = RadioSetting("autodial9_ro", "AutoDial 9 (read only)", dialentry) + autodial.append(rs) + + options = ["50ms", "100ms"] + rs = RadioSetting( + "pagingspeed", "Paging Speed", + RadioSettingValueList(options, options[_settings.pagingspeed])) + dtmf.append(rs) + + options = ["250ms", "450ms", "750ms", "1000ms"] + rs = RadioSetting( + "pagingdelay", "Paging Delay", + RadioSettingValueList(options, options[_settings.pagingdelay])) + dtmf.append(rs) + + options = ["off", "1", "3", "5", "8", "rpt"] + rs = RadioSetting( + "pagingbell", "Paging Bell Repetitions", + RadioSettingValueList(options, options[_settings.pagingbell])) + dtmf.append(rs) + + options = ["off", "ans", "for"] + rs = RadioSetting( + "paginganswer", "Paging Answerback", + RadioSettingValueList(options, + options[_settings.paginganswer])) + dtmf.append(rs) + + rs = RadioSetting( + "code_dec_c_en", "Paging Code C Decode Enable", + RadioSettingValueBoolean(_settings.code_dec_c_en)) + dtmf.append(rs) + + _str = str(bitwise.bcd_to_int(_settings.pagingcodec_ro)) + code = RadioSettingValueString(0, 3, _str) + code.set_charset(NUMERIC_CHARSET + list(" ")) + code.set_mutable(False) + rs = RadioSetting("pagingcodec_ro", "Paging Code C (read only)", code) + dtmf.append(rs) + + rs = RadioSetting( + "code_dec_p_en", "Paging Code P Decode Enable", + RadioSettingValueBoolean(_settings.code_dec_p_en)) + dtmf.append(rs) + + _str = str(bitwise.bcd_to_int(_settings.pagingcodep)) + code = RadioSettingValueString(0, 3, _str) + code.set_charset(NUMERIC_CHARSET + list(" ")) + rs = RadioSetting("pagingcodep", "Paging Code P", code) + dtmf.append(rs) + + for i in range(0, 6): + num = str(i+1) + name = "code_dec_" + num + "_en" + rs = RadioSetting( + name, "Paging Code " + num + " Decode Enable", + RadioSettingValueBoolean(getattr(_settings, name))) + dtmf.append(rs) + + _str = str(bitwise.bcd_to_int(_settings.pagingcode[i].digits)) + code = RadioSettingValueString(0, 3, _str) + code.set_charset(NUMERIC_CHARSET + list(" ")) + rs = RadioSetting("pagingcode" + num, "Paging Code " + num, code) + dtmf.append(rs) + + return top + + def set_settings(self, uisettings): + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + _settings = self._memobj.settings + if re.match('autodial\d', setting): + # set autodial fields + dtmfstr = str(element.value).strip() + newval = [] + for i in range(0, 16): + if i < len(dtmfstr): + newval.append(DTMFCHARSET.index(dtmfstr[i])) + else: + newval.append(0xFF) + LOG.debug(newval) + idx = int(setting[-1:]) - 1 + _settings = self._memobj.settings.autodial[idx] + _settings.digits = newval + continue + if (setting == "pagingcodep"): + bitwise.int_to_bcd(_settings.pagingcodep, + int(element.value)) + continue + if re.match('pagingcode\d', setting): + idx = int(setting[-1:]) - 1 + bitwise.int_to_bcd(_settings.pagingcode[idx].digits, + int(element.value)) + continue + newval = element.value + oldval = getattr(_settings, setting) + if setting == "cwid": + newval = self._encode_cwid(newval) + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def sync_out(self): + self.update_checksums() + return _clone_out(self) + + +def _clone_out(radio): + try: + return __clone_out(radio) + except Exception, e: + raise errors.RadioError("Failed to communicate with the radio: %s" % e) + + +def __clone_out(radio): + pipe = radio.pipe + block_lengths = radio._block_lengths + total_written = 0 + + def _status(): + status = chirp_common.Status() + status.msg = "Cloning to radio" + status.max = sum(block_lengths) + status.cur = total_written + radio.status_fn(status) + + start = time.time() + + blocks = 0 + pos = 0 + for block in radio._block_lengths: + blocks += 1 + data = radio.get_mmap()[pos:pos + block] + # LOG.debug(util.hexprint(data)) + + recvd = "" + # Radio echos every block received + for byte in data: + time.sleep(0.01) + pipe.write(byte) + # flush & sleep so don't loose ack + pipe.flush() + time.sleep(0.015) + recvd += pipe.read(1) # chew the echo + # LOG.debug(util.hexprint(recvd)) + LOG.debug("Bytes sent: %i" % len(data)) + + # Radio does not ack last block + if (blocks < 8): + buf = pipe.read(block) + LOG.debug("ACK attempt: " + util.hexprint(buf)) + if buf and buf[0] != chr(yaesu_clone.CMD_ACK): + buf = pipe.read(block) + if not buf or buf[-1] != chr(yaesu_clone.CMD_ACK): + raise errors.RadioError("Radio did not ack block %i" % blocks) + + total_written += len(data) + _status() + pos += block + + pipe.read(pos) # Chew the echo if using a 2-pin cable + + LOG.debug("Clone completed in %i seconds" % (time.time() - start)) diff --git a/chirp/drivers/ft60.py b/chirp/drivers/ft60.py new file mode 100644 index 0000000..3c00d84 --- /dev/null +++ b/chirp/drivers/ft60.py @@ -0,0 +1,828 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import logging + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, memmap, bitwise, directory, errors +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +ACK = "\x06" + + +def _send(pipe, data): + pipe.write(data) + echo = pipe.read(len(data)) + if echo != data: + raise errors.RadioError("Error reading echo (Bad cable?)") + + +def _download(radio): + data = "" + for i in range(0, 10): + chunk = radio.pipe.read(8) + if len(chunk) == 8: + data += chunk + break + elif chunk: + raise Exception("Received invalid response from radio") + time.sleep(1) + LOG.info("Trying again...") + + if not data: + raise Exception("Radio is not responding") + + _send(radio.pipe, ACK) + + for i in range(0, 448): + chunk = radio.pipe.read(64) + data += chunk + _send(radio.pipe, ACK) + if len(chunk) == 1 and i == 447: + break + elif len(chunk) != 64: + raise Exception("Reading block %i was short (%i)" % + (i, len(chunk))) + if radio.status_fn: + status = chirp_common.Status() + status.cur = i * 64 + status.max = radio.get_memsize() + status.msg = "Cloning from radio" + radio.status_fn(status) + + return memmap.MemoryMap(data) + + +def _upload(radio): + _send(radio.pipe, radio.get_mmap()[0:8]) + + ack = radio.pipe.read(1) + if ack != ACK: + raise Exception("Radio did not respond") + + for i in range(0, 448): + offset = 8 + (i * 64) + _send(radio.pipe, radio.get_mmap()[offset:offset + 64]) + ack = radio.pipe.read(1) + if ack != ACK: + raise Exception(_("Radio did not ack block %i") % i) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = offset + 64 + status.max = radio.get_memsize() + status.msg = "Cloning to radio" + radio.status_fn(status) + + +def _decode_freq(freqraw): + freq = int(freqraw) * 10000 + if freq > 8000000000: + freq = (freq - 8000000000) + 5000 + + if freq > 4000000000: + freq -= 4000000000 + for i in range(0, 3): + freq += 2500 + if chirp_common.required_step(freq) == 12.5: + break + + return freq + + +def _encode_freq(freq): + freqraw = freq / 10000 + flags = 0x00 + if ((freq / 1000) % 10) >= 5: + flags += 0x80 + if chirp_common.is_fractional_step(freq): + flags += 0x40 + return freqraw, flags + + +def _decode_name(mem): + name = "" + for i in mem: + if i == 0xFF: + break + try: + name += CHARSET[i] + except IndexError: + LOG.error("Unknown char index: %i " % (i)) + return name + + +def _encode_name(mem): + name = [None] * 6 + for i in range(0, 6): + try: + name[i] = CHARSET.index(mem[i]) + except IndexError: + name[i] = CHARSET.index(" ") + + return name + + +MEM_FORMAT = """ +#seekto 0x0024; +struct { + u8 apo; + u8 x25:3, + tot:5; + u8 x26; + u8 x27; + u8 x28:4, + rf_sql:4; + u8 x29:4, + int_cd:4; + u8 x2A:4, + int_mr:4; + u8 x2B:5, + lock:3; + u8 x2C:5, + dt_dly:3; + u8 x2D:7, + dt_spd:1; + u8 ar_bep; + u8 x2F:6, + lamp:2; + u8 x30:5, + bell:3; + u8 x31:5, + rxsave:3; + u8 x32; + u8 x33; + u8 x34; + u8 x35; + u8 x36; + u8 x37; + u8 wx_alt:1, + x38_1:3, + ar_int:1, + x38_5:3; + u8 x39:3, + ars:1, + vfo_bnd:1, + dcs_nr:2, + ssrch:1; + u8 pri_rvt:1, + x3A_1:1, + beep_sc:1, + edg_bep:1, + beep_key:1, + inet:2, + x3A_7:1; + u8 x3B_0:5, + scn_md:1, + x3B_6:2; + u8 x3C_0:2, + rev_hm:1, + mt_cl:1 + resume:2, + txsave:1, + pag_abk:1; + u8 x3D_0:1, + scn_lmp:1, + x3D_2:1, + bsy_led:1, + x3D_4:1, + tx_led:1, + x3D_6:2; + u8 x3E_0:2, + bclo:1, + x3E_3:5; +} settings; + +#seekto 0x09E; +ul16 mbs; + +#seekto 0x0C8; +struct { + u8 memory[16]; +} dtmf[9]; + +struct mem { + u8 used:1, + unknown1:1, + isnarrow:1, + isam:1, + duplex:4; + bbcd freq[3]; + u8 unknown2:1, + step:3, + unknown2_1:1, + tmode:3; + bbcd tx_freq[3]; + u8 power:2, + tone:6; + u8 unknown4:1, + dtcs:7; + u8 unknown5; + u16 unknown5_1:1 + offset:15; + u8 unknown6[3]; +}; + +#seekto 0x0248; +struct mem memory[1000]; + +#seekto 0x40c8; +struct mem pms[100]; + +#seekto 0x6EC8; +// skips:2 for Memory M in [1, 1000] is in flags[(M-1)/4].skip((M-1)%4). +// Interpret with SKIPS[]. +// PMS memories L0 - U50 aka memory 1001 - 1100 don't have skip flags. +struct { + u8 skip3:2, + skip2:2, + skip1:2, + skip0:2; +} flags[250]; + +#seekto 0x4708; +struct { + u8 name[6]; + u8 use_name:1, + unknown1:7; + u8 valid:1, + unknown2:7; +} names[1000]; + +#seekto 0x69C8; +struct { + bbcd memory[128]; +} banks[10]; + +#seekto 0x6FC8; +u8 checksum; +""" + +DUPLEX = ["", "", "-", "+", "split", "off"] +TMODES = ["", "Tone", "TSQL", "TSQL-R", "DTCS"] +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.0), + chirp_common.PowerLevel("Mid", watts=2.0), + chirp_common.PowerLevel("Low", watts=0.5)] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +SKIPS = ["", "S", "P"] +DTMF_CHARS = list("0123456789ABCD*#") +CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ [?]^__|`?$%&-()*+,-,/|;/=>?@" +SPECIALS = ["%s%d" % (c, i + 1) for i in range(0, 50) for c in ('L', 'U')] + + +class FT60BankModel(chirp_common.BankModel): + + def get_num_mappings(self): + return 10 + + def get_mappings(self): + banks = [] + for i in range(0, self.get_num_mappings()): + bank = chirp_common.Bank(self, "%i" % (i + 1), "Bank %i" % (i + 1)) + bank.index = i + banks.append(bank) + return banks + + def add_memory_to_mapping(self, memory, bank): + number = (memory.number - 1) / 8 + mask = 1 << ((memory.number - 1) & 7) + self._radio._memobj.banks[bank.index].memory[number].set_bits(mask) + + def remove_memory_from_mapping(self, memory, bank): + number = (memory.number - 1) / 8 + mask = 1 << ((memory.number - 1) & 7) + m = self._radio._memobj.banks[bank.index].memory[number] + if m.get_bits(mask) != mask: + raise Exception("Memory %i is not in bank %s." % + (memory.number, bank)) + self._radio._memobj.banks[bank.index].memory[number].clr_bits(mask) + + def get_mapping_memories(self, bank): + memories = [] + for i in range(*self._radio.get_features().memory_bounds): + number = (i - 1) / 8 + mask = 1 << ((i - 1) & 7) + m = self._radio._memobj.banks[bank.index].memory[number] + if m.get_bits(mask) == mask: + memories.append(self._radio.get_memory(i)) + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + number = (memory.number - 1) / 8 + mask = 1 << ((memory.number - 1) & 7) + m = self._radio._memobj.banks[bank.index].memory[number] + if m.get_bits(mask) == mask: + banks.append(bank) + return banks + + +@directory.register +class FT60Radio(yaesu_clone.YaesuCloneModeRadio): + + """Yaesu FT-60""" + BAUD_RATE = 9600 + VENDOR = "Yaesu" + MODEL = "FT-60" + _model = "AH017" + + _memsize = 28617 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [MONI] switch while turning the + radio on. +4. Rotate the DIAL job to select "F8 CLONE". +5. Press the [F/W] key momentarily. +6. After clicking OK, hold the [PTT] switch + for one second to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [MONI] switch while turning the + radio on. +4. Rotate the DIAL job to select "F8 CLONE". +5. Press the [F/W] key momentarily. +6. Press the [MONI] switch ("--RX--" will appear on the LCD).""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 1000) + rf.valid_duplexes = DUPLEX[1:] + rf.valid_tmodes = TMODES + rf.valid_power_levels = POWER_LEVELS + rf.valid_tuning_steps = STEPS + rf.valid_skips = SKIPS + rf.valid_special_chans = SPECIALS + rf.valid_characters = CHARSET + rf.valid_name_length = 6 + rf.valid_modes = ["FM", "NFM", "AM"] + rf.valid_bands = [(108000000, 520000000), (700000000, 999990000)] + rf.can_odd_split = True + rf.has_ctone = False + rf.has_bank = True + rf.has_settings = True + rf.has_dtcs_polarity = False + + return rf + + def get_bank_model(self): + return FT60BankModel(self) + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x6FC7)] + + def sync_in(self): + try: + self._mmap = _download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + self.check_checksums() + + def sync_out(self): + self.update_checksums() + try: + _upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_settings(self): + _settings = self._memobj.settings + + repeater = RadioSettingGroup("repeater", "Repeater Settings") + ctcss = RadioSettingGroup("ctcss", "CTCSS/DCS/DTMF Settings") + arts = RadioSettingGroup("arts", "ARTS Settings") + scan = RadioSettingGroup("scan", "Scan Settings") + power = RadioSettingGroup("power", "Power Saver Settings") + wires = RadioSettingGroup("wires", "WiRES(tm) Settings") + eai = RadioSettingGroup("eai", "EAI/EPCS Settings") + switch = RadioSettingGroup("switch", "Switch/Knob Settings") + misc = RadioSettingGroup("misc", "Miscellaneous Settings") + mbls = RadioSettingGroup("banks", "Memory Bank Link Scan") + + setmode = RadioSettings(repeater, ctcss, arts, scan, power, + wires, eai, switch, misc, mbls) + + # APO + opts = ["OFF"] + ["%0.1f" % (x * 0.5) for x in range(1, 24 + 1)] + misc.append( + RadioSetting( + "apo", "Automatic Power Off", + RadioSettingValueList(opts, opts[_settings.apo]))) + + # AR.BEP + opts = ["OFF", "INRANG", "ALWAYS"] + arts.append( + RadioSetting( + "ar_bep", "ARTS Beep", + RadioSettingValueList(opts, opts[_settings.ar_bep]))) + + # AR.INT + opts = ["25 SEC", "15 SEC"] + arts.append( + RadioSetting( + "ar_int", "ARTS Polling Interval", + RadioSettingValueList(opts, opts[_settings.ar_int]))) + + # ARS + opts = ["OFF", "ON"] + repeater.append( + RadioSetting( + "ars", "Automatic Repeater Shift", + RadioSettingValueList(opts, opts[_settings.ars]))) + + # BCLO + opts = ["OFF", "ON"] + misc.append(RadioSetting( + "bclo", "Busy Channel Lock-Out", + RadioSettingValueList(opts, opts[_settings.bclo]))) + + # BEEP + opts = ["OFF", "KEY", "KEY+SC"] + rs = RadioSetting( + "beep_key", "Enable the Beeper", + RadioSettingValueList( + opts, opts[_settings.beep_key + _settings.beep_sc])) + + def apply_beep(s, obj): + setattr(obj, "beep_key", + (int(s.value) & 1) or ((int(s.value) >> 1) & 1)) + setattr(obj, "beep_sc", (int(s.value) >> 1) & 1) + rs.set_apply_callback(apply_beep, self._memobj.settings) + switch.append(rs) + + # BELL + opts = ["OFF", "1T", "3T", "5T", "8T", "CONT"] + ctcss.append(RadioSetting("bell", "Bell Repetitions", + RadioSettingValueList(opts, opts[ + _settings.bell]))) + + # BSY.LED + opts = ["ON", "OFF"] + misc.append(RadioSetting("bsy_led", "Busy LED", + RadioSettingValueList(opts, opts[ + _settings.bsy_led]))) + + # DCS.NR + opts = ["TR/X N", "RX R", "TX R", "T/RX R"] + ctcss.append(RadioSetting("dcs_nr", "\"Inverted\" DCS Code Decoding", + RadioSettingValueList(opts, opts[ + _settings.dcs_nr]))) + + # DT.DLY + opts = ["50 MS", "100 MS", "250 MS", "450 MS", "750 MS", "1000 MS"] + ctcss.append(RadioSetting("dt_dly", "DTMF Autodialer Delay Time", + RadioSettingValueList(opts, opts[ + _settings.dt_dly]))) + + # DT.SPD + opts = ["50 MS", "100 MS"] + ctcss.append(RadioSetting("dt_spd", "DTMF Autodialer Sending Speed", + RadioSettingValueList(opts, opts[ + _settings.dt_spd]))) + + # DT.WRT + for i in range(0, 9): + dtmf = self._memobj.dtmf[i] + str = "" + for c in dtmf.memory: + if c == 0xFF: + break + if c < len(DTMF_CHARS): + str += DTMF_CHARS[c] + val = RadioSettingValueString(0, 16, str, False) + val.set_charset(DTMF_CHARS + list("abcd")) + rs = RadioSetting("dtmf_%i" % i, + "DTMF Autodialer Memory %i" % (i + 1), val) + + def apply_dtmf(s, obj): + str = s.value.get_value().upper().rstrip() + val = [DTMF_CHARS.index(x) for x in str] + for x in range(len(val), 16): + val.append(0xFF) + obj.memory = val + rs.set_apply_callback(apply_dtmf, dtmf) + ctcss.append(rs) + + # EDG.BEP + opts = ["OFF", "ON"] + misc.append(RadioSetting("edg_bep", "Band Edge Beeper", + RadioSettingValueList(opts, opts[ + _settings.edg_bep]))) + + # I.NET + opts = ["OFF", "COD", "MEM"] + rs = RadioSetting("inet", "Internet Link Connection", + RadioSettingValueList( + opts, opts[_settings.inet - 1])) + + def apply_inet(s, obj): + setattr(obj, s.get_name(), int(s.value) + 1) + rs.set_apply_callback(apply_inet, self._memobj.settings) + wires.append(rs) + + # INT.CD + opts = ["CODE 0", "CODE 1", "CODE 2", "CODE 3", "CODE 4", + "CODE 5", "CODE 6", "CODE 7", "CODE 8", "CODE 9", + "CODE A", "CODE B", "CODE C", "CODE D", "CODE E", "CODE F"] + wires.append(RadioSetting("int_cd", "Access Number for WiRES(TM)", + RadioSettingValueList(opts, opts[ + _settings.int_cd]))) + + # INT.MR + opts = ["d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9"] + wires.append(RadioSetting( + "int_mr", "Access Number (DTMF) for Non-WiRES(TM)", + RadioSettingValueList(opts, opts[_settings.int_mr]))) + + # LAMP + opts = ["KEY", "5SEC", "TOGGLE"] + switch.append(RadioSetting("lamp", "Lamp Mode", + RadioSettingValueList(opts, opts[ + _settings.lamp]))) + + # LOCK + opts = ["LK KEY", "LKDIAL", "LK K+D", "LK PTT", + "LP P+K", "LK P+D", "LK ALL"] + rs = RadioSetting("lock", "Control Locking", + RadioSettingValueList( + opts, opts[_settings.lock - 1])) + + def apply_lock(s, obj): + setattr(obj, s.get_name(), int(s.value) + 1) + rs.set_apply_callback(apply_lock, self._memobj.settings) + switch.append(rs) + + # M/T-CL + opts = ["MONI", "T-CALL"] + switch.append(RadioSetting("mt_cl", "MONI Switch Function", + RadioSettingValueList(opts, opts[ + _settings.mt_cl]))) + + # PAG.ABK + opts = ["OFF", "ON"] + eai.append(RadioSetting("pag_abk", "Paging Answer Back", + RadioSettingValueList(opts, opts[ + _settings.pag_abk]))) + + # RESUME + opts = ["TIME", "HOLD", "BUSY"] + scan.append(RadioSetting("resume", "Scan Resume Mode", + RadioSettingValueList(opts, opts[ + _settings.resume]))) + + # REV/HM + opts = ["REV", "HOME"] + switch.append(RadioSetting("rev_hm", "HM/RV Key Function", + RadioSettingValueList(opts, opts[ + _settings.rev_hm]))) + + # RF.SQL + opts = ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6", + "S-7", "S-8", "S-FULL"] + misc.append(RadioSetting("rf_sql", "RF Squelch Threshold", + RadioSettingValueList(opts, opts[ + _settings.rf_sql]))) + + # PRI.RVT + opts = ["OFF", "ON"] + scan.append(RadioSetting("pri_rvt", "Priority Revert", + RadioSettingValueList(opts, opts[ + _settings.pri_rvt]))) + + # RXSAVE + opts = ["OFF", "200 MS", "300 MS", "500 MS", "1 S", "2 S"] + power.append(RadioSetting( + "rxsave", "Receive Mode Batery Savery Interval", + RadioSettingValueList(opts, opts[_settings.rxsave]))) + + # S.SRCH + opts = ["SINGLE", "CONT"] + misc.append(RadioSetting("ssrch", "Smart Search Sweep Mode", + RadioSettingValueList(opts, opts[ + _settings.ssrch]))) + + # SCN.MD + opts = ["MEM", "ONLY"] + scan.append(RadioSetting( + "scn_md", "Memory Scan Channel Selection Mode", + RadioSettingValueList(opts, opts[_settings.scn_md]))) + + # SCN.LMP + opts = ["OFF", "ON"] + scan.append(RadioSetting("scn_lmp", "Scan Lamp", + RadioSettingValueList(opts, opts[ + _settings.scn_lmp]))) + + # TOT + opts = ["OFF"] + ["%dMIN" % (x) for x in range(1, 30 + 1)] + misc.append(RadioSetting("tot", "Timeout Timer", + RadioSettingValueList(opts, opts[ + _settings.tot]))) + + # TX.LED + opts = ["ON", "OFF"] + misc.append(RadioSetting("tx_led", "TX LED", + RadioSettingValueList(opts, opts[ + _settings.tx_led]))) + + # TXSAVE + opts = ["OFF", "ON"] + power.append(RadioSetting("txsave", "Transmitter Battery Saver", + RadioSettingValueList(opts, opts[ + _settings.txsave]))) + + # VFO.BND + opts = ["BAND", "ALL"] + misc.append(RadioSetting("vfo_bnd", "VFO Band Edge Limiting", + RadioSettingValueList(opts, opts[ + _settings.vfo_bnd]))) + + # WX.ALT + opts = ["OFF", "ON"] + scan.append(RadioSetting("wx_alt", "Weather Alert Scan", + RadioSettingValueList(opts, opts[ + _settings.wx_alt]))) + + # MBS + for i in range(0, 10): + opts = ["OFF", "ON"] + mbs = (self._memobj.mbs >> i) & 1 + rs = RadioSetting("mbs%i" % i, "Bank %s Scan" % (i + 1), + RadioSettingValueList(opts, opts[mbs])) + + def apply_mbs(s, index): + if int(s.value): + self._memobj.mbs |= (1 << index) + else: + self._memobj.mbs &= ~(1 << index) + rs.set_apply_callback(apply_mbs, i) + mbls.append(rs) + + return setmode + + def set_settings(self, uisettings): + _settings = self._memobj.settings + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + + try: + name = element.get_name() + value = element.value + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + obj = getattr(_settings, name) + setattr(_settings, name, value) + + LOG.debug("Setting %s: %s" % (name, value)) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + \ + repr(self._memobj.flags[(number - 1) / 4]) + \ + repr(self._memobj.names[number - 1]) + + def get_memory(self, number): + + mem = chirp_common.Memory() + + if isinstance(number, str): + # pms channel + mem.number = 1001 + SPECIALS.index(number) + mem.extd_number = number + mem.immutable = ["number", "extd_number", "name", "skip"] + _mem = self._memobj.pms[mem.number - 1001] + _nam = _skp = None + elif number > 1000: + # pms channel + mem.number = number + mem.extd_number = SPECIALS[number - 1001] + mem.immutable = ["number", "extd_number", "name", "skip"] + _mem = self._memobj.pms[mem.number - 1001] + _nam = _skp = None + else: + mem.number = number + _mem = self._memobj.memory[mem.number - 1] + _nam = self._memobj.names[mem.number - 1] + _skp = self._memobj.flags[(mem.number - 1) / 4] + + if not _mem.used: + mem.empty = True + return mem + + mem.freq = _decode_freq(_mem.freq) + mem.offset = int(_mem.offset) * 50000 + mem.duplex = DUPLEX[_mem.duplex] + if mem.duplex == "split": + if int(_mem.tx_freq) == 0: + mem.duplex = "off" + else: + mem.offset = _decode_freq(_mem.tx_freq) + mem.tmode = TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.power = POWER_LEVELS[_mem.power] + mem.mode = _mem.isam and "AM" or _mem.isnarrow and "NFM" or "FM" + mem.tuning_step = STEPS[_mem.step] + + if _skp is not None: + skip = _skp["skip%i" % ((mem.number - 1) % 4)] + mem.skip = SKIPS[skip] + + if _nam is not None: + if _nam.use_name and _nam.valid: + mem.name = _decode_name(_nam.name).rstrip() + + return mem + + def set_memory(self, mem): + + if mem.number > 1000: + # pms channel + _mem = self._memobj.pms[mem.number - 1001] + _nam = _skp = None + else: + _mem = self._memobj.memory[mem.number - 1] + _nam = self._memobj.names[mem.number - 1] + _skp = self._memobj.flags[(mem.number - 1) / 4] + + assert(_mem) + if mem.empty: + _mem.used = False + return + + if not _mem.used: + _mem.set_raw("\x00" * 16) + _mem.used = 1 + + _mem.freq, flags = _encode_freq(mem.freq) + _mem.freq[0].set_bits(flags) + if mem.duplex == "split": + _mem.tx_freq, flags = _encode_freq(mem.offset) + _mem.tx_freq[0].set_bits(flags) + _mem.offset = 0 + elif mem.duplex == "off": + _mem.tx_freq = 0 + _mem.offset = 0 + else: + _mem.tx_freq = 0 + _mem.offset = mem.offset / 50000 + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.tmode = TMODES.index(mem.tmode) + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.power = mem.power and POWER_LEVELS.index(mem.power) or 0 + _mem.isnarrow = mem.mode == "NFM" + _mem.isam = mem.mode == "AM" + _mem.step = STEPS.index(mem.tuning_step) + + if _skp is not None: + _skp["skip%i" % ((mem.number - 1) % 4)] = SKIPS.index(mem.skip) + + if _nam is not None: + _nam.name = _encode_name(mem.name) + _nam.use_name = mem.name.strip() and True or False + _nam.valid = _nam.use_name diff --git a/chirp/drivers/ft70.py b/chirp/drivers/ft70.py new file mode 100644 index 0000000..20d1000 --- /dev/null +++ b/chirp/drivers/ft70.py @@ -0,0 +1,1191 @@ +# Copyright 2010 Dan Smith +# Copyright 2017 Nicolas Pike +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueList, RadioSettingValueBoolean, \ + InvalidValueError +from textwrap import dedent +import string +LOG = logging.getLogger(__name__) + +# Testing + +# 37 PAG.ABK Turn the pager answer back Function ON/OFF +# 38 PAG.CDR Specify a personal code (receive) +# 39 PAG.CDT Specify a personal code (transmit) +# 47 RX.MOD Select the receive mode. Auto FM AM + +MEM_SETTINGS_FORMAT = """ + +// FT-70DE New Model #5329 +// +// Communications Mode ? AMS,FM DN,DW TX vs RX? +// Mode not currently correctly stored in memories ? - ALL show as FM in memories +// SKIP test/where stored +// Check storage of steps +// Pager settings ? +// Triple check/ understand _memsize and _block_lengths +// Bank name label name size display 6 store 16? padded with 0xFF same for MYCALL and message +// CHIRP mode DIG not supported - is there a CHIRP Fusion mode? Auto? +// Check character set +// Supported Modes ? +// Supported Bands ? +// rf.has_dtcs_polarity = False - think radio supports DTCS polarity +// rf.memory_bounds = (1, 900) - should this be 0? as zero displays as blank +// RPT offsets (stored per band) not included. +// 59 Display radio firmware version info and Radio ID? +// Front Panel settings power etc? +// Banks and VFO? + +// Features Required +// Default AMS and Memory name (in mem extras) to enabled. + +// Bugs +// MYCALL and Opening Message errors if not 10 characters +// Values greater than one sometimes stored as whole bytes, these need to be refactored into bit fields +// to prevent accidental overwriting of adjacent values +// Bank Name length not checked on gui input - but first 6 characters are saved correctly. +// Extended characters entered as bank names on radio are corrupted in Chirp + +// Missing +// 50 SCV.WTH Set the VFO scan frequency range. BAND / ALL - NOT FOUND +// 49 SCM.WTH Set the memory scan frequency range. ALL / BAND - NOT FOUND + +// Radio Questions +// Temp unit C/F not saved by radio, always goes back to C ? +// 44 RF SQL Adjusts the RF Squelch threshold level. OFF / S1 - S9? Default is OFF - Based on RF strength - for AM? How +// is this different from F, Monitor, Dial Squelch? +// Password setting on radio allows letters (DTMF), but letters cannot be entered at the radio's password prompt? +// 49 SCM.WTH Set the memory scan frequency range. ALL / BAND Defaults to ALL Not Band as stated in the manual. + + #seekto 0x049a; + struct { + u8 unknown0:4, + squelch:4; // Squelch F, Monitor, Dial Adjust the Squelch level + } squelch_settings; + + #seekto 0x04ba; + struct { + u8 unknown:3, + scan_resume:5; // 52 SCN.RSM Configure the scan stop mode settings. 2.0 S - 5.0 S - 10.0 S / BUSY / HOLD + u8 unknown1:3, + dw_resume_interval:5; // 22 DW RSM Configure the scan stop mode settings for Dual Receive. 2.0S-10.0S/BUSY/HOLD + u8 unknown2; + u8 unknown3:3, + apo:5; // 02 APO Set the length of time until the transceiver turns off automatically. + u8 unknown4:6, + gm_ring:2; // 24 GM RNG Select the beep option while receiving digital GM info. OFF/IN RNG/ALWAYS + u8 temp_cf; // Placeholder as not found + u8 unknown5; + } first_settings; + + #seekto 0x04ed; + struct { + u8 unknown1:1, + unknown2:1, + unknown3:1, + unknown4:1, + unknown5:1, + unknown6:1, + unknown7:1, + unknown8:1; + } test_bit_field; + + #seekto 0x04c0; + struct { + u8 unknown1:5, + beep_level:3; // 05 BEP.LVL Beep volume setting LEVEL1 - LEVEL4 - LEVEL7 + u8 unknown2:6, + beep_select:2; // 04 BEEP Sets the beep sound function OFF / KEY+SC / KEY + } beep_settings; + + #seekto 0x04ce; + struct { + u8 lcd_dimmer; // 14 DIMMER LCD Dimmer + u8 dtmf_delay; // 18 DT DLY DTMF delay + u8 unknown0[3]; + u8 unknown1:4, + unknown1:4; + u8 lamp; // 28 LAMP Set the duration time of the backlight and keys to be lit + u8 lock; // 30 LOCK Configure the lock mode setting. KEY/DIAL/K+D/PTT/K+P/D+P/ALL + u8 unknown2_1; + u8 mic_gain; // 31 MCGAIN Adjust the microphone gain level + u8 unknown2_3; + u8 dw_interval; // 21 DW INT Set the priority memory ch mon int during Dual RX 0.1S-5.0S-10.0S + u8 ptt_delay; // 42 PTT.DLY Set the PTT delay time. OFF / 20 MS / 50 MS / 100 MS / 200 MS + u8 rx_save; // 48 RX.SAVE Set the battery save time. OFF / 0.2 S - 60.0 S + u8 scan_restart; // 53 SCN.STR Set the scanning restart time. 0.1 S - 2.0 S - 10.0 S + u8 unknown2_5; + u8 unknown2_6; + u8 unknown4[5]; + u8 tot; // 56 TOT Set the transmission timeout timer + u8 unknown5[3]; // 26 + u8 vfo_mode:1, // 60 VFO.MOD Set freq setting range in the VFO mode by DIAL knob. ALL / BAND + unknown7:1, + scan_lamp:1, // 51 SCN.LMP Set the scan lamp ON or OFF when scanning stops On/Off + unknown8:1, + ars:1, // 45 RPT.ARS Turn the ARS function on/off. + dtmf_speed:1, // 20 DT SPD Set DTMF speed + unknown8:1, + dtmf_mode:1; // DTMF Mode set from front panel + u8 busy_led:1, // Not Supported ? + unknown8_2:1, + unknown8_3:1, + bclo:1, // 03 BCLO Turns the busy channel lockout function on/off. + beep_edge:1, // 06 BEP.Edg Sets the beep sound ON or OFF when a band edge is encountered. + unknown8_6:1, + unknown8_7:1, + unknown8_8:1; // 28 + u8 unknown9_1:1, + unknown9_2:1, + unknown9_3:1, + unknown9_4:1, + unknown9_5:1, + password:1, // Placeholder location + home_rev:1, // 26 HOME/REV Select the function of the [HOME/REV] key. + moni:1; // 32 Mon/T-Call Select the function of the [MONI/T-CALL] switch. + u8 gm_interval:4, // 30 // 25 GM INT Set tx interval of digital GM information. OFF / NORMAL / LONG + unknown10:4; + u8 unknown11; + u8 unknown12:1, + unknown12_2:1, + unknown12_3:1, + unknown12_4:1, + home_vfo:1, // 27 HOME->VFO Turn transfer VFO to the Home channel ON or OFF. + unknown12_6:1, + unknown12_7:1, + dw_rt:1; // 32 // 23 DW RVT Turn "Priority Channel Revert" feature ON or OFF during Dual Rx. + u8 unknown33; + u8 unknown34; + u8 unknown35; + u8 unknown36; + u8 unknown37; + u8 unknown38; + u8 unknown39; + u8 unknown40; + u8 unknown41; + u8 unknown42; + u8 unknown43; + u8 unknown44; + u8 unknown45; + u8 prog_key1; // P1 Set Mode Items to the Programmable Key + u8 prog_key2; // P2 Set Mode Items to the Programmable Key + u8 unknown48; + u8 unknown49; + u8 unknown50; + } scan_settings; + + #seekto 0x064b; + struct { + u8 unknown1:1, + unknown2:1, + unknown3:1, + unknown4:1, + vfo_scan_width:1, // Placeholder as not found - 50 SCV.WTH Set the VFO scan frequency range. BAND / ALL + memory_scan_width:1, // Placeholder as not found - 49 SCM.WTH Set the memory scan frequency range. ALL / BAND + unknown7:1, + unknown8:1; + } scan_settings_1; + + #seekto 0x06B6; + struct { + u8 unknown1:3, + volume:5; // # VOL and Dial Adjust the volume level + } scan_settings_2; + + #seekto 0x0690; // Memory or VFO Settings Map? + struct { + u8 unknown[48]; // Array cannot be 64 elements! + u8 unknown1[16]; // Exception: Not implemented for chirp.bitwise.structDataElement + } vfo_info_1; + + #seekto 0x0710; // Backup Memory or VFO Settings Map? + struct { + u8 unknown[48]; + u8 unknown1[16]; + } vfo_backup_info_1; + + #seekto 0x047e; + struct { + u8 unknown1; + u8 flag; + u16 unknown2; + struct { + char padded_string[6]; // 36 OPN.MSG Select MSG then key vm to edit it + } message; + } opening_message; // 36 OPN.MSG Select the Opening Message when transceiver is ON. OFF/MSG/DC + + #seekto 0x094a; // DTMF Memories + struct { + u8 memory[16]; + } dtmf[10]; + + #seekto 0x154a; + struct { + u16 channel[100]; + } bank_members[24]; + + #seekto 0x54a; + struct { + u16 in_use; + } bank_used[24]; + + #seekto 0x0EFE; + struct { + u8 unknown[2]; + u8 name[6]; + u8 unknown1[10]; + } bank_info[24]; + + #seekto 0xCF30; + struct { + u8 unknown0; + u8 unknown1; + u8 unknown2; + u8 unknown3; + u8 unknown4; + u8 unknown5; + u8 unknown6; + u8 digital_popup; // 15 DIG.POP Call sign display pop up time + } digital_settings_more; + + #seekto 0xCF7C; + struct { + u8 unknown0:6, + ams_tx_mode:2; // AMS TX Mode Short Press AMS button AMS TX Mode + u8 unknown1; + u8 unknown2:7, + standby_beep:1; // 07 BEP.STB Standby Beep in the digital C4FM mode. On/Off + u8 unknown3; + u8 unknown4:6, + gm_ring:2; // 24 GM RNG Select beep option while rx digital GM info. OFF/IN RNG/ALWAYS + u8 unknown5; + u8 rx_dg_id; // RX DG-ID Long Press Mode Key, Mode Key to select, Dial + u8 tx_dg_id; // TX DG-ID Long Press Mode Key, Dial + u8 unknown6:7, + vw_mode:1; // 16 DIG VW Turn the VW mode selection ON or OFF + u8 unknown7; + } digital_settings; + + // ^^^ All above referenced U8's have been refactored to minimum number of bits. + + """ + +MEM_FORMAT = """ + #seekto 0x2D4A; + struct { // 32 Bytes per memory entry + u8 display_tag:1, // 0 Display Freq, 1 Display Name + unknown0:1, // Mode if AMS not selected???????? + deviation:1, // 0 Full deviation (Wide), 1 Half deviation (Narrow) + clock_shift:1, // 0 None, 1 CPU clock shifted + unknown1:4; // 1 + u8 mode:2, // FM,AM,WFM only? - check + duplex:2, // Works + tune_step:4; // Works - check all steps? 7 = Auto // 1 + bbcd freq[3]; // Works // 3 + u8 power:2, // Works + unknown2:1, // 0 FM, 1 Digital - If AMS off + ams:1, // 0 AMS off, 1 AMS on ? + tone_mode:4; // Works // 1 + u8 charsetbits[2]; // 2 + char label[6]; // Works - Can only input 6 on screen // 6 + char unknown7[10]; // Rest of label ??? // 10 + bbcd offset[3]; // Works // 3 + u8 unknown5:2, + tone:6; // Works // 1 + u8 unknown6:1, + dcs:7; // Works // 1 + u8 unknown9; + u8 ams_on_dn_vw_fm:2, // AMS DN, AMS VW, AMS FM + unknown8_3:1, + unknown8_4:1, + unknown8_5:1, + unknown8_6:1, + unknown8_7:1, + unknown8_8:1; + u8 unknown10; + } memory[%d]; // DN, VW, FM, AM + // AMS DN, AMS VW, AMS FM + + #seekto 0x280A; + struct { + u8 nosubvfo:1, + unknown:3, + pskip:1, // PSkip (Select?) + skip:1, // Skip memory during scan + used:1, // Memory used + valid:1; // Aways 1? + } flag[%d]; + """ + +MEM_CALLSIGN_FORMAT = """ +#seekto 0x0ced0; + struct { + char callsign[10]; // 63 MYCALL Set the call sign. (up to 10 characters) + u16 charset; // character set ID + } my_call; + """ + +MEM_CHECKSUM_FORMAT = """ + #seekto 0xFECA; + u8 checksum; + """ + +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+", "split"] + +MODES = ["FM", "AM"] + +STEPS = [0, 5, 6.25, 10, 12.5, 15, 20, 25, 50, 100] # 0 = auto +RFSQUELCH = ["OFF", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"] + +SKIPS = ["", "S", "P"] +FT70_DTMF_CHARS = list("0123456789ABCDEF-") + +CHARSET = ["%i" % int(x) for x in range(0, 10)] + \ + [chr(x) for x in range(ord("A"), ord("Z") + 1)] + \ + [" ", ] + \ + [chr(x) for x in range(ord("a"), ord("z") + 1)] + \ + list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100) + +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("Mid", watts=2.00), + chirp_common.PowerLevel("Low", watts=.50)] + + +class FT70Bank(chirp_common.NamedBank): + """A FT70 bank""" + + def get_name(self): + _bank = self._model._radio._memobj.bank_info[self.index] + name = "" + for i in _bank.name: + if i == 0xff: + break + name += chr(i & 0xFF) + return name.rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_info[self.index] + _bank.name = [ord(x) for x in name.ljust(6, chr(0xFF))[:6]] + + +class FT70BankModel(chirp_common.BankModel): + """A FT70 bank model""" + + def __init__(self, radio, name='Banks'): + super(FT70BankModel, self).__init__(radio, name) + + _banks = self._radio._memobj.bank_info + self._bank_mappings = [] + for index, _bank in enumerate(_banks): + bank = FT70Bank(self, "%i" % index, "BANK-%i" % index) + bank.index = index + self._bank_mappings.append(bank) + + def get_num_mappings(self): + return len(self._bank_mappings) + + def get_mappings(self): + return self._bank_mappings + + def _channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xFFFF: + return set() + + _members = self._radio._memobj.bank_members[bank.index] + return set([int(ch) + 1 for ch in _members.channel if ch != 0xFFFF]) + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.bank_members[bank.index] + if len(channels_in_bank) > len(_members.channel): + raise Exception("Too many entries in bank %d" % bank.index) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channel[index] = channel_number - 1 + empty = index + 1 + for index in range(empty, len(_members.channel)): + _members.channel[index] = 0xFFFF + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0x06 + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + if not channels_in_bank: + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0xFFFF + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +@directory.register +class FT70Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FT-70DE""" + BAUD_RATE = 38400 + VENDOR = "Yaesu" + MODEL = "FT-70D" + + _model = "AH51G" + + _memsize = 65227 # 65227 read from dump + _block_lengths = [10, 65217] + _block_size = 32 + _mem_params = (900, # size of memories array + 900, # size of flags array + ) + + _has_vibrate = False + _has_af_dual = True + + _BEEP_SELECT = ("Off", "Key+Scan", "Key") + _OPENING_MESSAGE = ("Off", "DC", "Message") + _MIC_GAIN = ("Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7", "Level 8", "Level 9") + _AMS_TX_MODE = ("TX Auto", "TX DIGITAL", "TX FM") + _VW_MODE = ("On", "Off") + _DIG_POP_UP = ("Off", "2sec", "4sec", "6sec", "8sec", "10sec", "20sec", "30sec", "60sec", "Continuous") + _STANDBY_BEEP = ("On", "Off") + _SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \ + ["Busy", "Hold"] + _SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \ + ["%.1fs" % (0.5 * x) for x in range(2, 21)] + _LAMP_KEY = ["Key %d sec" % x + for x in range(2, 11)] + ["Continuous", "OFF"] + _LCD_DIMMER = ["Level %d" % x for x in range(1, 7)] + _TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)] + _OFF_ON = ("Off", "On") + _ON_OFF = ("On", "Off") + _DTMF_MODE = ("Manual", "Auto") + _DTMF_SPEED = ("50ms", "100ms") + _DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms") + _TEMP_CF = ("Centigrade", "Fahrenheit") + _APO_SELECT = ("Off", "0.5H", "1.0H", "1.5H", "2.0H", "2.5H", "3.0H", "3.5H", "4.0H", "4.5H", "5.0H", + "5.5H", "6.0H", "6.5H", "7.0H", "7.5H", "8.0H", "8.5H", "9.0H", "9.5H", "10.0H", "10.5H", + "11.0H", "11.5H", "12.0H") + _MONI_TCALL = ("Monitor", "Tone-CALL") + _HOME_REV = ("Home", "Reverse") + _LOCK = ("KEY", "DIAL", "Key+Dial", "PTT", "Key+PTT", "Dial+PTT", "ALL") + _PTT_DELAY = ("Off", "20 MS", "50 MS", "100 MS", "200 MS") + _BEEP_LEVEL = ("Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7") + _SET_MODE = ("Level 1", "Level 2", "Level 3", "Level 4", "Level 5", "Level 6", "Level 7") + _RX_SAVE = ("OFF", "0.2s", ".3s", ".4s", ".5s", ".6s", ".7s", ".8s", ".9s", "1.0s", "1.5s", + "2.0s", "2.5s", "3.0s", "3.5s", "4.0s", "4.5s", "5.0s", "5.5s", "6.0s", "6.5s", "7.0s", + "7.5s", "8.0s", "8.5s", "9.0s", "10.0s", "15s", "20s", "25s", "30s", "35s", "40s", "45s", "50s", "55s", + "60s") + _VFO_MODE = ("ALL", "BAND") + _VFO_SCAN_MODE = ("BAND", "ALL") + _MEMORY_SCAN_MODE = ("BAND", "ALL") + + _VOLUME = ["Level %d" % x for x in range(0, 32)] + _SQUELCH = ["Level %d" % x for x in range(0, 16)] + + _DG_ID = ["%d" % x for x in range(0, 100)] + _GM_RING = ("OFF", "IN RING", "AlWAYS") + _GM_INTERVAL = ("LONG", "NORMAL", "OFF") + + _MYCALL_CHR_SET = list(string.uppercase) + list(string.digits) + ['-','/' ] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + + rp.pre_download = _(dedent("""\ + 1. Turn radio on. + 2. Connect cable to DATA terminal. + 3. Unclip battery. + 4. Press and hold in the [AMS] key and power key while clipping the battery back in + ("ADMS" will appear on the display). + 5. After clicking OK, press the [BAND] key.""" + )) + rp.pre_upload = _(dedent("""\ + 1. Turn radio on. + 2. Connect cable to DATA terminal. + 3. Unclip battery. + 4. Press and hold in the [AMS] key and power key while clipping the battery back in + ("ADMS" will appear on the display). + 5. Press the [MODE] key ("-WAIT-" will appear on the LCD). Then click OK""")) + return rp + + def process_mmap(self): + + mem_format = MEM_SETTINGS_FORMAT + MEM_FORMAT + MEM_CALLSIGN_FORMAT + MEM_CHECKSUM_FORMAT + + self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(500000, 999900000)] + rf.valid_skips = SKIPS + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 6 + rf.memory_bounds = (1, 900) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_bank_names = True + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return "\n".join([repr(self._memobj.memory[number - 1]), + repr(self._memobj.flag[number - 1])]) + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)] # The whole file -2 bytes + + @staticmethod + def _add_ff_pad(val, length): + return val.ljust(length, "\xFF")[:length] + + @classmethod + def _strip_ff_pads(cls, messages): + result = [] + for msg_text in messages: + result.append(str(msg_text).rstrip("\xFF")) + return result + + def get_memory(self, number): + flag = self._memobj.flag[number - 1] + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + mem.number = number + if not flag.used: + mem.empty = True + if not flag.valid: + mem.empty = True + return mem + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 1000 + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + self._get_tmode(mem, _mem) + mem.duplex = DUPLEX[_mem.duplex] + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + mem.mode = self._decode_mode(_mem) + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.tuning_step = STEPS[_mem.tune_step] + mem.power = self._decode_power_level(_mem) + mem.skip = flag.pskip and "P" or flag.skip and "S" or "" + mem.name = self._decode_label(_mem) + + return mem + + def _decode_label(self, mem): + return str(mem.label).rstrip("\xFF") + + def _encode_label(self, mem): + return self._add_ff_pad(mem.name.rstrip(), 6) + + def _encode_charsetbits(self, mem): + # We only speak english here in chirpville + return [0x00, 0x00] + + def _decode_power_level(self, mem): # 3 High 2 Mid 1 Low + return POWER_LEVELS[3 - mem.power] + + def _encode_power_level(self, mem): + return 3 - POWER_LEVELS.index(mem.power) + + def _decode_mode(self, mem): + return MODES[mem.mode] + + def _encode_mode(self, mem): + return MODES.index(mem.mode) + + def _get_tmode(self, mem, _mem): + mem.tmode = TMODES[_mem.tone_mode] + + def _set_tmode(self, _mem, mem): + _mem.tone_mode = TMODES.index(mem.tmode) + + def _set_mode(self, _mem, mem): + _mem.mode = self._encode_mode(mem) + + def _debank(self, mem): + bm = self.get_bank_model() + for bank in bm.get_memory_mappings(mem): + bm.remove_memory_from_mapping(mem, bank) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + flag = self._memobj.flag[mem.number - 1] + + self._debank(mem) + + if not mem.empty and not flag.valid: + self._wipe_memory(_mem) + + if mem.empty and flag.valid and not flag.used: + flag.valid = False + return + flag.used = not mem.empty + flag.valid = flag.used + + if mem.empty: + return + + if mem.freq < 30000000 or \ + (mem.freq > 88000000 and mem.freq < 108000000) or \ + mem.freq > 580000000: + flag.nosubvfo = True # Masked from VFO B + else: + flag.nosubvfo = False # Available in both VFOs + + _mem.freq = int(mem.freq / 1000) + _mem.offset = int(mem.offset / 1000) + _mem.tone = chirp_common.TONES.index(mem.rtone) + self._set_tmode(_mem, mem) + _mem.duplex = DUPLEX.index(mem.duplex) + self._set_mode(_mem, mem) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = STEPS.index(mem.tuning_step) + + if mem.power: + _mem.power = self._encode_power_level(mem) + else: + _mem.power = 3 # Set 3 - High power as the default + + _mem.label = self._encode_label(mem) + charsetbits = self._encode_charsetbits(mem) + _mem.charsetbits[0], _mem.charsetbits[1] = charsetbits + + flag.skip = mem.skip == "S" + flag.pskip = mem.skip == "P" + + _mem.display_tag = 1 # Always Display Memory Name (For the moment..) + + @classmethod + def _wipe_memory(cls, mem): + mem.set_raw("\x00" * (mem.size() / 8)) + mem.unknown1 = 0x05 + + def get_bank_model(self): + return FT70BankModel(self) + + def _get_dtmf_settings(self): + menu = RadioSettingGroup("dtmf_settings", "DTMF") + dtmf = self._memobj.scan_settings + + val = RadioSettingValueList( + self._DTMF_MODE, + self._DTMF_MODE[dtmf.dtmf_mode]) + rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DTMF_DELAY, + self._DTMF_DELAY[dtmf.dtmf_delay]) + rs = RadioSetting( + "scan_settings.dtmf_delay", "DTMF Delay", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DTMF_SPEED, + self._DTMF_SPEED[dtmf.dtmf_speed]) + rs = RadioSetting( + "scan_settings.dtmf_speed", "DTMF Speed", val) + menu.append(rs) + + for i in range(10): + + name = "dtmf_%02d" % (i + 1) + if i == 9: + name = "dtmf_%02d" % 0 + + dtmfsetting = self._memobj.dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c == 0xFF: + break + if c < len(FT70_DTMF_CHARS): + dtmfstr += FT70_DTMF_CHARS[c] + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + dtmfentry.set_charset( + FT70_DTMF_CHARS + list("abcdef ")) # Allow input in lowercase, space ? validation fails otherwise + rs = RadioSetting(name, name.upper(), dtmfentry) + rs.set_apply_callback(self.apply_dtmf, i) + menu.append(rs) + + return menu + + def _get_display_settings(self): + menu = RadioSettingGroup("display_settings", "Display") + scan_settings = self._memobj.scan_settings + + val = RadioSettingValueList( + self._LAMP_KEY, + self._LAMP_KEY[scan_settings.lamp]) + rs = RadioSetting("scan_settings.lamp", "Lamp", val) + menu.append(rs) + + val = RadioSettingValueList( + self._LCD_DIMMER, + self._LCD_DIMMER[scan_settings.lcd_dimmer]) + rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val) + menu.append(rs) + + opening_message = self._memobj.opening_message + val = RadioSettingValueList( + self._OPENING_MESSAGE, + self._OPENING_MESSAGE[opening_message.flag]) + rs = RadioSetting("opening_message.flag", "Opening Msg Mode", val) + menu.append(rs) + + return menu + + def _get_config_settings(self): + menu = RadioSettingGroup("config_settings", "Config") + scan_settings = self._memobj.scan_settings + + # 02 APO Set the length of time until the transceiver turns off automatically. + + first_settings = self._memobj.first_settings + val = RadioSettingValueList( + self._APO_SELECT, + self._APO_SELECT[first_settings.apo]) + rs = RadioSetting("first_settings.apo", "APO", val) + menu.append(rs) + + # 03 BCLO Turns the busy channel lockout function on/off. + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.bclo]) + rs = RadioSetting("scan_settings.bclo", "Busy Channel Lockout", val) + menu.append(rs) + + # 04 BEEP Sets the beep sound function. + + beep_settings = self._memobj.beep_settings + val = RadioSettingValueList( + self._BEEP_SELECT, + self._BEEP_SELECT[beep_settings.beep_select]) + rs = RadioSetting("beep_settings.beep_select", "Beep", val) + menu.append(rs) + + # 05 BEP.LVL Beep volume setting LEVEL1 - LEVEL4 - LEVEL7 + + val = RadioSettingValueList( + self._BEEP_LEVEL, + self._BEEP_LEVEL[beep_settings.beep_level]) + rs = RadioSetting("beep_settings", "Beep Level", val) + menu.append(rs) + + # 06 BEP.Edg Sets the beep sound ON or OFF when a band edge is encountered. + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.beep_edge]) + rs = RadioSetting("scan_settings.beep_edge", "Beep Band Edge", val) + menu.append(rs) + + # 10 Bsy.LED Turn the MODE/STATUS Indicator ON or OFF while receiving signals. + + val = RadioSettingValueList( + self._ON_OFF, + self._ON_OFF[scan_settings.busy_led]) + rs = RadioSetting("scan_settings.busy_led", "Busy LED", val) + menu.append(rs) + + # 26 HOME/REV Select the function of the [HOME/REV] key. + + val = RadioSettingValueList( + self._HOME_REV, + self._HOME_REV[scan_settings.home_rev]) + rs = RadioSetting("scan_settings.home_rev", "HOME/REV", val) + menu.append(rs) + + # 27 HOME->VFO Turn transfer VFO to the Home channel ON or OFF. + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.home_vfo]) + rs = RadioSetting("scan_settings.home_vfo", "Home->VFO", val) + menu.append(rs) + + # 30 LOCK Configure the lock mode setting. KEY / DIAL / K+D / PTT / K+P / D+P / ALL + + val = RadioSettingValueList( + self._LOCK, + self._LOCK[scan_settings.lock]) + rs = RadioSetting("scan_settings.lock", "Lock Mode", val) + menu.append(rs) + + # 32 Mon/T-Call Select the function of the [MONI/T-CALL] switch. + + val = RadioSettingValueList( + self._MONI_TCALL, + self._MONI_TCALL[scan_settings.moni]) + rs = RadioSetting("scan_settings.moni", "MONI/T-CALL", val) + menu.append(rs) + + # 42 PTT.DLY Set the PTT delay time. OFF / 20 MS / 50 MS / 100 MS / 200 MS + + val = RadioSettingValueList( + self._PTT_DELAY, + self._PTT_DELAY[scan_settings.ptt_delay]) + rs = RadioSetting("scan_settings.ptt_delay", "PTT Delay", val) + menu.append(rs) + + # 45 RPT.ARS Turn the ARS function on/off. + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.ars]) + rs = RadioSetting("scan_settings.ars", "ARS", val) + menu.append(rs) + + # 48 RX.SAVE Set the battery save time. OFF / 0.2 S - 60.0 S + + val = RadioSettingValueList( + self._RX_SAVE, + self._RX_SAVE[scan_settings.rx_save]) + rs = RadioSetting("scan_settings.rx_save", "RX SAVE", val) + menu.append(rs) + + # 60 VFO.MOD Set the frequency setting range in the VFO mode by DIAL knob. ALL / BAND + + val = RadioSettingValueList( + self._VFO_MODE, + self._VFO_MODE[scan_settings.vfo_mode]) + rs = RadioSetting("scan_settings.vfo_mode", "VFO MODE", val) + menu.append(rs) + + # 56 TOT Set the timeout timer. + + val = RadioSettingValueList( + self._TOT_TIME, + self._TOT_TIME[scan_settings.tot]) + rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val) + menu.append(rs) + + # 31 MCGAIN Adjust the microphone gain level + + val = RadioSettingValueList( + self._MIC_GAIN, + self._MIC_GAIN[scan_settings.mic_gain]) + rs = RadioSetting("scan_settings.mic_gain", "Mic Gain", val) + menu.append(rs) + + # VOLUME Adjust the volume level + + scan_settings_2 = self._memobj.scan_settings_2 + val = RadioSettingValueList( + self._VOLUME, + self._VOLUME[scan_settings_2.volume]) + rs = RadioSetting("scan_settings_2.volume", "Volume", val) + menu.append(rs) + + # Squelch F key, Hold Monitor, Dial to adjust squelch level + + squelch_settings = self._memobj.squelch_settings + val = RadioSettingValueList( + self._SQUELCH, + self._SQUELCH[squelch_settings.squelch]) + rs = RadioSetting("squelch_settings.squelch", "Squelch", val) + menu.append(rs) + + return menu + + def _get_digital_settings(self): + menu = RadioSettingGroup("digital_settings", "Digital") + + # MYCALL + mycall = self._memobj.my_call + mycallstr = str(mycall.callsign).rstrip("\xFF") + + mycallentry = RadioSettingValueString(0, 10, mycallstr, False, charset=self._MYCALL_CHR_SET) + rs = RadioSetting('mycall.callsign', 'MYCALL', mycallentry) + rs.set_apply_callback(self.apply_mycall, mycall) + menu.append(rs) + + # Short Press AMS button AMS TX Mode + + digital_settings = self._memobj.digital_settings + val = RadioSettingValueList( + self._AMS_TX_MODE, + self._AMS_TX_MODE[digital_settings.ams_tx_mode]) + rs = RadioSetting("digital_settings.ams_tx_mode", "AMS TX Mode", val) + menu.append(rs) + + # 16 DIG VW Turn the VW mode selection ON or OFF. + + val = RadioSettingValueList( + self._VW_MODE, + self._VW_MODE[digital_settings.vw_mode]) + rs = RadioSetting("digital_settings.vw_mode", "VW Mode", val) + menu.append(rs) + + # TX DG-ID Long Press Mode Key, Dial + + val = RadioSettingValueList( + self._DG_ID, + self._DG_ID[digital_settings.tx_dg_id]) + rs = RadioSetting("digital_settings.tx_dg_id", "TX DG-ID", val) + menu.append(rs) + + # RX DG-ID Long Press Mode Key, Mode Key to select, Dial + + val = RadioSettingValueList( + self._DG_ID, + self._DG_ID[digital_settings.rx_dg_id]) + rs = RadioSetting("digital_settings.rx_dg_id", "RX DG-ID", val) + menu.append(rs) + + # 15 DIG.POP Call sign display pop up time + + # 00 OFF 00 + # 0A 2s 10 + # 0B 4s 11 + # 0C 6s 12 + # 0D 8s 13 + # 0E 10s 14 + # 0F 20s 15 + # 10 30s 16 + # 11 60s 17 + # 12 CONT 18 + + digital_settings_more = self._memobj.digital_settings_more + + val = RadioSettingValueList( + self._DIG_POP_UP, + self._DIG_POP_UP[ + 0 if digital_settings_more.digital_popup == 0 else digital_settings_more.digital_popup - 9]) + + rs = RadioSetting("digital_settings_more.digital_popup", "Digital Popup", val) + rs.set_apply_callback(self.apply_digital_popup, digital_settings_more) + menu.append(rs) + + # 07 BEP.STB Standby Beep in the digital C4FM mode. On/Off + + val = RadioSettingValueList( + self._STANDBY_BEEP, + self._STANDBY_BEEP[digital_settings.standby_beep]) + rs = RadioSetting("digital_settings.standby_beep", "Standby Beep", val) + menu.append(rs) + + return menu + + def _get_gm_settings(self): + menu = RadioSettingGroup("first_settings", "Group Monitor") + + # 24 GM RNG Select the beep option while receiving digital GM information. OFF / IN RNG /ALWAYS + + first_settings = self._memobj.first_settings + val = RadioSettingValueList( + self._GM_RING, + self._GM_RING[first_settings.gm_ring]) + rs = RadioSetting("first_settings.gm_ring", "GM Ring", val) + menu.append(rs) + + # 25 GM INT Set the transmission interval of digital GM information. OFF / NORMAL / LONG + + scan_settings = self._memobj.scan_settings + val = RadioSettingValueList( + self._GM_INTERVAL, + self._GM_INTERVAL[scan_settings.gm_interval]) + rs = RadioSetting("scan_settings.gm_interval", "GM Interval", val) + menu.append(rs) + + return menu + + def _get_scan_settings(self): + menu = RadioSettingGroup("scan_settings", "Scan") + scan_settings = self._memobj.scan_settings + + # 23 DW RVT Turn the "Priority Channel Revert" feature ON or OFF during Dual Receive. + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.dw_rt]) + rs = RadioSetting("scan_settings.dw_rt", "Dual Watch Priority Channel Revert", val) + menu.append(rs) + + # 21 DW INT Set the priority memory channel monitoring interval during Dual Receive. 0.1S - 5.0S - 10.0S + + val = RadioSettingValueList( + self._SCAN_RESTART, + self._SCAN_RESTART[scan_settings.dw_interval]) + rs = RadioSetting("scan_settings.dw_interval", "Dual Watch Interval", val) + menu.append(rs) + + # 22 DW RSM Configure the scan stop mode settings for Dual Receive. 2.0S - 10.0 S / BUSY / HOLD + + first_settings = self._memobj.first_settings + val = RadioSettingValueList( + self._SCAN_RESUME, + self._SCAN_RESUME[first_settings.dw_resume_interval]) + rs = RadioSetting("first_settings.dw_resume_interval", "Dual Watch Resume Interval", val) + menu.append(rs) + + # 51 SCN.LMP Set the scan lamp ON or OFF when scanning stops. OFF / ON + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.scan_lamp]) + rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val) + menu.append(rs) + + # 53 SCN.STR Set the scanning restart time. 0.1 S - 2.0 S - 10.0 S + + val = RadioSettingValueList( + self._SCAN_RESTART, + self._SCAN_RESTART[scan_settings.scan_restart]) + rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val) + menu.append(rs) + + # Scan Width Section + + # 50 SCV.WTH Set the VFO scan frequency range. BAND / ALL - NOT FOUND! + + # Scan Resume Section + + # 52 SCN.RSM Configure the scan stop mode settings. 2.0 S - 5.0 S - 10.0 S / BUSY / HOLD + + first_settings = self._memobj.first_settings + val = RadioSettingValueList( + self._SCAN_RESUME, + self._SCAN_RESUME[first_settings.scan_resume]) + rs = RadioSetting("first_settings.scan_resume", "Scan Resume", val) + menu.append(rs) + + return menu + + def _get_settings(self): + top = RadioSettings( + self._get_config_settings(), + self._get_digital_settings(), + self._get_display_settings(), + self._get_dtmf_settings(), + self._get_gm_settings(), + self._get_scan_settings() + ) + return top + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + @classmethod + def apply_ff_padded_string(cls, setting, obj): + setattr(obj, "padded_string", cls._add_ff_pad(setting.value.get_value().rstrip(), 6)) + + def set_settings(self, settings): + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + if element.has_apply_callback(): + LOG.debug("Using apply callback") + try: + element.run_apply_callback() + except NotImplementedError as e: + LOG.error(e) + continue + + # Find the object containing setting. + obj = _mem + bits = element.get_name().split(".") + setting = bits[-1] + for name in bits[:-1]: + if name.endswith("]"): + name, index = name.split("[") + index = int(index[:-1]) + obj = getattr(obj, name)[index] + else: + obj = getattr(obj, name) + + try: + old_val = getattr(obj, setting) + LOG.debug("Setting %s(%r) <= %s" % ( + element.get_name(), old_val, element.value)) + setattr(obj, setting, element.value) + except AttributeError as e: + LOG.error("Setting %s is not in the memory map: %s" % + (element.get_name(), e)) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def apply_volume(cls, setting, vfo): + val = setting.value.get_value() + cls._memobj.vfo_info[(vfo * 2)].volume = val + cls._memobj.vfo_info[(vfo * 2) + 1].volume = val + + def apply_dtmf(cls, setting, i): + rawval = setting.value.get_value().upper().rstrip() + val = [FT70_DTMF_CHARS.index(x) for x in rawval] + for x in range(len(val), 16): + val.append(0xFF) + cls._memobj.dtmf[i].memory = val + + def apply_digital_popup(cls, setting, obj): + rawval = setting.value.get_value() + val = 0 if cls._DIG_POP_UP.index(rawval) == 0 else cls._DIG_POP_UP.index(rawval) + 9 + obj.digital_popup = val + + def apply_mycall(cls, setting, obj): + cs = setting.value.get_value() + if cs[0] in ('-', '/'): + raise InvalidValueError("First character of call sign can't be - or /: {0:s}".format(cs)) + else: + obj.callsign = cls._add_ff_pad(cs.rstrip(), 10) diff --git a/chirp/drivers/ft7100.py b/chirp/drivers/ft7100.py new file mode 100644 index 0000000..ca51367 --- /dev/null +++ b/chirp/drivers/ft7100.py @@ -0,0 +1,1231 @@ +# Copyright 2011 Dan Smith +# +# FT-2900-specific modifications by Richard Cochran, +# Initial work on settings by Chris Fosnight, +# FT-7100-specific modifications by Bruno Maire, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import logging + +from chirp import util, memmap, chirp_common, bitwise, directory, errors +from chirp.drivers.yaesu_clone import YaesuCloneModeRadio +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList, RadioSettingValueString, RadioSettings, \ + RadioSettingValueInteger, RadioSettingValueBoolean + +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +ACK = "\x06" +NB_OF_BLOCKS = 248 +BLOCK_LEN = 32 + + +def _send(pipe, data): + time.sleep(0.035) # Same delay as "FT7100 Programmer" from RT Systems + # pipe.write(data) --> It seems, that the single bytes are sent too fast + # so send character per character with a delay + for ch in data: + pipe.write(ch) + time.sleep(0.0012) # 0.0011 is to short. No ACK after a few packets + echo = pipe.read(len(data)) + if data == "": + raise Exception("Failed to read echo." + " Maybe serial hardware not connected." + " Maybe radio not powered or not in receiving mode.") + if data != echo: + LOG.debug("expecting echo\n%s\n", util.hexprint(data)) + LOG.debug("got echo\n%s\n", util.hexprint(echo)) + raise Exception("Got false echo. Expected: '{}', got: '{}'." + .format(data, echo)) + + +def _send_ack(pipe): + time.sleep(0.01) # Wait for radio input buffer ready + # time.sleep(0.0003) is the absolute working minimum. + # This delay is not critical for the transfer as there are not many ACKs. + _send(pipe, ACK) + + +def _wait_for_ack(pipe): + echo = pipe.read(1) + if echo == "": + raise Exception("Failed to read ACK. No response from radio.") + if echo != ACK: + raise Exception("Failed to read ACK. Expected: '{}', got: '{}'." + .format(util.hexprint(ACK), util.hexprint(echo))) + + +def _download(radio): + LOG.debug("in _download\n") + data = "" + for _i in range(0, 60): + data = radio.pipe.read(BLOCK_LEN) + LOG.debug("Header:\n%s", util.hexprint(data)) + LOG.debug("len(header) = %s\n", len(data)) + if data == radio.IDBLOCK: + break + if data == "": + raise Exception("Got no data from radio.") + if data != radio.IDBLOCK: + raise Exception("Got false header. Expected: '{}', got: '{}'." + .format(radio.IDBLOCK, data)) + _send_ack(radio.pipe) + + # read 16 Byte block + # and ignore it because it is constant. This might be a bug. + # It was built in at the very beginning and discovered very late that the + # data might be necessary later to write to the radio. + # Now the data is hardcoded in _upload(radio) + data = radio.pipe.read(16) + _send_ack(radio.pipe) + + # initialize data, the big var that holds all memory + data = "" + for block_nr in range(NB_OF_BLOCKS): + chunk = radio.pipe.read(BLOCK_LEN) + if len(chunk) != BLOCK_LEN: + LOG.debug("Block %i ", block_nr) + LOG.debug("Got: %i:\n%s", len(chunk), util.hexprint(chunk)) + LOG.debug("len chunk is %i\n", len(chunk)) + raise Exception("Failed to get full data block") + else: + data += chunk + _send_ack(radio.pipe) + + if radio.status_fn: + status = chirp_common.Status() + status.max = NB_OF_BLOCKS * BLOCK_LEN + status.cur = len(data) + status.msg = "Cloning from radio" + radio.status_fn(status) + + LOG.debug("Total: %i", len(data)) + _send_ack(radio.pipe) + + # for debugging purposes, dump the channels, in hex. + for _i in range(0, (NB_OF_BLOCKS * BLOCK_LEN) / 26): + _start_data = 4 + 26 * _i + chunk = data[_start_data:_start_data + 26] + LOG.debug("channel %i:\n%s", _i-21, util.hexprint(chunk)) + + return memmap.MemoryMap(data) + + +def _upload(radio): + data = radio.pipe.read(256) # Clear buffer + _send(radio.pipe, radio.IDBLOCK) + _wait_for_ack(radio.pipe) + + # write 16 Byte block + # If there should be a problem, see remarks in _download(radio) + _send(radio.pipe, "\xCC\x77\x01\x00\x0C\x07\x0C\x07" + "\x00\x00\x00\x00\x00\x00\x00\x00") + _wait_for_ack(radio.pipe) + + for block_nr in range(NB_OF_BLOCKS): + data = radio.get_mmap()[block_nr * BLOCK_LEN: + (block_nr + 1) * BLOCK_LEN] + LOG.debug("Writing block_nr %i:\n%s", block_nr, util.hexprint(data)) + _send(radio.pipe, data) + _wait_for_ack(radio.pipe) + + if radio.status_fn: + status = chirp_common.Status() + status.max = NB_OF_BLOCKS * BLOCK_LEN + status.cur = block_nr * BLOCK_LEN + status.msg = "Cloning to radio" + radio.status_fn(status) + block_nr += 1 + +MEM_FORMAT = """ +struct mem { + u8 is_used:1, + is_masked:1, + is_skip:1, + unknown11:3, + show_name:1, + is_split:1; + u8 unknown2; + ul32 freq_rx_Hz; + ul32 freq_tx_Hz; + ul16 offset_10khz; + u8 unknown_dependent_of_band_144_b0000_430_b0101:4, + tuning_step_index_1_2:4; + u8 unknown51:2, + is_offset_minus:1, + is_offset_plus:1, + unknown52:1, + tone_mode_index:3; + u8 tone_index; + u8 dtcs_index; + u8 is_mode_am:1, + unknown71:2, + is_packet96:1 + unknown72:2, + power_index:2; + u8 unknown81:2, + tuning_step_index_2_2:4, + unknown82:2; + char name[6]; + u8 unknown9; + u8 unknownA; +}; + +// Settings are often present multiple times. +// The memories which is written to are mapped here +struct +{ +#seekto 0x41; + u8 current_band; +#seekto 0xa1; + u8 apo; +#seekto 0xa2; + u8 ars_vhf; +#seekto 0xe2; + u8 ars_uhf; +#seekto 0xa3; + u8 arts_vhf; +#seekto 0xa3; + u8 arts_uhf; +#seekto 0xa4; + u8 beep; +#seekto 0xa5; + u8 cwid; +#seekto 0x80; + char cwidw[6]; +#seekto 0xa7; + u8 dim; +#seekto 0xaa; + u8 dcsnr_vhf; +#seekto 0xea; + u8 dcsnr_uhf; +#seekto 0xab; + u8 disp; +#seekto 0xac; + u8 dtmfd; +#seekto 0xad; + u8 dtmfs; +#seekto 0xae; + u8 dtmfw; +#seekto 0xb0; + u8 lockt; +#seekto 0xb1; + u8 mic; +#seekto 0xb2; + u8 mute; +#seekto 0xb4; + u8 button[4]; +#seekto 0xb8; + u8 rf_sql_vhf; +#seekto 0xf8; + u8 rf_sql_uhf; +#seekto 0xb9; + u8 scan_vhf; +#seekto 0xf9; + u8 scan_uhf; +#seekto 0xbc; + u8 speaker_cnt; +#seekto 0xff; + u8 tot; +#seekto 0xc0; + u8 txnar_vhf; +#seekto 0x100; + u8 txnar_uhf; +#seekto 0xc1; + u8 vfotr; +#seekto 0xc2; + u8 am; +} overlay; + +// All known memories +#seekto 0x20; + u8 nb_mem_used_vhf; +#seekto 0x22; + u8 nb_mem_used_vhf_and_limits; +#seekto 0x24; + u8 nb_mem_used_uhf; +#seekto 0x26; + u8 nb_mem_used_uhf_and_limits; + +#seekto 0x41; + u8 current_band; + +#seekto 0x42; + u8 current_nb_mem_used_vhf_maybe_not; + +#seekto 0x4c; + u8 priority_channel_maybe_1; // not_implemented + u8 priority_channel_maybe_2; // not_implemented + u8 priority_channel; // not_implemented + +#seekto 0x87; + u8 opt_01_apo_1_4; +#seekto 0xa1; + u8 opt_01_apo_2_4; +#seekto 0xc5; + u8 opt_01_apo_3_4; +#seekto 0xe1; + u8 opt_01_apo_4_4; + +#seekto 0x88; + u8 opt_02_ars_vhf_1_2; +#seekto 0xa2; + u8 opt_02_ars_vhf_2_2; +#seekto 0xc6; + u8 opt_02_ars_uhf_1_2; +#seekto 0xe2; + u8 opt_02_ars_uhf_2_2; + +#seekto 0x89; + u8 opt_03_arts_mode_vhf_1_2; +#seekto 0xa3; + u8 opt_03_arts_mode_vhf_2_2; +#seekto 0xc7; + u8 opt_03_arts_mode_uhf_1_2; +#seekto 0xa3; + u8 opt_03_arts_mode_vhf_2_2; + +#seekto 0x8a; + u8 opt_04_beep_1_2; +#seekto 0xa4; + u8 opt_04_beep_2_2; + +#seekto 0x8b; + u8 opt_05_cwid_on_1_4; +#seekto 0xa5; + u8 opt_05_cwid_on_2_4; +#seekto 0xc9; + u8 opt_05_cwid_on_3_4; +#seekto 0xe5; + u8 opt_05_cwid_on_4_4; + +#seekto 0x80; + char opt_06_cwidw[6]; + +#seekto 0x8d; + u8 opt_07_dim_1_4; +#seekto 0xa7; + u8 opt_07_dim_2_4; +#seekto 0xcb; + u8 opt_07_dim_3_4; +#seekto 0xe7; + u8 opt_07_dim_4_4; + +#seekto 0x90; + u8 opt_10_dcsnr_vhf_1_2; +#seekto 0xaa; + u8 opt_10_dcsnr_vhf_2_2; +#seekto 0xce; + u8 opt_10_dcsnr_uhf_1_2; +#seekto 0xea; + u8 opt_10_dcsnr_uhf_2_2; + +#seekto 0x91; + u8 opt_11_disp_1_4; +#seekto 0xab; + u8 opt_11_disp_2_4; +#seekto 0xcf; + u8 opt_11_disp_3_4; +#seekto 0xeb; + u8 opt_11_disp_4_4; + +#seekto 0x92; + u8 opt_12_dtmf_delay_1_4; +#seekto 0xac; + u8 opt_12_dtmf_delay_2_4; +#seekto 0xd0; + u8 opt_12_dtmf_delay_3_4; +#seekto 0xec; + u8 opt_12_dtmf_delay_4_4; + +#seekto 0x93; + u8 opt_13_dtmf_speed_1_4; +#seekto 0xad; + u8 opt_13_dtmf_speed_2_4; +#seekto 0xd1; + u8 opt_13_dtmf_speed_3_4; +#seekto 0xed; + u8 opt_13_dtmf_speed_4_4; + +#seekto 0x94; + u8 opt_14_dtmfw_index_1_4; +#seekto 0xae; + u8 opt_14_dtmfw_index_2_4; +#seekto 0xd2; + u8 opt_14_dtmfw_index_3_4; +#seekto 0xee; + u8 opt_14_dtmfw_index_4_4; + +#seekto 0x96; + u8 opt_16_lockt_1_4; +#seekto 0xb0; + u8 opt_16_lockt_2_4; +#seekto 0xd4; + u8 opt_16_lockt_3_4; +#seekto 0xf0; + u8 opt_16_lockt_4_4; + +#seekto 0x97; + u8 opt_17_mic_MH48_1_4; +#seekto 0xb1; + u8 opt_17_mic_MH48_2_4; +#seekto 0xd5; + u8 opt_17_mic_MH48_3_4; +#seekto 0xf1; + u8 opt_17_mic_MH48_4_4; + +#seekto 0x98; + u8 opt_18_mute_1_4; +#seekto 0xb2; + u8 opt_18_mute_2_4; +#seekto 0xd6; + u8 opt_18_mute_3_4; +#seekto 0xf2; + u8 opt_18_mute_4_4; + +#seekto 0x9a; + u8 opt_20_pg_p_1_4[4]; +#seekto 0xb4; + u8 opt_20_pg_p_2_4[4]; +#seekto 0xd8; + u8 opt_20_pg_p_3_4[4]; +#seekto 0xf4; + u8 opt_20_pg_p_4_4[4]; + +#seekto 0x9e; + u8 opt_24_rf_sql_vhf_1_2; +#seekto 0xb8; + u8 opt_24_rf_sql_vhf_2_2; +#seekto 0xdc; + u8 opt_24_rf_sql_uhf_1_2; +#seekto 0xf8; + u8 opt_24_rf_sql_uhf_2_2; + +#seekto 0x9f; + u8 opt_25_scan_resume_vhf_1_2; +#seekto 0xb9; + u8 opt_25_scan_resume_vhf_2_2; +#seekto 0xdd; + u8 opt_25_scan_resume_uhf_1_2; +#seekto 0xf9; + u8 opt_25_scan_resume_uhf_2_2; + +#seekto 0xbc; + u8 opt_28_speaker_cnt_1_2; +#seekto 0xfc; + u8 opt_28_speaker_cnt_2_2; + +#seekto 0xbf; + u8 opt_31_tot_1_2; +#seekto 0xff; + u8 opt_31_tot_2_2; + +#seekto 0xc0; + u8 opt_32_tx_nar_vhf; +#seekto 0x100; + u8 opt_32_tx_nar_uhf; + +#seekto 0xc1; + u8 opt_33_vfo_tr; + +#seekto 0xc2; + u8 opt_34_am_1_2; +#seekto 0x102; + u8 opt_34_am_2_2; + +#seekto 260; +struct { + struct mem mem_struct; + char fill_ff[2]; +} unknown00; + +struct { + struct mem mem_struct; + char fill_00[6]; +} current_vfo_vhf_uhf[2]; + +struct { + struct mem mem_struct; + char fill_ff[6]; +} current_mem_vhf_uhf[2]; + +struct { + struct mem mem_struct; + char fill_ff[6]; +} home_vhf_uhf[2]; + +struct { + struct mem mem_struct; + char fill_010003000000[6]; +} vhome; + +struct { + struct mem mem_struct; + char fill_010001000000[6]; +} unknown01; + +struct { + char name[32]; +} Vertex_Standard_AH003M_Backup_DT; + +struct mem + memory[260]; + +struct { + char name[24]; +} Vertex_Standard_AH003M; + +struct { + u8 dtmf[16]; +} dtmf_mem[16]; +""" + +MODES_VHF = ["FM", "AM"] +MODES_UHF = ["FM"] # AM can be set but is ignored by the radio +DUPLEX = ["", "-", "+", "split"] +TONE_MODES_RADIO = ["", "Tone", "TSQL", "CTCSS Bell", "DTCS"] +TONE_MODES = ["", "Tone", "TSQL", "DTCS"] +POWER_LEVELS = [ + chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Low2", watts=10), + chirp_common.PowerLevel("Low3", watts=20), + chirp_common.PowerLevel("High", watts=35), + ] +TUNING_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] +SKIP_VALUES = ["", "S"] +CHARSET = r"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^ _" +DTMF_CHARSET = "0123456789*# " +SPECIAL_CHANS = ['VFO-VHF', 'VFO-UHF', 'Home-VHF', 'Home-UHF', 'VFO', 'Home'] +SCAN_LIMITS = ["L1", "U1", "L2", "U2", "L3", "U3", "L4", "U4", "L5", "U5"] + + +def do_download(radio): + """This is your download function""" + return _download(radio) + + +@directory.register +class FT7100Radio(YaesuCloneModeRadio): + + """Yaesu FT-7100M""" + MODEL = "FT-7100M" + VARIANT = "" + IDBLOCK = "Vartex Standard AH003M M-Map V04" + BAUD_RATE = 9600 + + # Return information about this radio's features, including + # how many memories it has, what bands it supports, etc + def get_features(self): + LOG.debug("get_features") + rf = chirp_common.RadioFeatures() + rf.has_bank = False + + rf.memory_bounds = (0, 259) + # This radio supports 120 + 10 + 120 + 10 = 260 memories + # These are zero based for chirpc + rf.valid_bands = [ + (108000000, 180000000), # Supports 2-meters tx + (320000000, 999990000), # Supports 70-centimeters tx + ] + rf.can_odd_split = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_bank_names = False + rf.has_settings = True + rf.has_sub_devices = True + rf.valid_tuning_steps = TUNING_STEPS + rf.valid_modes = MODES_VHF + rf.valid_tmodes = TONE_MODES + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = DUPLEX + rf.valid_skips = SKIP_VALUES + rf.valid_name_length = 6 + rf.valid_characters = CHARSET + rf.valid_special_chans = SPECIAL_CHANS + return rf + + def sync_in(self): + start = time.time() + try: + self._mmap = _download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Downloaded in %.2f sec", (time.time() - start)) + self.process_mmap() + + def sync_out(self): + self.pipe.timeout = 1 + start = time.time() + try: + _upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Uploaded in %.2f sec", (time.time() - start)) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + LOG.debug("get_memory Number: {}".format(number)) + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Get a low-level memory object mapped to the image + if number < 0: + number = SPECIAL_CHANS[number + 10] + if isinstance(number, str): + mem.number = -10 + SPECIAL_CHANS.index(number) + mem.extd_number = number + band = 0 + if self._memobj.overlay.current_band != 0: + band = 1 + if number == 'VFO-VHF': + _mem = self._memobj.current_vfo_vhf_uhf[0].mem_struct + elif number == 'VFO-UHF': + _mem = self._memobj.current_vfo_vhf_uhf[1].mem_struct + elif number == 'Home-VHF': + _mem = self._memobj.home_vhf_uhf[0].mem_struct + elif number == 'Home-UHF': + _mem = self._memobj.home_vhf_uhf[1].mem_struct + elif number == 'VFO': + _mem = self._memobj.current_vfo_vhf_uhf[band].mem_struct + elif number == 'Home': + _mem = self._memobj.home_vhf_uhf[band].mem_struct + _mem.is_used = True + else: + mem.number = number # Set the memory number + _mem = self._memobj.memory[number] + upper_channel = self._memobj.nb_mem_used_vhf + upper_limit = self._memobj.nb_mem_used_vhf_and_limits + if number >= upper_channel and number < upper_limit: + i = number - upper_channel + mem.extd_number = SCAN_LIMITS[i] + if number >= 260-10: + i = number - (260-10) + mem.extd_number = SCAN_LIMITS[i] + + # Convert your low-level frequency to Hertz + mem.freq = int(_mem.freq_rx_Hz) + mem.name = str(_mem.name).rstrip() # Set the alpha tag + + mem.rtone = chirp_common.TONES[_mem.tone_index] + mem.ctone = chirp_common.TONES[_mem.tone_index] + + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs_index] + mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dtcs_index] + + tmode_radio = TONE_MODES_RADIO[_mem.tone_mode_index] + # CTCSS Bell is TSQL plus a flag in the extra setting + is_ctcss_bell = tmode_radio == "CTCSS Bell" + if is_ctcss_bell: + mem.tmode = "TSQL" + else: + mem.tmode = tmode_radio + + mem.duplex = "" + if _mem.is_offset_plus: + mem.duplex = "+" + elif _mem.is_offset_minus: + mem.duplex = "-" + elif _mem.is_split: + mem.duplex = "split" + + if _mem.is_split: + mem.offset = int(_mem.freq_tx_Hz) + else: + mem.offset = int(_mem.offset_10khz)*10000 # 10kHz to Hz + + if _mem.is_mode_am: + mem.mode = "AM" + else: + mem.mode = "FM" + + mem.power = POWER_LEVELS[_mem.power_index] + + mem.tuning_step = TUNING_STEPS[_mem.tuning_step_index_1_2] + + if _mem.is_skip: + mem.skip = "S" + else: + mem.skip = "" + + # mem.comment = "" + + mem.empty = not _mem.is_used + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("show_name", "Show Name", + RadioSettingValueBoolean(_mem.show_name)) + mem.extra.append(rs) + rs = RadioSetting("is_masked", "Is Masked", + RadioSettingValueBoolean(_mem.is_masked)) + mem.extra.append(rs) + rs = RadioSetting("is_packet96", "Packet 9600", + RadioSettingValueBoolean(_mem.is_packet96)) + mem.extra.append(rs) + rs = RadioSetting("is_ctcss_bell", "CTCSS Bell", + RadioSettingValueBoolean(is_ctcss_bell)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + LOG.debug("set_memory Number: {}".format(mem.number)) + if mem.number < 0: + number = SPECIAL_CHANS[mem.number+10] + if number == 'VFO-VHF': + _mem = self._memobj.current_vfo_vhf_uhf[0].mem_struct + elif number == 'VFO-UHF': + _mem = self._memobj.current_vfo_vhf_uhf[1].mem_struct + elif number == 'Home-VHF': + _mem = self._memobj.home_vhf_uhf[0].mem_struct + elif number == 'Home-UHF': + _mem = self._memobj.home_vhf_uhf[1].mem_struct + else: + raise errors.RadioError("Unexpected Memory Number: {}" + .format(mem.number)) + else: + _mem = self._memobj.memory[mem.number] + + _mem.name = mem.name.ljust(6) + + _mem.tone_index = chirp_common.TONES.index(mem.rtone) + _mem.dtcs_index = chirp_common.DTCS_CODES.index(mem.dtcs) + if mem.tmode == "TSQL": + _mem.tone_index = chirp_common.TONES.index(mem.ctone) + if mem.tmode == "DTSC-R": + _mem.dtcs_index = chirp_common.DTCS_CODES.index(mem.rx_dtcs) + + _mem.is_offset_plus = 0 + _mem.is_offset_minus = 0 + _mem.is_split = 0 + _mem.freq_rx_Hz = mem.freq + _mem.freq_tx_Hz = mem.freq + if mem.duplex == "+": + _mem.is_offset_plus = 1 + _mem.freq_tx_Hz = mem.freq + mem.offset + _mem.offset_10khz = int(mem.offset/10000) + elif mem.duplex == "-": + _mem.is_offset_minus = 1 + _mem.freq_tx_Hz = mem.freq - mem.offset + _mem.offset_10khz = int(mem.offset/10000) + elif mem.duplex == "split": + _mem.is_split = 1 + _mem.freq_tx_Hz = mem.offset + # No change to _mem.offset_10khz + + _mem.is_mode_am = mem.mode == "AM" + + if mem.power: + _mem.power_index = POWER_LEVELS.index(mem.power) + + _mem.tuning_step_index_1_2 = TUNING_STEPS.index(mem.tuning_step) + + _mem.is_skip = mem.skip == "S" + + _mem.is_used = not mem.empty + + # if name not empty: show name + _mem.show_name = mem.name.strip() != "" + + # In testmode there is no setting in mem.extra + # This is why the following line is not located in the else part of + # the if structure below + _mem.tone_mode_index = TONE_MODES_RADIO.index(mem.tmode) + + for setting in mem.extra: + if setting.get_name() == "is_ctcss_bell": + if mem.tmode == "TSQL" and setting.value: + _mem.tone_mode_index = TONE_MODES_RADIO.index("CTCSS Bell") + else: + setattr(_mem, setting.get_name(), setting.value) + + LOG.debug("encoded mem\n%s\n", (util.hexprint(_mem.get_raw()[0:25]))) + LOG.debug(repr(_mem)) + + def get_settings(self): + common = RadioSettingGroup("common", "Common Settings") + band = RadioSettingGroup("band", "Band dependent Settings") + arts = RadioSettingGroup("arts", + "Auto Range Transponder System (ARTS)") + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + mic_button = RadioSettingGroup("mic_button", "Microphone Buttons") + setmode = RadioSettings(common, band, arts, dtmf, mic_button) + + _overlay = self._memobj.overlay + + # numbers and names of settings refer to the way they're + # presented in the set menu, as well as the list starting on + # page 49 of the manual + + # 1 Automatic Power Off + opts = [ + "Off", "30 Min", + "1 Hour", "1.5 Hours", + "2 Hours", "2.5 Hours", + "3 Hours", "3.5 Hours", + "4 Hours", "4.5 Hours", + "5 Hours", "5.5 Hours", + "6 Hours", "6.5 Hours", + "7 Hours", "7.5 Hours", + "8 Hours", "8.5 Hours", + "9 Hours", "9.5 Hours", + "10 Hours", "10.5 Hours", + "11 Hours", "11.5 Hours", + "12 Hours", + ] + common.append( + RadioSetting( + "apo", "Automatic Power Off", + RadioSettingValueList(opts, opts[_overlay.apo]))) + + # 2 Automatic Repeater Shift function + opts = ["Off", "On"] + band.append( + RadioSetting( + "ars_vhf", "Automatic Repeater Shift VHF", + RadioSettingValueList(opts, opts[_overlay.ars_vhf]))) + band.append( + RadioSetting( + "ars_uhf", "Automatic Repeater Shift UHF", + RadioSettingValueList(opts, opts[_overlay.ars_uhf]))) + + # 3 Selects the ARTS mode. + # -> Only useful to set it on the radio directly + + # 4 Enables/disables the key/button beeper. + opts = ["Off", "On"] + common.append( + RadioSetting( + "beep", "Key/Button Beep", + RadioSettingValueList(opts, opts[_overlay.beep]))) + + # 5 Enables/disables the CW IDer during ARTS operation. + opts = ["Off", "On"] + arts.append( + RadioSetting( + "cwid", "Enables/Disables the CW ID", + RadioSettingValueList(opts, opts[_overlay.cwid]))) + + # 6 Callsign during ARTS operation. + cwidw = _overlay.cwidw.get_raw() + cwidw = cwidw.rstrip('\x00') + val = RadioSettingValueString(0, 6, cwidw) + val.set_charset(CHARSET) + rs = RadioSetting("cwidw", "CW Identifier Callsign", val) + + def apply_cwid(setting): + value_string = setting.value.get_value() + _overlay.cwidw.set_value(value_string) + rs.set_apply_callback(apply_cwid) + arts.append(rs) + + # 7 Front panel display's illumination level. + opts = ["0: Off", "1: Max", "2", "3", "4", "5", "6", "7: Min"] + common.append( + RadioSetting( + "dim", "Display Illumination", + RadioSettingValueList(opts, opts[_overlay.dim]))) + + # 8 Setting the DCS code number. + # Note: This Menu item can be set independently for each band, + # and independently in each memory. + + # 9 Activates the DCS Code Search + # -> Only useful if set on radio itself + + # 10 Selects 'Normal' or 'Inverted' DCS coding. + opts = ["TRX Normal", "RX Reversed", "TX Reversed", "TRX Reversed"] + band.append( + RadioSetting( + "dcsnr_vhf", "DCS coding VHF", + RadioSettingValueList(opts, opts[_overlay.dcsnr_vhf]))) + band.append( + RadioSetting( + "dcsnr_uhf", "DCS coding UHF", + RadioSettingValueList(opts, opts[_overlay.dcsnr_uhf]))) + + # 11 Selects the 'sub' band display format + opts = ["Frequency", "Off / Sub Band disabled", + "DC Input Voltage", "CW ID"] + common.append( + RadioSetting( + "disp", "Sub Band Display Format", + RadioSettingValueList(opts, opts[_overlay.disp]))) + + # 12 Setting the DTMF Autodialer delay time + opts = ["50 ms", "250 ms", "450 ms", "750 ms", "1 s"] + dtmf.append( + RadioSetting( + "dtmfd", "Autodialer delay time", + RadioSettingValueList(opts, opts[_overlay.dtmfd]))) + + # 13 Setting the DTMF Autodialer sending speed + opts = ["50 ms", "75 ms", "100 ms"] + dtmf.append( + RadioSetting( + "dtmfs", "Autodialer sending speed", + RadioSettingValueList(opts, opts[_overlay.dtmfs]))) + + # 14 Current DTMF Autodialer memory + rs = RadioSetting("dtmfw", "Current Autodialer memory", + RadioSettingValueInteger(1, 16, _overlay.dtmfw + 1)) + + def apply_dtmfw(setting): + _overlay.dtmfw = setting.value.get_value() - 1 + rs.set_apply_callback(apply_dtmfw) + dtmf.append(rs) + + # DTMF Memory + for i in range(16): + dtmf_string = "" + for j in range(16): + dtmf_char = '' + dtmf_int = int(self._memobj.dtmf_mem[i].dtmf[j]) + if dtmf_int < 10: + dtmf_char = str(dtmf_int) + elif dtmf_int == 14: + dtmf_char = '*' + elif dtmf_int == 15: + dtmf_char = '#' + elif dtmf_int == 255: + break + dtmf_string += dtmf_char + radio_setting_value_string = RadioSettingValueString(0, 16, + dtmf_string) + radio_setting_value_string.set_charset(DTMF_CHARSET) + rs = RadioSetting("dtmf_{0:02d}".format(i), + "DTMF Mem " + str(i+1), + radio_setting_value_string) + + def apply_dtmf(setting, index): + radio_setting_value_string = setting.value.get_value().rstrip() + j = 0 + for dtmf_char in radio_setting_value_string: + dtmf_int = 255 + if dtmf_char in "0123456789": + dtmf_int = int(dtmf_char) + elif dtmf_char == '*': + dtmf_int = 14 + elif dtmf_char == '#': + dtmf_int = 15 + if dtmf_int < 255: + self._memobj.dtmf_mem[index].dtmf[j] = dtmf_int + j += 1 + if j < 16: + self._memobj.dtmf_mem[index].dtmf[j] = 255 + rs.set_apply_callback(apply_dtmf, i) + dtmf.append(rs) + + # 16 Enables/disables the PTT switch lock + opts = ["Off", "Band A", "Band B", "Both"] + common.append( + RadioSetting( + "lockt", "PTT switch lock", + RadioSettingValueList(opts, opts[_overlay.lockt]))) + + # 17 Selects the Microphone type to be used + opts = ["MH-42", "MH-48"] + common.append( + RadioSetting( + "mic", "Microphone type", + RadioSettingValueList(opts, opts[_overlay.mic]))) + + # 18 Reduces the audio level on the sub receiver when the + # main receiver is active + opts = ["Off", "On"] + common.append( + RadioSetting( + "mute", "Mute Sub Receiver", + RadioSettingValueList(opts, opts[_overlay.mute]))) + + # 20 - 23 Programming the microphones button assignment + buttons = [ + "ACC / P1", + "P / P2", + "P1 / P3", + "P2 / P4", + ] + opts_button = ["Low", "Tone", "MHz", "Rev", "Home", "Band", + "VFO / Memory", "Sql Off", "1750 Hz Tone Call", + "Repeater", "Priority"] + for i, button in enumerate(buttons): + rs = RadioSetting( + "button" + str(i), button, + RadioSettingValueList(opts_button, + opts_button[_overlay.button[i]])) + + def apply_button(setting, index): + value_string = setting.value.get_value() + value_int = opts_button.index(value_string) + _overlay.button[index] = value_int + rs.set_apply_callback(apply_button, i) + mic_button.append(rs) + + # 24 Adjusts the RF SQL threshold level + opts = ["Off", "S-1", "S-5", "S-9", "S-FULL"] + band.append( + RadioSetting( + "rf_sql_vhf", "RF Sql VHF", + RadioSettingValueList(opts, opts[_overlay.rf_sql_vhf]))) + band.append( + RadioSetting( + "rf_sql_uhf", "RF Sql UHF", + RadioSettingValueList(opts, opts[_overlay.rf_sql_uhf]))) + + # 25 Selects the Scan-Resume mode + opts = ["Busy", "Time"] + band.append( + RadioSetting( + "scan_vhf", "Scan-Resume VHF", + RadioSettingValueList(opts, opts[_overlay.scan_vhf]))) + band.append( + RadioSetting( + "scan_uhf", "Scan-Resume UHF", + RadioSettingValueList(opts, opts[_overlay.scan_uhf]))) + + # 28 Defining the audio path to the external speaker + opts = ["Off", "Band A", "Band B", "Both"] + common.append( + RadioSetting( + "speaker_cnt", "External Speaker", + RadioSettingValueList(opts, opts[_overlay.speaker_cnt]))) + + # 31 Sets the Time-Out Timer + opts = ["Off", "Band A", "Band B", "Both"] + common.append( + RadioSetting( + "tot", "TX Time-Out [Min.] (0 = Off)", + RadioSettingValueInteger(0, 30, _overlay.tot))) + + # 32 Reducing the MIC Gain (and Deviation) + opts = ["Off", "On"] + band.append( + RadioSetting( + "txnar_vhf", "TX Narrowband VHF", + RadioSettingValueList(opts, opts[_overlay.txnar_vhf]))) + band.append( + RadioSetting( + "txnar_uhf", "TX Narrowband UHF", + RadioSettingValueList(opts, opts[_overlay.txnar_uhf]))) + + # 33 Enables/disables the VFO Tracking feature + opts = ["Off", "On"] + common.append( + RadioSetting( + "vfotr", "VFO Tracking", + RadioSettingValueList(opts, opts[_overlay.vfotr]))) + + # 34 Selects the receiving mode on the VHF band + opts = ["Inhibit (only FM)", "AM", "Auto"] + common.append( + RadioSetting( + "am", "AM Mode", + RadioSettingValueList(opts, opts[_overlay.am]))) + + # Current Band + opts = ["VHF", "UHF"] + common.append( + RadioSetting( + "current_band", "Current Band", + RadioSettingValueList(opts, opts[_overlay.current_band]))) + + # Show number of VHF and UHF channels + val = RadioSettingValueString(0, 7, + str(int(self._memobj.nb_mem_used_vhf))) + val.set_mutable(False) + rs = RadioSetting("num_chan_vhf", "Number of VHF channels", val) + common.append(rs) + val = RadioSettingValueString(0, 7, + str(int(self._memobj.nb_mem_used_uhf))) + val.set_mutable(False) + rs = RadioSetting("num_chan_uhf", "Number of UHF channels", val) + common.append(rs) + + return setmode + + def set_settings(self, uisettings): + _overlay = self._memobj.overlay + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + + try: + name = element.get_name() + value = element.value + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + setattr(_overlay, name, value) + + LOG.debug("Setting %s: %s", name, value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _get_upper_vhf_limit(self): + if self._memobj is None: + # test with tox has no _memobj + upper_vhf_limit = 130 + else: + upper_vhf_limit = int(self._memobj.nb_mem_used_vhf_and_limits) + return upper_vhf_limit + + def _get_upper_uhf_limit(self): + if self._memobj is None: + # test with tox has no _memobj + upper_uhf_limit = 130 + else: + upper_uhf_limit = int(self._memobj.nb_mem_used_uhf_and_limits) + return upper_uhf_limit + + @classmethod + def match_model(cls, filedata, filename): + return filedata[0x1ec0:0x1ec0+len(cls.IDBLOCK)] == cls.IDBLOCK + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn Radio off. + 2. Connect data cable. + 3. While holding "TONE" and "REV" buttons, turn radio on. + 4. After clicking OK, press "TONE" to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn Radio off. + 2. Connect data cable. + 3. While holding "TONE" and "REV" buttons, turn radio on. + 4. Press "REV" to receive image. + 5. Make sure display says "CLONE RX" and green led is blinking + 6. Click OK to start transfer.""")) + return rp + + def get_sub_devices(self): + if not self.VARIANT: + return [FT7100RadioVHF(self._mmap), + FT7100RadioUHF(self._mmap)] + else: + return [] + + +class FT7100RadioVHF(FT7100Radio): + VARIANT = "VHF Band" + + def get_features(self): + LOG.debug("VHF get_features") + upper_vhf_limit = self._get_upper_vhf_limit() + rf = FT7100Radio.get_features(self) + rf.memory_bounds = (1, upper_vhf_limit) + # Normally this band supports 120 + 10 memories. 1 based for chirpw + rf.valid_bands = [(108000000, 180000000)] # Supports 2-meters tx + rf.valid_modes = MODES_VHF + rf.valid_special_chans = ['VFO', 'Home'] + rf.has_sub_devices = False + return rf + + def get_memory(self, number): + LOG.debug("get_memory VHF Number: {}".format(number)) + if isinstance(number, int): + if number >= 0: + mem = FT7100Radio.get_memory(self, number + 0 - 1) + else: + mem = FT7100Radio.get_memory(self, number) + mem.number = number + else: + mem = FT7100Radio.get_memory(self, number + '-VHF') + mem.extd_number = number + mem.immutable = ["number", "extd_number", "skip"] + return mem + + def set_memory(self, mem): + LOG.debug("set_memory VHF Number: {}".format(mem.number)) + # mem is used further in test by tox. So save the modified members. + _number = mem.number + if isinstance(mem.number, int): + if mem.number >= 0: + mem.number += -1 + else: + mem.number += '-VHF' + super(FT7100RadioVHF, self).set_memory(mem) + # Restore modified members + mem.number = _number + return + + +class FT7100RadioUHF(FT7100Radio): + VARIANT = "UHF Band" + + def get_features(self): + LOG.debug("UHF get_features") + upper_uhf_limit = self._get_upper_uhf_limit() + rf = FT7100Radio.get_features(self) + rf.memory_bounds = (1, upper_uhf_limit) + # Normally this band supports 120 + 10 memories. 1 based for chirpw + rf.valid_bands = [(320000000, 999990000)] # Supports 70-centimeters tx + rf.valid_modes = MODES_UHF + rf.valid_special_chans = ['VFO', 'Home'] + rf.has_sub_devices = False + return rf + + def get_memory(self, number): + LOG.debug("get_memory UHF Number: {}".format(number)) + upper_vhf_limit = self._get_upper_vhf_limit() + if isinstance(number, int): + if number >= 0: + mem = FT7100Radio.get_memory(self, number + + upper_vhf_limit - 1) + else: + mem = FT7100Radio.get_memory(self, number) + mem.number = number + else: + mem = FT7100Radio.get_memory(self, number + '-UHF') + mem.extd_number = number + mem.immutable = ["number", "extd_number", "skip"] + return mem + + def set_memory(self, mem): + LOG.debug("set_memory UHF Number: {}".format(mem.number)) + # mem is used further in test by tox. So save the modified members. + _number = mem.number + upper_vhf_limit = self._get_upper_vhf_limit() + if isinstance(mem.number, int): + if mem.number >= 0: + mem.number += upper_vhf_limit - 1 + else: + mem.number += '-UHF' + super(FT7100RadioUHF, self).set_memory(mem) + # Restore modified members + mem.number = _number + return diff --git a/chirp/drivers/ft7800.py b/chirp/drivers/ft7800.py new file mode 100644 index 0000000..c9d706c --- /dev/null +++ b/chirp/drivers/ft7800.py @@ -0,0 +1,1028 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import logging +import os +import re + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, memmap, directory, bitwise, errors +from textwrap import dedent +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + +from collections import defaultdict + +LOG = logging.getLogger(__name__) + +ACK = 0x06 + +MEM_FORMAT = """ +#seekto 0x002A; +u8 banks_unk2; +u8 current_channel; +u8 unk3; +u8 unk4; +u8 current_menu; + +#seekto 0x0035; +u8 banks_unk1; + +#seekto 0x00C8; +struct { + u8 memory[16]; +} dtmf[16]; + +#seekto 0x003A; +struct { + u8 apo; + u8 tot; + u8 lock:3, + arts_interval:1, + unk1a:1, + prog_panel_acc:3; + u8 prog_p1; + u8 prog_p2; + u8 prog_p3; + u8 prog_p4; + u8 rf_sql; + u8 inet_dtmf_mem:4, + inet_dtmf_digit:4; + u8 arts_cwid_enable:1, + prog_tone_vm:1, + unk2a:1, + hyper_write:2, + memory_only:1, + dimmer:2; + u8 beep_scan:1, + beep_edge:1, + beep_key:1, + unk3a:1, + inet_mode:1, + unk3b:1, + dtmf_speed:2; + u8 dcs_polarity:2, + smart_search:1, + priority_revert:1, + unk4a:1, + dtmf_delay:3; + u8 unk5a:3, + microphone_type:1, + scan_resume:1, + unk5b:1, + arts_mode:2; + u8 unk6; +} settings; + +struct mem_struct { + u8 used:1, + unknown1:1, + mode:2, + unknown2:1, + duplex:3; + bbcd freq[3]; + u8 clockshift:1, + tune_step:3, + unknown5:1, // TODO: tmode has extended settings, at least 4 bits + tmode:3; + bbcd split[3]; + u8 power:2, + tone:6; + u8 unknown6:1, + dtcs:7; + u8 unknown7[2]; + u8 offset; + u8 unknown9[3]; +}; + +#seekto 0x0048; +struct mem_struct vfos[5]; + +#seekto 0x01C8; +struct mem_struct homes[5]; + +#seekto 0x0218; +u8 arts_cwid[6]; + +#seekto 0x04C8; +struct mem_struct memory[1000]; + +#seekto 0x4988; +struct { + char name[6]; + u8 enabled:1, + unknown1:7; + u8 used:1, + unknown2:7; +} names[1000]; + +#seekto 0x6c48; +struct { + u32 bitmap[32]; +} bank_channels[20]; + +#seekto 0x7648; +struct { + u8 skip0:2, + skip1:2, + skip2:2, + skip3:2; +} flags[250]; + +#seekto 0x7B48; +u8 checksum; +""" + +MODES = ["FM", "AM", "NFM"] +DUPLEX = ["", "", "-", "+", "split"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +SKIPS = ["", "S", "P", ""] + +CHARSET = ["%i" % int(x) for x in range(0, 10)] + \ + [chr(x) for x in range(ord("A"), ord("Z")+1)] + \ + list(" " * 10) + \ + list("*+,- /| [ ] _") + \ + list("\x00" * 100) + +DTMFCHARSET = list("0123456789ABCD*#") + + +def _send(ser, data): + for i in data: + ser.write(bytes([i])) + time.sleep(0.002) + echo = ser.read(len(data)) + if echo != data: + raise errors.RadioError("Error reading echo (Bad cable?)") + + +def _download(radio): + data = bytes(b"") + + chunk = bytes(b"") + for i in range(0, 30): + chunk += radio.pipe.read(radio._block_lengths[0]) + if chunk: + break + + if len(chunk) != radio._block_lengths[0]: + raise Exception("Failed to read header (%i)" % len(chunk)) + data += chunk + + _send(radio.pipe, bytes([ACK])) + + for i in range(0, radio._block_lengths[1], radio._block_size): + chunk = radio.pipe.read(radio._block_size) + data += chunk + if len(chunk) != radio._block_size: + break + time.sleep(0.01) + _send(radio.pipe, ACK) + if radio.status_fn: + status = chirp_common.Status() + status.max = radio.get_memsize() + status.cur = i+len(chunk) + status.msg = "Cloning from radio" + radio.status_fn(status) + + data += radio.pipe.read(1) + _send(radio.pipe, bytes([ACK])) + + return memmap.MemoryMapBytes(data) + + +def _upload(radio): + cur = 0 + mmap = radio.get_mmap().get_byte_compatible() + for block in radio._block_lengths: + for _i in range(0, block, radio._block_size): + length = min(radio._block_size, block) + # LOG.debug("i=%i length=%i range: %i-%i" % + # (i, length, cur, cur+length)) + _send(radio.pipe, mmap[cur:cur+length]) + if radio.pipe.read(1) != ACK: + raise errors.RadioError("Radio did not ack block at %i" % cur) + cur += length + time.sleep(0.05) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = cur + status.max = radio.get_memsize() + status.msg = "Cloning to radio" + radio.status_fn(status) + + +def get_freq(rawfreq): + """Decode a frequency that may include a fractional step flag""" + # Ugh. The 0x80 and 0x40 indicate values to add to get the + # real frequency. Gross. + if rawfreq > 8000000000: + rawfreq = (rawfreq - 8000000000) + 5000 + + if rawfreq > 4000000000: + rawfreq = (rawfreq - 4000000000) + 2500 + + return rawfreq + + +def set_freq(freq, obj, field): + """Encode a frequency with any necessary fractional step flags""" + obj[field] = freq / 10000 + if (freq % 1000) == 500: + obj[field][0].set_bits(0x40) + + if (freq % 10000) >= 5000: + obj[field][0].set_bits(0x80) + + return freq + + +class FTx800Radio(yaesu_clone.YaesuCloneModeRadio): + """Base class for FT-7800,7900,8800,8900 radios""" + BAUD_RATE = 9600 + VENDOR = "Yaesu" + MODES = list(MODES) + NEEDS_COMPAT_SERIAL = False + _block_size = 64 + + POWER_LEVELS_VHF = [chirp_common.PowerLevel("Hi", watts=50), + chirp_common.PowerLevel("Mid1", watts=20), + chirp_common.PowerLevel("Mid2", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + + POWER_LEVELS_UHF = [chirp_common.PowerLevel("Hi", watts=35), + chirp_common.PowerLevel("Mid1", watts=20), + chirp_common.PowerLevel("Mid2", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to DATA jack. +3. Press and hold in the [MHz(PRI)] key while turning the + radio on. +4. Rotate the DIAL job to select "F-7 CLONE". +5. Press and hold in the [BAND(SET)] key. The display + will disappear for a moment, then the "CLONE" notation + will appear. +6. After clicking OK, press the [V/M(MW)] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to DATA jack. +3. Press and hold in the [MHz(PRI)] key while turning the + radio on. +4. Rotate the DIAL job to select "F-7 CLONE". +5. Press and hold in the [BAND(SET)] key. The display + will disappear for a moment, then the "CLONE" notation + will appear. +6. Press the [LOW(ACC)] key ("--RX--" will appear on the display).""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 999) + rf.has_bank = False + rf.has_ctone = False + rf.has_dtcs_polarity = False + rf.valid_modes = MODES + rf.valid_tmodes = self.TMODES + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_tuning_steps = STEPS + rf.valid_bands = [(108000000, 520000000), (700000000, 990000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_power_levels = self.POWER_LEVELS_VHF + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 6 + rf.can_odd_split = True + return rf + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x7B47)] + + def sync_in(self): + start = time.time() + try: + self._mmap = _download(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Download finished in %i seconds" % (time.time() - start)) + self.check_checksums() + self.process_mmap() + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_out(self): + self.update_checksums() + start = time.time() + try: + _upload(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + LOG.info("Upload finished in %i seconds" % (time.time() - start)) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def _get_mem_offset(self, mem, _mem): + if mem.duplex == "split": + return get_freq(int(_mem.split) * 10000) + else: + return (_mem.offset * 5) * 10000 + + def _set_mem_offset(self, mem, _mem): + if mem.duplex == "split": + set_freq(mem.offset, _mem, "split") + else: + _mem.offset = (int(mem.offset / 10000) / 5) + + def _get_mem_name(self, mem, _mem): + _nam = self._memobj.names[mem.number - 1] + + name = "" + if _nam.used: + for i in str(_nam.name): + name += CHARSET[ord(i)] + + return name.rstrip() + + def _set_mem_name(self, mem, _mem): + _nam = self._memobj.names[mem.number - 1] + + if mem.name.rstrip(): + name = [chr(CHARSET.index(x)) for x in mem.name.ljust(6)[:6]] + _nam.name = "".join(name) + _nam.used = 1 + _nam.enabled = 1 + else: + _nam.used = 0 + _nam.enabled = 0 + + def _get_mem_skip(self, mem, _mem): + _flg = self._memobj.flags[(mem.number - 1) / 4] + flgidx = (mem.number - 1) % 4 + return SKIPS[_flg["skip%i" % flgidx]] + + def _set_mem_skip(self, mem, _mem): + _flg = self._memobj.flags[(mem.number - 1) / 4] + flgidx = (mem.number - 1) % 4 + _flg["skip%i" % flgidx] = SKIPS.index(mem.skip) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + mem.number = number + mem.empty = not _mem.used + if mem.empty: + return mem + + mem.freq = get_freq(int(_mem.freq) * 10000) + mem.rtone = chirp_common.TONES[_mem.tone] + mem.tmode = self.TMODES[_mem.tmode] + mem.mode = self.MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + if self.get_features().has_tuning_step: + mem.tuning_step = STEPS[_mem.tune_step] + mem.duplex = DUPLEX[_mem.duplex] + mem.offset = self._get_mem_offset(mem, _mem) + mem.name = self._get_mem_name(mem, _mem) + + if int(mem.freq / 100) == 4: + mem.power = self.POWER_LEVELS_UHF[_mem.power] + else: + mem.power = self.POWER_LEVELS_VHF[_mem.power] + + mem.skip = self._get_mem_skip(mem, _mem) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + _mem.used = int(not mem.empty) + if mem.empty: + return + + set_freq(mem.freq, _mem, "freq") + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.tmode = self.TMODES.index(mem.tmode) + _mem.mode = self.MODES.index(mem.mode) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + if self.get_features().has_tuning_step: + _mem.tune_step = STEPS.index(mem.tuning_step) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.split = mem.duplex == "split" and int(mem.offset / 10000) or 0 + if mem.power: + _mem.power = self.POWER_LEVELS_VHF.index(mem.power) + else: + _mem.power = 0 + _mem.unknown5 = 0 # Make sure we don't leave garbage here + + # NB: Leave offset after mem name for the 8800! + self._set_mem_name(mem, _mem) + self._set_mem_offset(mem, _mem) + + self._set_mem_skip(mem, _mem) + + +class FT7800BankModel(chirp_common.BankModel): + """Yaesu FT-7800/7900 bank model""" + def __init__(self, radio): + super(FT7800BankModel, self).__init__(radio) + self.__b2m_cache = defaultdict(list) + self.__m2b_cache = defaultdict(list) + + def __precache(self): + if self.__b2m_cache: + return + + for bank in self.get_mappings(): + self.__b2m_cache[bank.index] = self._get_bank_memories(bank) + for memnum in self.__b2m_cache[bank.index]: + self.__m2b_cache[memnum].append(bank.index) + + def get_num_mappings(self): + return 20 + + def get_mappings(self): + banks = [] + for i in range(0, self.get_num_mappings()): + bank = chirp_common.Bank(self, "%i" % i, "BANK-%i" % (i + 1)) + bank.index = i + banks.append(bank) + + return banks + + def add_memory_to_mapping(self, memory, bank): + self.__precache() + + index = memory.number - 1 + _bitmap = self._radio._memobj.bank_channels[bank.index] + ishft = 31 - (index % 32) + _bitmap.bitmap[index // 32] |= (1 << ishft) + self.__m2b_cache[memory.number].append(bank.index) + self.__b2m_cache[bank.index].append(memory.number) + + def remove_memory_from_mapping(self, memory, bank): + self.__precache() + + index = memory.number - 1 + _bitmap = self._radio._memobj.bank_channels[bank.index] + ishft = 31 - (index % 32) + if not (_bitmap.bitmap[index // 32] & (1 << ishft)): + raise Exception("Memory {num} is " + + "not in bank {bank}".format(num=memory.number, + bank=bank)) + _bitmap.bitmap[index // 32] &= ~(1 << ishft) + self.__b2m_cache[bank.index].remove(memory.number) + self.__m2b_cache[memory.number].remove(bank.index) + + def _get_bank_memories(self, bank): + memories = [] + upper = self._radio.get_features().memory_bounds[1] + c = self._radio._memobj.bank_channels[bank.index] + for i in range(0, upper): + _bitmap = c.bitmap[i // 32] + ishft = 31 - (i % 32) + if _bitmap & (1 << ishft): + memories.append(i + 1) + return memories + + def get_mapping_memories(self, bank): + self.__precache() + + return [self._radio.get_memory(n) + for n in self.__b2m_cache[bank.index]] + + def get_memory_mappings(self, memory): + self.__precache() + + _banks = self.get_mappings() + return [_banks[b] for b in self.__m2b_cache[memory.number]] + + +@directory.register +class FT7800Radio(FTx800Radio): + """Yaesu FT-7800""" + MODEL = "FT-7800/7900" + + _model = "AH016" + _memsize = 31561 + _block_lengths = [8, 31552, 1] + TMODES = ["", "Tone", "TSQL", "TSQL-R", "DTCS"] + + def get_bank_model(self): + return FT7800BankModel(self) + + def get_features(self): + rf = FTx800Radio.get_features(self) + rf.has_bank = True + rf.has_settings = True + return rf + + def set_memory(self, memory): + if memory.empty: + self._wipe_memory_banks(memory) + FTx800Radio.set_memory(self, memory) + + def _decode_chars(self, inarr): + LOG.debug("@_decode_chars, type: %s" % type(inarr)) + LOG.debug(inarr) + outstr = "" + for i in inarr: + if i == 0xFF: + break + outstr += CHARSET[i & 0x7F] + return outstr.rstrip() + + def _encode_chars(self, instr, length=16): + LOG.debug("@_encode_chars, type: %s" % type(instr)) + LOG.debug(instr) + outarr = [] + instr = str(instr) + for i in range(length): + if i < len(instr): + outarr.append(CHARSET.index(instr[i])) + else: + outarr.append(0xFF) + return outarr + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + dtmf = RadioSettingGroup("dtmf", "DTMF") + arts = RadioSettingGroup("arts", "ARTS") + prog = RadioSettingGroup("prog", "Programmable Buttons") + + top = RadioSettings(basic, dtmf, arts, prog) + + basic.append(RadioSetting( + "priority_revert", "Priority Revert", + RadioSettingValueBoolean(_settings.priority_revert))) + + basic.append(RadioSetting( + "memory_only", "Memory Only mode", + RadioSettingValueBoolean(_settings.memory_only))) + + opts = ["off"] + ["%0.1f" % (t / 60.0) for t in range(30, 750, 30)] + basic.append(RadioSetting( + "apo", "APO time (hrs)", + RadioSettingValueList(opts, opts[_settings.apo]))) + + basic.append(RadioSetting( + "beep_scan", "Beep: Scan", + RadioSettingValueBoolean(_settings.beep_scan))) + + basic.append(RadioSetting( + "beep_edge", "Beep: Edge", + RadioSettingValueBoolean(_settings.beep_edge))) + + basic.append(RadioSetting( + "beep_key", "Beep: Key", + RadioSettingValueBoolean(_settings.beep_key))) + + opts = ["T/RX Normal", "RX Reverse", "TX Reverse", "T/RX Reverse"] + basic.append(RadioSetting( + "dcs_polarity", "DCS polarity", + RadioSettingValueList(opts, opts[_settings.dcs_polarity]))) + + opts = ["off", "dim 1", "dim 2", "dim 3"] + basic.append(RadioSetting( + "dimmer", "Dimmer", + RadioSettingValueList(opts, opts[_settings.dimmer]))) + + opts = ["manual", "auto", "1-auto"] + basic.append(RadioSetting( + "hyper_write", "Hyper Write", + RadioSettingValueList(opts, opts[_settings.hyper_write]))) + + opts = ["", "key", "dial", "key+dial", "ptt", + "ptt+key", "ptt+dial", "all"] + basic.append(RadioSetting( + "lock", "Lock mode", + RadioSettingValueList(opts, opts[_settings.lock]))) + + opts = ["MH-42", "MH-48"] + basic.append(RadioSetting( + "microphone_type", "Microphone Type", + RadioSettingValueList(opts, opts[_settings.microphone_type]))) + + opts = ["off"] + ["S-%d" % n for n in range(2, 10)] + ["S-Full"] + basic.append(RadioSetting( + "rf_sql", "RF Squelch", + RadioSettingValueList(opts, opts[_settings.rf_sql]))) + + opts = ["time", "hold", "busy"] + basic.append(RadioSetting( + "scan_resume", "Scan Resume", + RadioSettingValueList(opts, opts[_settings.scan_resume]))) + + opts = ["single", "continuous"] + basic.append(RadioSetting( + "smart_search", "Smart Search", + RadioSettingValueList(opts, opts[_settings.smart_search]))) + + opts = ["off"] + ["%d" % t for t in range(1, 31)] + basic.append(RadioSetting( + "tot", "Time-out timer (mins)", + RadioSettingValueList(opts, opts[_settings.tot]))) + + # dtmf tab + + opts = ["50", "100", "250", "450", "750", "1000"] + dtmf.append(RadioSetting( + "dtmf_delay", "DTMF delay (ms)", + RadioSettingValueList(opts, opts[_settings.dtmf_delay]))) + + opts = ["50", "75", "100"] + dtmf.append(RadioSetting( + "dtmf_speed", "DTMF speed (ms)", + RadioSettingValueList(opts, opts[_settings.dtmf_speed]))) + + for i in range(16): + name = "dtmf%02d" % i + dtmfsetting = self._memobj.dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c == 0xFF: + break + if c < len(DTMFCHARSET): + dtmfstr += DTMFCHARSET[c] + LOG.debug(dtmfstr) + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + dtmfentry.set_charset(DTMFCHARSET + list(" ")) + rs = RadioSetting(name, name.upper(), dtmfentry) + dtmf.append(rs) + + # arts tab + + opts = ["off", "in range", "always"] + arts.append(RadioSetting( + "arts_mode", "ARTS beep", + RadioSettingValueList(opts, opts[_settings.arts_mode]))) + + opts = ["15", "25"] + arts.append(RadioSetting( + "arts_interval", "ARTS interval", + RadioSettingValueList(opts, opts[_settings.arts_interval]))) + + arts.append(RadioSetting( + "arts_cwid_enable", "CW ID", + RadioSettingValueBoolean(_settings.arts_cwid_enable))) + + _arts_cwid = self._memobj.arts_cwid + cwid = RadioSettingValueString( + 0, 16, self._decode_chars(_arts_cwid.get_value())) + cwid.set_charset(CHARSET) + arts.append(RadioSetting("arts_cwid", "CW ID", cwid)) + + # prog buttons + + opts = ["WX", "Reverse", "Repeater", "SQL Off", "Lock", "Dimmer"] + prog.append(RadioSetting( + "prog_panel_acc", "Prog Panel - Low(ACC)", + RadioSettingValueList(opts, opts[_settings.prog_panel_acc]))) + + opts = ["Reverse", "Home"] + prog.append(RadioSetting( + "prog_tone_vm", "TONE | V/M", + RadioSettingValueList(opts, opts[_settings.prog_tone_vm]))) + + opts = ["" for n in range(26)] + \ + ["Priority", "Low", "Tone", "MHz", "Reverse", "Home", "Band", + "VFO/MR", "Scan", "Sql Off", "TCall", "SSCH", "ARTS", "Tone Freq", + "DCSC", "WX", "Repeater"] + + prog.append(RadioSetting( + "prog_p1", "P1", + RadioSettingValueList(opts, opts[_settings.prog_p1]))) + + prog.append(RadioSetting( + "prog_p2", "P2", + RadioSettingValueList(opts, opts[_settings.prog_p2]))) + + prog.append(RadioSetting( + "prog_p3", "P3", + RadioSettingValueList(opts, opts[_settings.prog_p3]))) + + prog.append(RadioSetting( + "prog_p4", "P4", + RadioSettingValueList(opts, opts[_settings.prog_p4]))) + + return top + + def set_settings(self, uisettings): + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + _settings = self._memobj.settings + setting = element.get_name() + if re.match('dtmf\d', setting): + # set dtmf fields + dtmfstr = str(element.value).strip() + newval = [] + for i in range(0, 16): + if i < len(dtmfstr): + newval.append(DTMFCHARSET.index(dtmfstr[i])) + else: + newval.append(0xFF) + LOG.debug(newval) + idx = int(setting[-2:]) + _settings = self._memobj.dtmf[idx] + _settings.memory = newval + continue + if setting == "arts_cwid": + oldval = self._memobj.arts_cwid + newval = self._encode_chars(newval.get_value(), 6) + self._memobj.arts_cwid = newval + continue + # normal settings + newval = element.value + oldval = getattr(_settings, setting) + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception as e: + LOG.debug(element.get_name()) + raise + +MEM_FORMAT_8800 = """ +#seekto 0x%X; +struct { + u8 used:1, + unknown1:1, + mode:2, + unknown2:1, + duplex:3; + bbcd freq[3]; + u8 unknown3:1, + tune_step:3, + power:2, + tmode:2; + bbcd split[3]; + u8 nameused:1, + unknown5:1, + tone:6; + u8 namevalid:1, + dtcs:7; + u8 name[6]; +} memory[500]; + +#seekto 0x51C8; +struct { + u8 skip0:2, + skip1:2, + skip2:2, + skip3:2; +} flags[250]; + +#seekto 0x%X; +struct { + u32 bitmap[16]; +} bank_channels[10]; + + +#seekto 0x7B48; +u8 checksum; +""" + + +class FT8800BankModel(FT7800BankModel): + def get_num_mappings(self): + return 10 + + +@directory.register +class FT8800Radio(FTx800Radio): + """Base class for Yaesu FT-8800""" + MODEL = "FT-8800" + + _model = "AH018" + _memsize = 22217 + + _block_lengths = [8, 22208, 1] + + _memstart = 0x0000 + + TMODES = ["", "Tone", "TSQL", "DTCS"] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA jack. + 3. Press and hold in the "left" [V/M] key while turning the + radio on. + 4. Rotate the "right" DIAL knob to select "CLONE START". + 5. Press the [SET] key. The display will disappear + for a moment, then the "CLONE" notation will appear. + 6. After clicking OK, press the "left" [V/M] key to + send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA jack. + 3. Press and hold in the "left" [V/M] key while turning the + radio on. + 4. Rotate the "right" DIAL knob to select "CLONE START". + 5. Press the [SET] key. The display will disappear + for a moment, then the "CLONE" notation will appear. + 6. Press the "left" [LOW] key ("CLONE -RX-" will appear on + the display).""")) + return rp + + def get_features(self): + rf = FTx800Radio.get_features(self) + rf.has_sub_devices = self.VARIANT == "" + rf.has_bank = True + rf.memory_bounds = (1, 500) + return rf + + def get_sub_devices(self): + return [FT8800RadioLeft(self._mmap), FT8800RadioRight(self._mmap)] + + def get_bank_model(self): + return FT8800BankModel(self) + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x56C7)] + + def process_mmap(self): + if not self._memstart: + return + + self._memobj = bitwise.parse(MEM_FORMAT_8800 % (self._memstart, + self._bankstart), + self._mmap) + + def _get_mem_offset(self, mem, _mem): + if mem.duplex == "split": + return get_freq(int(_mem.split) * 10000) + + # The offset is packed into the upper two bits of the last four + # bytes of the name (?!) + val = 0 + for i in _mem.name[2:6]: + val <<= 2 + val |= ((i & 0xC0) >> 6) + + return (val * 5) * 10000 + + def _set_mem_offset(self, mem, _mem): + if mem.duplex == "split": + set_freq(mem.offset, _mem, "split") + return + + val = int(mem.offset / 10000) // 5 + for i in reversed(list(range(2, 6))): + _mem.name[i] = (_mem.name[i] & 0x3F) | ((val & 0x03) << 6) + val >>= 2 + + def _get_mem_name(self, mem, _mem): + name = "" + if _mem.namevalid: + for i in _mem.name: + index = int(i) & 0x3F + if index < len(CHARSET): + name += CHARSET[index] + + return name.rstrip() + + def _set_mem_name(self, mem, _mem): + _mem.name = [CHARSET.index(x) for x in mem.name.ljust(6)[:6]] + _mem.namevalid = 1 + _mem.nameused = bool(mem.name.rstrip()) + + +class FT8800RadioLeft(FT8800Radio): + """Yaesu FT-8800 Left VFO subdevice""" + VARIANT = "Left" + _memstart = 0x0948 + _bankstart = 0x4BC8 + + +class FT8800RadioRight(FT8800Radio): + """Yaesu FT-8800 Right VFO subdevice""" + VARIANT = "Right" + _memstart = 0x2948 + _bankstart = 0x4BC8 + +MEM_FORMAT_8900 = """ +#seekto 0x0708; +struct { + u8 used:1, + skip:2, + sub_used:1, + unknown2:1, + duplex:3; + bbcd freq[3]; + u8 mode:2, + nameused:1, + unknown4:1, + power:2, + tmode:2; + bbcd split[3]; + u8 unknown5:2, + tone:6; + u8 namevalid:1, + dtcs:7; + u8 name[6]; +} memory[799]; + +#seekto 0x51C8; +struct { + u8 skip0:2, + skip1:2, + skip2:2, + skip3:2; +} flags[400]; + +#seekto 0x7B48; +u8 checksum; +""" + + +@directory.register +class FT8900Radio(FT8800Radio): + """Yaesu FT-8900""" + MODEL = "FT-8900" + + _model = "AH008" + _memsize = 14793 + _block_lengths = [8, 14784, 1] + + MODES = ["FM", "NFM", "AM"] + + def get_bank_model(self): + return + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT_8900, self._mmap) + + def get_features(self): + rf = FT8800Radio.get_features(self) + rf.has_sub_devices = False + rf.has_bank = False + rf.valid_modes = self.MODES + rf.valid_bands = [(28000000, 29700000), + (50000000, 54000000), + (108000000, 180000000), + (320000000, 480000000), + (700000000, 985000000)] + rf.memory_bounds = (1, 799) + rf.has_tuning_step = False + + return rf + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x39C7)] + + def _get_mem_skip(self, mem, _mem): + return SKIPS[_mem.skip] + + def _set_mem_skip(self, mem, _mem): + _mem.skip = SKIPS.index(mem.skip) + + def get_memory(self, number): + mem = FT8800Radio.get_memory(self, number) + + _mem = self._memobj.memory[number - 1] + + return mem + + def set_memory(self, mem): + FT8800Radio.set_memory(self, mem) + + # The 8900 has a bit flag that tells the radio whether or not + # the memory should show up on the sub (right) band + _mem = self._memobj.memory[mem.number - 1] + if mem.freq < 108000000 or mem.freq > 480000000: + _mem.sub_used = 0 + else: + _mem.sub_used = 1 diff --git a/chirp/drivers/ft8100.py b/chirp/drivers/ft8100.py new file mode 100644 index 0000000..1d238c3 --- /dev/null +++ b/chirp/drivers/ft8100.py @@ -0,0 +1,314 @@ +# Copyright 2010 Eric Allen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os + +from chirp import chirp_common, directory, bitwise, errors +from chirp.drivers import yaesu_clone + +TONES = chirp_common.OLD_TONES + +TMODES = ["", "Tone"] + +MODES = ['FM', 'AM'] + +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] + +DUPLEX = ["", "-", "+", "split"] + +# "M" for masked memories, which are invisible until un-masked +SKIPS = ["", "S", "M"] + +POWER_LEVELS_VHF = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=20), + chirp_common.PowerLevel("High", watts=50)] + +POWER_LEVELS_UHF = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=20), + chirp_common.PowerLevel("High", watts=35)] + +SPECIALS = {'1L': -1, + '1U': -2, + '2L': -3, + '2U': -4, + 'Home': -5} + +MEM_FORMAT = """ +#seekto 0x{skips:X}; +u8 skips[13]; + +#seekto 0x{enables:X}; +u8 enables[13]; + +struct mem_struct {{ + u8 unknown4:2, + baud9600:1, + am:1, + unknown4b:4; + u8 power:2, + duplex:2, + unknown1b:4; + u8 unknown2:1, + tone_enable:1, + tone:6; + bbcd freq[3]; + bbcd offset[3]; +}}; + +#seekto 0x{memories:X}; +struct mem_struct memory[99]; +""" + + +@directory.register +class FT8100Radio(yaesu_clone.YaesuCloneModeRadio): + """Implementation for Yaesu FT-8100""" + MODEL = "FT-8100" + + _memstart = 0 + _memsize = 2968 + _block_lengths = [10, 32, 114, 101, 101, 97, 128, 128, 128, 128, 128, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 1] + + @classmethod + def match_model(cls, data, path): + if (len(data) == cls._memsize and + data[1:10] == '\x01\x01\x07\x08\x02\x01\x01\x00\x01'): + return True + + return False + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 99) + rf.has_ctone = False + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_name = False + + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_power_levels = POWER_LEVELS_VHF + rf.has_sub_devices = self.VARIANT == '' + + rf.valid_tuning_steps = list(STEPS) + + rf.valid_bands = [(110000000, 550000000), + (750000000, 1300000000)] + + rf.valid_skips = SKIPS + + rf.can_odd_split = True + + # TODO + #rf.valid_special_chans = SPECIALS.keys() + + # TODO + #rf.has_tuning_step = False + return rf + + def sync_in(self): + super(FT8100Radio, self).sync_in() + self.pipe.write(chr(yaesu_clone.CMD_ACK)) + self.pipe.read(1) + + def sync_out(self): + self.update_checksums() + return _clone_out(self) + + def process_mmap(self): + if not self._memstart: + return + + mem_format = MEM_FORMAT.format(memories=self._memstart, + skips=self._skipstart, + enables=self._enablestart + ) + + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_sub_devices(self): + return [FT8100RadioVHF(self._mmap), FT8100RadioUHF(self._mmap)] + + def get_memory(self, number): + bit, byte = self._bit_byte(number) + + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + mem.freq = int(_mem.freq) * 1000 + if _mem.tone >= len(TONES) or _mem.duplex >= len(DUPLEX): + mem.empty = True + return mem + else: + mem.rtone = TONES[_mem.tone] + mem.tmode = TMODES[_mem.tone_enable] + mem.mode = MODES[_mem.am] + mem.duplex = DUPLEX[_mem.duplex] + + if _mem.duplex == DUPLEX.index("split"): + tx_freq = int(_mem.offset) * 1000 + print self.VARIANT, number, tx_freq, mem.freq + mem.offset = tx_freq - mem.freq + else: + mem.offset = int(_mem.offset) * 1000 + + if int(mem.freq / 100) == 4: + mem.power = POWER_LEVELS_UHF[_mem.power] + else: + mem.power = POWER_LEVELS_VHF[_mem.power] + + # M01 can't be disabled + if not self._memobj.enables[byte] & bit and number != 1: + mem.empty = True + + print 'R', self.VARIANT, number, _mem.baud9600 + + return mem + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def set_memory(self, mem): + bit, byte = self._bit_byte(mem.number) + + _mem = self._memobj.memory[mem.number - 1] + + _mem.freq = int(mem.freq / 1000) + _mem.tone = TONES.index(mem.rtone) + _mem.tone_enable = TMODES.index(mem.tmode) + _mem.am = MODES.index(mem.mode) + _mem.duplex = DUPLEX.index(mem.duplex) + + if mem.duplex == "split": + tx_freq = mem.freq + mem.offset + _mem.split_high = tx_freq / 10000000 + _mem.offset = (tx_freq % 10000000) / 1000 + else: + _mem.offset = int(mem.offset / 1000) + + if mem.power: + _mem.power = POWER_LEVELS_VHF.index(mem.power) + else: + _mem.power = 0 + + if mem.empty: + self._memobj.enables[byte] &= ~bit + else: + self._memobj.enables[byte] |= bit + + # TODO expose these options + _mem.baud9600 = 0 + _mem.am = 0 + + # These need to be cleared, otherwise strange things happen + _mem.unknown4 = 0 + _mem.unknown4b = 0 + _mem.unknown1b = 0 + _mem.unknown2 = 0 + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x0B96)] + + # I didn't believe this myself, but it seems that there's a bit for + # enabling VHF M01, but no bit for UHF01, and the enables are shifted down, + # so that the first bit is for M02 + def _bit_byte(self, number): + if self.VARIANT == 'VHF': + bit = 1 << ((number - 1) % 8) + byte = (number - 1) / 8 + else: + bit = 1 << ((number - 2) % 8) + byte = (number - 2) / 8 + + return bit, byte + + +class FT8100RadioVHF(FT8100Radio): + """Yaesu FT-8100 VHF subdevice""" + VARIANT = "VHF" + _memstart = 0x447 + _skipstart = 0x02D + _enablestart = 0x04D + + +class FT8100RadioUHF(FT8100Radio): + """Yaesu FT-8100 UHF subdevice""" + VARIANT = "UHF" + _memstart = 0x7E6 + _skipstart = 0x03A + _enablestart = 0x05A + + +def _clone_out(radio): + try: + return __clone_out(radio) + except Exception, e: + raise errors.RadioError("Failed to communicate with the radio: %s" % e) + + +def __clone_out(radio): + pipe = radio.pipe + block_lengths = radio._block_lengths + total_written = 0 + + def _status(): + status = chirp_common.Status() + status.msg = "Cloning to radio" + status.max = sum(block_lengths) + status.cur = total_written + radio.status_fn(status) + + start = time.time() + + pos = 0 + for block in radio._block_lengths: + if os.getenv("CHIRP_DEBUG"): + print "\nSending %i-%i" % (pos, pos + block) + out = radio.get_mmap()[pos:pos + block] + + # need to chew byte-by-byte here or else we lose the ACK...not sure why + for b in out: + pipe.write(b) + pipe.read(1) # chew the echo + + ack = pipe.read(1) + + if ack != chr(yaesu_clone.CMD_ACK): + raise Exception("block not ack'ed: %s" % repr(ack)) + + total_written += len(out) + _status() + + pos += block + + print "Clone completed in %i seconds" % (time.time() - start) + + return True diff --git a/chirp/drivers/ft817.py b/chirp/drivers/ft817.py new file mode 100644 index 0000000..39fb1b6 --- /dev/null +++ b/chirp/drivers/ft817.py @@ -0,0 +1,1214 @@ +# +# Copyright 2012 Filippi Marco +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""FT817 - FT817ND - FT817ND/US management module""" + +from builtins import bytes +from chirp.drivers import yaesu_clone +from chirp import chirp_common, util, memmap, errors, directory, bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +import time +import logging +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +CMD_ACK = 0x06 + + +@directory.register +class FT817Radio(yaesu_clone.YaesuCloneModeRadio): + + """Yaesu FT-817""" + BAUD_RATE = 9600 + MODEL = "FT-817" + NEEDS_COMPAT_SERIAL = False + _model = "" + _US_model = False + + DUPLEX = ["", "-", "+", "split"] + # narrow modes has to be at end + MODES = ["LSB", "USB", "CW", "CWR", "AM", "FM", "DIG", "PKT", "NCW", + "NCWR", "NFM"] + TMODES = ["", "Tone", "TSQL", "DTCS"] + STEPSFM = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] + STEPSAM = [2.5, 5.0, 9.0, 10.0, 12.5, 25.0] + STEPSSSB = [1.0, 2.5, 5.0] + + # warning ranges has to be in this exact order + VALID_BANDS = [(100000, 33000000), (33000000, 56000000), + (76000000, 108000000), (108000000, 137000000), + (137000000, 154000000), (420000000, 470000000)] + + CHARSET = list(chirp_common.CHARSET_ASCII) + CHARSET.remove("\\") + + _memsize = 6509 + # block 9 (130 Bytes long) is to be repeted 40 times + _block_lengths = [2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 118] + + MEM_FORMAT = """ + struct mem_struct { + u8 tag_on_off:1, + tag_default:1, + unknown1:3, + mode:3; + u8 duplex:2, + is_duplex:1, + is_cwdig_narrow:1, + is_fm_narrow:1, + freq_range:3; + u8 skip:1, + unknown2:1, + ipo:1, + att:1, + unknown3:4; + u8 ssb_step:2, + am_step:3, + fm_step:3; + u8 unknown4:6, + tmode:2; + u8 unknown5:2, + tx_mode:3, + tx_freq_range:3; + u8 unknown6:1, + unknown_toneflag:1, + tone:6; + u8 unknown7:1, + dcs:7; + ul16 rit; + u32 freq; + u32 offset; + u8 name[8]; + }; + + #seekto 0x4; + struct { + u8 fst:1, + lock:1, + nb:1, + pbt:1, + unknownb:1, + dsp:1, + agc:2; + u8 vox:1, + vlt:1, + bk:1, + kyr:1, + unknown5:1, + cw_paddle:1, + pwr_meter_mode:2; + u8 vfob_band_select:4, + vfoa_band_select:4; + u8 unknowna; + u8 backlight:2, + color:2, + contrast:4; + u8 beep_freq:1, + beep_volume:7; + u8 arts_beep:2, + main_step:1, + cw_id:1, + scope:1, + pkt_rate:1, + resume_scan:2; + u8 op_filter:2, + lock_mode:2, + cw_pitch:4; + u8 sql_rf_gain:1, + ars_144:1, + ars_430:1, + cw_weight:5; + u8 cw_delay; + u8 unknown8:1, + sidetone:7; + u8 batt_chg:2, + cw_speed:6; + u8 disable_amfm_dial:1, + vox_gain:7; + u8 cat_rate:2, + emergency:1, + vox_delay:5; + u8 dig_mode:3, + mem_group:1, + unknown9:1, + apo_time:3; + u8 dcs_inv:2, + unknown10:1, + tot_time:5; + u8 mic_scan:1, + ssb_mic:7; + u8 mic_key:1, + am_mic:7; + u8 unknown11:1, + fm_mic:7; + u8 unknown12:1, + dig_mic:7; + u8 extended_menu:1, + pkt_mic:7; + u8 unknown14:1, + pkt9600_mic:7; + il16 dig_shift; + il16 dig_disp; + i8 r_lsb_car; + i8 r_usb_car; + i8 t_lsb_car; + i8 t_usb_car; + u8 unknown15:2, + menu_item:6; + u8 unknown16:4, + menu_sel:4; + u16 unknown17; + u8 art:1, + scn_mode:2, + dw:1, + pri:1, + unknown18:1, + tx_power:2; + u8 spl:1, + unknown:1, + uhf_antenna:1, + vhf_antenna:1, + air_antenna:1, + bc_antenna:1, + sixm_antenna:1, + hf_antenna:1; + } settings; + + #seekto 0x2A; + struct mem_struct vfoa[15]; + struct mem_struct vfob[15]; + struct mem_struct home[4]; + struct mem_struct qmb; + struct mem_struct mtqmb; + struct mem_struct mtune; + + #seekto 0x3FD; + u8 visible[25]; + u8 pmsvisible; + + #seekto 0x417; + u8 filled[25]; + u8 pmsfilled; + + #seekto 0x431; + struct mem_struct memory[200]; + struct mem_struct pms[2]; + + #seekto 0x18cf; + u8 callsign[7]; + + #seekto 0x1979; + struct mem_struct sixtymeterchannels[5]; + """ + _CALLSIGN_CHARSET = [chr(x) for x in list(range(ord("0"), ord("9") + 1)) + + list(range(ord("A"), ord("Z") + 1)) + [ord(" ")]] + _CALLSIGN_CHARSET_REV = dict( + list(zip(_CALLSIGN_CHARSET, + list(range(0, len(_CALLSIGN_CHARSET)))))) + + # WARNING Index are hard wired in memory management code !!! + SPECIAL_MEMORIES = { + "VFOa-1.8M": -35, + "VFOa-3.5M": -34, + "VFOa-7M": -33, + "VFOa-10M": -32, + "VFOa-14M": -31, + "VFOa-18M": -30, + "VFOa-21M": -29, + "VFOa-24M": -28, + "VFOa-28M": -27, + "VFOa-50M": -26, + "VFOa-FM": -25, + "VFOa-AIR": -24, + "VFOa-144": -23, + "VFOa-430": -22, + "VFOa-HF": -21, + "VFOb-1.8M": -20, + "VFOb-3.5M": -19, + "VFOb-7M": -18, + "VFOb-10M": -17, + "VFOb-14M": -16, + "VFOb-18M": -15, + "VFOb-21M": -14, + "VFOb-24M": -13, + "VFOb-28M": -12, + "VFOb-50M": -11, + "VFOb-FM": -10, + "VFOb-AIR": -9, + "VFOb-144M": -8, + "VFOb-430M": -7, + "VFOb-HF": -6, + "HOME HF": -5, + "HOME 50M": -4, + "HOME 144M": -3, + "HOME 430M": -2, + "QMB": -1, + } + FIRST_VFOB_INDEX = -6 + LAST_VFOB_INDEX = -20 + FIRST_VFOA_INDEX = -21 + LAST_VFOA_INDEX = -35 + + SPECIAL_PMS = { + "PMS-L": -37, + "PMS-U": -36, + } + LAST_PMS_INDEX = -37 + + SPECIAL_MEMORIES.update(SPECIAL_PMS) + + SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()), + list(SPECIAL_MEMORIES.keys())))) + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. After clicking OK, press the [A] key to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. Press the [C] key ("RX" will appear on the LCD).""")) + return rp + + def _read(self, block, blocknum, lastblock): + # be very patient at first block + if blocknum == 0: + attempts = 60 + else: + attempts = 5 + for _i in range(0, attempts): + data = self.pipe.read(block + 2) + if data: + break + time.sleep(0.5) + if len(data) == block + 2 and data[0] == blocknum: + checksum = yaesu_clone.YaesuChecksum(1, block) + if checksum.get_existing(data) != \ + checksum.get_calculated(data): + raise Exception("Checksum Failed [%02X<>%02X] block %02X" % + (checksum.get_existing(data), + checksum.get_calculated(data), blocknum)) + # Chew away the block number and the checksum + data = data[1:block + 1] + else: + if lastblock and self._US_model: + raise Exception(_("Unable to read last block. " + "This often happens when the selected model " + "is US but the radio is a non-US one (or " + "widebanded). Please choose the correct " + "model and try again.")) + else: + raise Exception("Unable to read block %02X expected %i got %i" + % (blocknum, block + 2, len(data))) + + LOG.debug("Read %i" % len(data)) + return data + + def _clone_in(self): + # Be very patient with the radio + self.pipe.timeout = 2 + + start = time.time() + + data = bytes(b"") + blocks = 0 + status = chirp_common.Status() + status.msg = _("Cloning from radio") + nblocks = len(self._block_lengths) + 39 + status.max = nblocks + for block in self._block_lengths: + if blocks == 8: + # repeated read of 40 block same size (memory area) + repeat = 40 + else: + repeat = 1 + for _i in range(0, repeat): + data += self._read(block, blocks, blocks == nblocks - 1) + self.pipe.write(bytes([CMD_ACK])) + blocks += 1 + status.cur = blocks + self.status_fn(status) + + if not self._US_model: + status.msg = _("Clone completed, checking for spurious bytes") + self.status_fn(status) + moredata = self.pipe.read(2) + if moredata: + raise Exception( + _("Radio sent data after the last awaited block, " + "this happens when the selected model is a non-US " + "but the radio is a US one. " + "Please choose the correct model and try again.")) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + return memmap.MemoryMapBytes(data) + + def _clone_out(self): + delay = 0.5 + start = time.time() + + blocks = 0 + pos = 0 + status = chirp_common.Status() + status.msg = _("Cloning to radio") + status.max = len(self._block_lengths) + 39 + mmap = self.get_mmap().get_byte_compatible() + for block in self._block_lengths: + if blocks == 8: + # repeated read of 40 block same size (memory area) + repeat = 40 + else: + repeat = 1 + for _i in range(0, repeat): + time.sleep(0.01) + checksum = yaesu_clone.YaesuChecksum(pos, pos + block - 1) + LOG.debug("Block %i - will send from %i to %i byte " % + (blocks, pos, pos + block)) + LOG.debug(util.hexprint(chr(blocks))) + LOG.debug(util.hexprint(self.get_mmap()[pos:pos + block])) + LOG.debug(util.hexprint(chr(checksum.get_calculated(mmap)))) + self.pipe.write(bytes([blocks])) + self.pipe.write(mmap[pos:pos + block]) + self.pipe.write(bytes([checksum.get_calculated( + self.get_mmap())])) + buf = self.pipe.read(1) + if not buf or buf[0] != CMD_ACK: + time.sleep(delay) + buf = self.pipe.read(1) + if not buf or buf[0] != CMD_ACK: + LOG.debug(util.hexprint(buf)) + raise Exception(_("Radio did not ack block %i") % blocks) + pos += block + blocks += 1 + status.cur = blocks + self.status_fn(status) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + def sync_in(self): + try: + self._mmap = self._clone_in() + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + self._clone_out() + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_dtcs_polarity = False + rf.has_nostep_tuning = True + rf.valid_modes = list(set(self.MODES)) + rf.valid_tmodes = list(self.TMODES) + rf.valid_duplexes = list(self.DUPLEX) + rf.valid_tuning_steps = list(self.STEPSFM) + rf.valid_bands = self.VALID_BANDS + rf.valid_skips = ["", "S"] + rf.valid_power_levels = [] + rf.valid_characters = "".join(self.CHARSET) + rf.valid_name_length = 8 + rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys()) + rf.memory_bounds = (1, 200) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_duplex(self, mem, _mem): + if _mem.is_duplex == 1: + mem.duplex = self.DUPLEX[_mem.duplex] + else: + mem.duplex = "" + + def _get_tmode(self, mem, _mem): + mem.tmode = self.TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + + def _set_duplex(self, mem, _mem): + _mem.duplex = self.DUPLEX.index(mem.duplex) + _mem.is_duplex = mem.duplex != "" + + def _set_tmode(self, mem, _mem): + _mem.tmode = self.TMODES.index(mem.tmode) + # have to put this bit to 0 otherwise we get strange display in tone + # frequency (menu 83). See bug #88 and #163 + _mem.unknown_toneflag = 0 + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + + def get_memory(self, number): + if isinstance(number, str): + return self._get_special(number) + elif number < 0: + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special(self.SPECIAL_MEMORIES_REV[number]) + else: + return self._get_normal(number) + + def set_memory(self, memory): + if memory.number < 0: + return self._set_special(memory) + else: + return self._set_normal(memory) + + def _get_special(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_MEMORIES[number] + mem.extd_number = number + + if mem.number in range(self.FIRST_VFOA_INDEX, + self.LAST_VFOA_INDEX - 1, + -1): + _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number] + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number in range(self.FIRST_VFOB_INDEX, + self.LAST_VFOB_INDEX - 1, + -1): + _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number] + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number in range(-2, -6, -1): + _mem = self._memobj.home[5 + mem.number] + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number == -1: + _mem = self._memobj.qmb + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number in list(self.SPECIAL_PMS.values()): + bitindex = -self.LAST_PMS_INDEX + mem.number + used = (self._memobj.pmsvisible >> bitindex) & 0x01 + valid = (self._memobj.pmsfilled >> bitindex) & 0x01 + if not used: + mem.empty = True + if not valid: + mem.empty = True + return mem + _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number] + immutable = ["number", "skip", "rtone", "ctone", "extd_number", + "dtcs", "tmode", "cross_mode", "dtcs_polarity", + "power", "duplex", "offset", "comment"] + else: + raise Exception("Sorry, special memory index %i " % mem.number + + "unknown you hit a bug!!") + + mem = self._get_memory(mem, _mem) + mem.immutable = immutable + + return mem + + def _set_special(self, mem): + if mem.empty and mem.number not in list(self.SPECIAL_PMS.values()): + # can't delete special memories! + raise Exception("Sorry, special memory can't be deleted") + + cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[mem.number]) + + # TODO add frequency range check for vfo and home memories + if mem.number in range(self.FIRST_VFOA_INDEX, + self.LAST_VFOA_INDEX - 1, + -1): + _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number] + elif mem.number in range(self.FIRST_VFOB_INDEX, + self.LAST_VFOB_INDEX - 1, + -1): + _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number] + elif mem.number in range(-2, -6, -1): + _mem = self._memobj.home[5 + mem.number] + elif mem.number == -1: + _mem = self._memobj.qmb + elif mem.number in list(self.SPECIAL_PMS.values()): + # this case has to be last because 817 pms keys overlap with + # 857 derived class other special memories + bitindex = -self.LAST_PMS_INDEX + mem.number + wasused = (self._memobj.pmsvisible >> bitindex) & 0x01 + wasvalid = (self._memobj.pmsfilled >> bitindex) & 0x01 + if mem.empty: + if wasvalid and not wasused: + # pylint get confused by &= operator + self._memobj.pmsfilled = self._memobj.pmsfilled & \ + ~ (1 << bitindex) + # pylint get confused by &= operator + self._memobj.pmsvisible = self._memobj.pmsvisible & \ + ~ (1 << bitindex) + return + # pylint get confused by |= operator + self._memobj.pmsvisible = self._memobj.pmsvisible | 1 << bitindex + self._memobj.pmsfilled = self._memobj.pmsfilled | 1 << bitindex + _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number] + else: + raise Exception("Sorry, special memory index %i " % mem.number + + "unknown you hit a bug!!") + + for key in cur_mem.immutable: + if key != "extd_number": + if cur_mem.__dict__[key] != mem.__dict__[key]: + raise errors.RadioError("Editing field `%s' " % key + + "is not supported on this channel") + + self._set_memory(mem, _mem) + + def _get_normal(self, number): + _mem = self._memobj.memory[number - 1] + used = (self._memobj.visible[(number - 1) / 8] >> (number - 1) % 8) \ + & 0x01 + valid = (self._memobj.filled[(number - 1) / 8] >> (number - 1) % 8) \ + & 0x01 + + mem = chirp_common.Memory() + mem.number = number + if not used: + mem.empty = True + if not valid or _mem.freq == 0xffffffff: + return mem + + return self._get_memory(mem, _mem) + + def _set_normal(self, mem): + _mem = self._memobj.memory[mem.number - 1] + wasused = (self._memobj.visible[(mem.number - 1) / 8] >> + (mem.number - 1) % 8) & 0x01 + wasvalid = (self._memobj.filled[(mem.number - 1) / 8] >> + (mem.number - 1) % 8) & 0x01 + + if mem.empty: + if mem.number == 1: + # as Dan says "yaesus are not good about that :(" + # if you ulpoad an empty image you can brick your radio + raise Exception("Sorry, can't delete first memory") + if wasvalid and not wasused: + self._memobj.filled[(mem.number - 1) // 8] &= \ + ~(1 << (mem.number - 1) % 8) + _mem.set_raw("\xFF" * (_mem.size() // 8)) # clean up + self._memobj.visible[(mem.number - 1) // 8] &= \ + ~(1 << (mem.number - 1) % 8) + return + if not wasvalid: + _mem.set_raw("\x00" * (_mem.size() // 8)) # clean up + + self._memobj.visible[(mem.number - 1) // 8] |= ( + 1 << (mem.number - 1) % 8) + self._memobj.filled[(mem.number - 1) // 8] |= ( + 1 << (mem.number - 1) % 8) + self._set_memory(mem, _mem) + + def _get_memory(self, mem, _mem): + mem.freq = int(_mem.freq) * 10 + mem.offset = int(_mem.offset) * 10 + self._get_duplex(mem, _mem) + mem.mode = self.MODES[_mem.mode] + if mem.mode == "FM": + if _mem.is_fm_narrow == 1: + mem.mode = "NFM" + mem.tuning_step = self.STEPSFM[_mem.fm_step] + elif mem.mode == "AM": + mem.tuning_step = self.STEPSAM[_mem.am_step] + elif mem.mode == "CW" or mem.mode == "CWR": + if _mem.is_cwdig_narrow == 1: + mem.mode = "N" + mem.mode + mem.tuning_step = self.STEPSSSB[_mem.ssb_step] + else: + try: + mem.tuning_step = self.STEPSSSB[_mem.ssb_step] + except IndexError: + pass + mem.skip = _mem.skip and "S" or "" + self._get_tmode(mem, _mem) + + if _mem.tag_on_off == 1: + for i in _mem.name: + if i == 0xFF: + break + if chr(i) in self.CHARSET: + mem.name += chr(i) + else: + # radio have some graphical chars that are not supported + # we replace those with a * + LOG.info("Replacing char %x with *" % i) + mem.name += "*" + mem.name = mem.name.rstrip() + else: + mem.name = "" + + mem.extra = RadioSettingGroup("extra", "Extra") + ipo = RadioSetting("ipo", "IPO", + RadioSettingValueBoolean(bool(_mem.ipo))) + ipo.set_doc("Bypass preamp") + mem.extra.append(ipo) + + att = RadioSetting("att", "ATT", + RadioSettingValueBoolean(bool(_mem.att))) + att.set_doc("10dB front end attenuator") + mem.extra.append(att) + + return mem + + def _set_memory(self, mem, _mem): + if len(mem.name) > 0: # not supported in chirp + # so I make label visible if have one + _mem.tag_on_off = 1 + else: + _mem.tag_on_off = 0 + _mem.tag_default = 0 # never use default label "CH-nnn" + self._set_duplex(mem, _mem) + if mem.mode[0] == "N": # is it narrow? + _mem.mode = self.MODES.index(mem.mode[1:]) + # here I suppose it's safe to set both + _mem.is_fm_narrow = _mem.is_cwdig_narrow = 1 + else: + _mem.mode = self.MODES.index(mem.mode) + # here I suppose it's safe to set both + _mem.is_fm_narrow = _mem.is_cwdig_narrow = 0 + i = 0 + for lo, hi in self.VALID_BANDS: + if mem.freq > lo and mem.freq < hi: + break + i += 1 + _mem.freq_range = i + # all this should be safe also when not in split but ... + if mem.duplex == "split": + _mem.tx_mode = _mem.mode + i = 0 + for lo, hi in self.VALID_BANDS: + if mem.offset >= lo and mem.offset < hi: + break + i += 1 + _mem.tx_freq_range = i + _mem.skip = mem.skip == "S" + self._set_tmode(mem, _mem) + try: + _mem.ssb_step = self.STEPSSSB.index(mem.tuning_step) + except ValueError: + pass + try: + _mem.am_step = self.STEPSAM.index(mem.tuning_step) + except ValueError: + pass + try: + _mem.fm_step = self.STEPSFM.index(mem.tuning_step) + except ValueError: + pass + _mem.rit = 0 # not supported in chirp + _mem.freq = mem.freq / 10 + _mem.offset = mem.offset / 10 + # there are ft857D that have problems with short labels, see bug #937 + # some of the radio fill with 0xff and some with blanks + # the latter is safe for all ft8x7 radio + # so why should i do it only for some? + for i in range(0, 8): + _mem.name[i] = ord(mem.name.ljust(8)[i]) + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def validate_memory(self, mem): + msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem) + + lo, hi = self.VALID_BANDS[2] # this is fm broadcasting + if mem.freq >= lo and mem.freq <= hi: + if mem.mode != "FM": + msgs.append(chirp_common.ValidationError( + "Only FM is supported in this band")) + # TODO check that step is valid in current mode + return msgs + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + cw = RadioSettingGroup("cw", "CW") + packet = RadioSettingGroup("packet", "Digital & packet") + panel = RadioSettingGroup("panel", "Panel settings") + extended = RadioSettingGroup("extended", "Extended") + antenna = RadioSettingGroup("antenna", "Antenna selection") + panelcontr = RadioSettingGroup("panelcontr", "Panel controls") + + top = RadioSettings(basic, cw, packet, + panelcontr, panel, extended, antenna) + + rs = RadioSetting("ars_144", "144 ARS", + RadioSettingValueBoolean(_settings.ars_144)) + basic.append(rs) + rs = RadioSetting("ars_430", "430 ARS", + RadioSettingValueBoolean(_settings.ars_430)) + basic.append(rs) + rs = RadioSetting("pkt9600_mic", "Paket 9600 mic level", + RadioSettingValueInteger(0, 100, + _settings.pkt9600_mic)) + packet.append(rs) + options = ["enable", "disable"] + rs = RadioSetting("disable_amfm_dial", "AM&FM Dial", + RadioSettingValueList(options, + options[ + _settings.disable_amfm_dial + ])) + panel.append(rs) + rs = RadioSetting("am_mic", "AM mic level", + RadioSettingValueInteger(0, 100, _settings.am_mic)) + basic.append(rs) + options = ["OFF", "1h", "2h", "3h", "4h", "5h", "6h"] + rs = RadioSetting("apo_time", "APO time", + RadioSettingValueList(options, + options[_settings.apo_time])) + basic.append(rs) + options = ["OFF", "Range", "All"] + rs = RadioSetting("arts_beep", "ARTS beep", + RadioSettingValueList(options, + options[_settings.arts_beep])) + basic.append(rs) + options = ["OFF", "ON", "Auto"] + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList(options, + options[_settings.backlight])) + panel.append(rs) + options = ["6h", "8h", "10h"] + rs = RadioSetting("batt_chg", "Battery charge", + RadioSettingValueList(options, + options[_settings.batt_chg])) + basic.append(rs) + options = ["440Hz", "880Hz"] + rs = RadioSetting("beep_freq", "Beep frequency", + RadioSettingValueList(options, + options[_settings.beep_freq])) + panel.append(rs) + rs = RadioSetting("beep_volume", "Beep volume", + RadioSettingValueInteger(0, 100, + _settings.beep_volume)) + panel.append(rs) + options = ["4800", "9600", "38400"] + rs = RadioSetting("cat_rate", "CAT rate", + RadioSettingValueList(options, + options[_settings.cat_rate])) + basic.append(rs) + options = ["Blue", "Amber", "Violet"] + rs = RadioSetting("color", "Color", + RadioSettingValueList(options, + options[_settings.color])) + panel.append(rs) + rs = RadioSetting("contrast", "Contrast", + RadioSettingValueInteger(1, 12, + _settings.contrast - 1)) + panel.append(rs) + rs = RadioSetting("cw_delay", "CW delay (*10 ms)", + RadioSettingValueInteger(1, 250, + _settings.cw_delay)) + cw.append(rs) + rs = RadioSetting("cw_id", "CW id", + RadioSettingValueBoolean(_settings.cw_id)) + cw.append(rs) + options = ["Normal", "Reverse"] + rs = RadioSetting("cw_paddle", "CW paddle", + RadioSettingValueList(options, + options[_settings.cw_paddle])) + cw.append(rs) + options = ["%i Hz" % i for i in range(300, 1001, 50)] + rs = RadioSetting("cw_pitch", "CW pitch", + RadioSettingValueList(options, + options[_settings.cw_pitch])) + cw.append(rs) + options = ["%i wpm" % i for i in range(4, 61)] + rs = RadioSetting("cw_speed", "CW speed", + RadioSettingValueList(options, + options[_settings.cw_speed])) + cw.append(rs) + options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)] + rs = RadioSetting("cw_weight", "CW weight", + RadioSettingValueList(options, + options[_settings.cw_weight])) + cw.append(rs) + rs = RadioSetting("dig_disp", "Dig disp (*10 Hz)", + RadioSettingValueInteger(-300, 300, + _settings.dig_disp)) + packet.append(rs) + rs = RadioSetting("dig_mic", "Dig mic", + RadioSettingValueInteger(0, 100, + _settings.dig_mic)) + packet.append(rs) + options = ["RTTY", "PSK31-L", "PSK31-U", "USER-L", "USER-U"] + rs = RadioSetting("dig_mode", "Dig mode", + RadioSettingValueList(options, + options[_settings.dig_mode])) + packet.append(rs) + rs = RadioSetting("dig_shift", "Dig shift (*10 Hz)", + RadioSettingValueInteger(-300, 300, + _settings.dig_shift)) + packet.append(rs) + rs = RadioSetting("fm_mic", "FM mic", + RadioSettingValueInteger(0, 100, + _settings.fm_mic)) + basic.append(rs) + options = ["Dial", "Freq", "Panel"] + rs = RadioSetting("lock_mode", "Lock mode", + RadioSettingValueList(options, + options[_settings.lock_mode])) + panel.append(rs) + options = ["Fine", "Coarse"] + rs = RadioSetting("main_step", "Main step", + RadioSettingValueList(options, + options[_settings.main_step])) + panel.append(rs) + rs = RadioSetting("mem_group", "Mem group", + RadioSettingValueBoolean(_settings.mem_group)) + basic.append(rs) + rs = RadioSetting("mic_key", "Mic key", + RadioSettingValueBoolean(_settings.mic_key)) + cw.append(rs) + rs = RadioSetting("mic_scan", "Mic scan", + RadioSettingValueBoolean(_settings.mic_scan)) + basic.append(rs) + options = ["Off", "SSB", "CW"] + rs = RadioSetting("op_filter", "Optional filter", + RadioSettingValueList(options, + options[_settings.op_filter])) + basic.append(rs) + rs = RadioSetting("pkt_mic", "Packet mic", + RadioSettingValueInteger(0, 100, _settings.pkt_mic)) + packet.append(rs) + options = ["1200", "9600"] + rs = RadioSetting("pkt_rate", "Packet rate", + RadioSettingValueList(options, + options[_settings.pkt_rate])) + packet.append(rs) + options = ["Off", "3 sec", "5 sec", "10 sec"] + rs = RadioSetting("resume_scan", "Resume scan", + RadioSettingValueList(options, + options[_settings.resume_scan]) + ) + basic.append(rs) + options = ["Cont", "Chk"] + rs = RadioSetting("scope", "Scope", + RadioSettingValueList(options, + options[_settings.scope])) + basic.append(rs) + rs = RadioSetting("sidetone", "Sidetone", + RadioSettingValueInteger(0, 100, _settings.sidetone)) + cw.append(rs) + options = ["RF-Gain", "Squelch"] + rs = RadioSetting("sql_rf_gain", "Squelch/RF-Gain", + RadioSettingValueList(options, + options[_settings.sql_rf_gain]) + ) + panel.append(rs) + rs = RadioSetting("ssb_mic", "SSB Mic", + RadioSettingValueInteger(0, 100, _settings.ssb_mic)) + basic.append(rs) + options = ["%i" % i for i in range(0, 21)] + options[0] = "Off" + rs = RadioSetting("tot_time", "Time-out timer", + RadioSettingValueList(options, + options[_settings.tot_time])) + basic.append(rs) + rs = RadioSetting("vox_delay", "VOX delay (*100 ms)", + RadioSettingValueInteger(1, 25, _settings.vox_delay)) + basic.append(rs) + rs = RadioSetting("vox_gain", "VOX Gain", + RadioSettingValueInteger(0, 100, _settings.vox_gain)) + basic.append(rs) + rs = RadioSetting("extended_menu", "Extended menu", + RadioSettingValueBoolean(_settings.extended_menu)) + extended.append(rs) + options = ["Tn-Rn", "Tn-Riv", "Tiv-Rn", "Tiv-Riv"] + rs = RadioSetting("dcs_inv", "DCS coding", + RadioSettingValueList(options, + options[_settings.dcs_inv])) + extended.append(rs) + rs = RadioSetting("r_lsb_car", "LSB Rx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.r_lsb_car)) + extended.append(rs) + rs = RadioSetting("r_usb_car", "USB Rx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.r_usb_car)) + extended.append(rs) + rs = RadioSetting("t_lsb_car", "LSB Tx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.t_lsb_car)) + extended.append(rs) + rs = RadioSetting("t_usb_car", "USB Tx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.t_usb_car)) + extended.append(rs) + + options = ["Hi", "L3", "L2", "L1"] + rs = RadioSetting("tx_power", "TX power", + RadioSettingValueList(options, + options[_settings.tx_power])) + basic.append(rs) + + options = ["Front", "Rear"] + rs = RadioSetting("hf_antenna", "HF", + RadioSettingValueList(options, + options[_settings.hf_antenna])) + antenna.append(rs) + rs = RadioSetting("sixm_antenna", "6M", + RadioSettingValueList(options, + options[_settings.sixm_antenna] + )) + antenna.append(rs) + rs = RadioSetting("bc_antenna", "Broadcasting", + RadioSettingValueList(options, + options[_settings.bc_antenna])) + antenna.append(rs) + rs = RadioSetting("air_antenna", "Air band", + RadioSettingValueList(options, + options[_settings.air_antenna]) + ) + antenna.append(rs) + rs = RadioSetting("vhf_antenna", "VHF", + RadioSettingValueList(options, + options[_settings.vhf_antenna]) + ) + antenna.append(rs) + rs = RadioSetting("uhf_antenna", "UHF", + RadioSettingValueList(options, + options[_settings.uhf_antenna]) + ) + antenna.append(rs) + + st = RadioSettingValueString(0, 7, ''.join([self._CALLSIGN_CHARSET[x] + for x in self._memobj. + callsign])) + st.set_charset(self._CALLSIGN_CHARSET) + rs = RadioSetting("callsign", "Callsign", st) + cw.append(rs) + + rs = RadioSetting("spl", "Split", + RadioSettingValueBoolean(_settings.spl)) + panelcontr.append(rs) + options = ["None", "Up", "Down"] + rs = RadioSetting("scn_mode", "Scan mode", + RadioSettingValueList(options, + options[_settings.scn_mode])) + panelcontr.append(rs) + rs = RadioSetting("pri", "Priority", + RadioSettingValueBoolean(_settings.pri)) + panelcontr.append(rs) + rs = RadioSetting("dw", "Dual watch", + RadioSettingValueBoolean(_settings.dw)) + panelcontr.append(rs) + rs = RadioSetting("art", "Auto-range transponder", + RadioSettingValueBoolean(_settings.art)) + panelcontr.append(rs) + rs = RadioSetting("nb", "Noise blanker", + RadioSettingValueBoolean(_settings.nb)) + panelcontr.append(rs) + options = ["Auto", "Fast", "Slow", "Off"] + rs = RadioSetting("agc", "AGC", + RadioSettingValueList(options, options[_settings.agc] + )) + panelcontr.append(rs) + options = ["PWR", "ALC", "SWR", "MOD"] + rs = RadioSetting("pwr_meter_mode", "Power meter mode", + RadioSettingValueList(options, + options[ + _settings.pwr_meter_mode + ])) + panelcontr.append(rs) + rs = RadioSetting("vox", "Vox", + RadioSettingValueBoolean(_settings.vox)) + panelcontr.append(rs) + rs = RadioSetting("bk", "Semi break-in", + RadioSettingValueBoolean(_settings.bk)) + cw.append(rs) + rs = RadioSetting("kyr", "Keyer", + RadioSettingValueBoolean(_settings.kyr)) + cw.append(rs) + options = ["enabled", "disabled"] + rs = RadioSetting("fst", "Fast", + RadioSettingValueList(options, options[_settings.fst] + )) + panelcontr.append(rs) + options = ["enabled", "disabled"] + rs = RadioSetting("lock", "Lock", + RadioSettingValueList(options, + options[_settings.lock])) + panelcontr.append(rs) + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + try: + LOG.debug("Setting %s(%s) <= %s" % (setting, + getattr(obj, setting), + element.value)) + except AttributeError: + LOG.debug("Setting %s <= %s" % (setting, element.value)) + if setting == "contrast": + setattr(obj, setting, int(element.value) + 1) + elif setting == "callsign": + self._memobj.callsign = \ + [self._CALLSIGN_CHARSET_REV[x] for x in + str(element.value)] + else: + setattr(obj, setting, element.value) + except: + LOG.debug(element.get_name()) + raise + + +@directory.register +class FT817NDRadio(FT817Radio): + + """Yaesu FT-817ND""" + MODEL = "FT-817ND" + + _model = "" + _memsize = 6521 + # block 9 (130 Bytes long) is to be repeted 40 times + _block_lengths = [2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 130] + + +@directory.register +class FT817NDUSRadio(FT817Radio): + + """Yaesu FT-817ND (US version)""" + # seems that radios configured for 5MHz operations send one paket + # more than others so we have to distinguish sub models + MODEL = "FT-817ND (US)" + + _model = "" + _US_model = True + _memsize = 6651 + # block 9 (130 Bytes long) is to be repeted 40 times + _block_lengths = [2, 40, 208, 182, 208, 182, 198, 53, 130, 118, 130, 130] + + SPECIAL_60M = { + "M-601": -42, + "M-602": -41, + "M-603": -40, + "M-604": -39, + "M-605": -38, + } + LAST_SPECIAL60M_INDEX = -42 + + SPECIAL_MEMORIES = dict(FT817Radio.SPECIAL_MEMORIES) + SPECIAL_MEMORIES.update(SPECIAL_60M) + + SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()), + list(SPECIAL_MEMORIES.keys())))) + + def _get_special_60m(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_60M[number] + mem.extd_number = number + + _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX + + mem.number] + + mem = self._get_memory(mem, _mem) + + mem.immutable = ["number", "rtone", "ctone", + "extd_number", "name", "dtcs", "tmode", "cross_mode", + "dtcs_polarity", "power", "duplex", "offset", + "comment", "empty"] + + return mem + + def _set_special_60m(self, mem): + if mem.empty: + # can't delete 60M memories! + raise Exception("Sorry, 60M memory can't be deleted") + + cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number]) + + for key in cur_mem.immutable: + if cur_mem.__dict__[key] != mem.__dict__[key]: + raise errors.RadioError("Editing field `%s' " % key + + "is not supported on M-60x channels") + + if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]: + raise errors.RadioError("Mode {mode} is not valid " + "in 60m channels".format(mode=mem.mode)) + _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX + + mem.number] + self._set_memory(mem, _mem) + + def get_memory(self, number): + if number in list(self.SPECIAL_60M.keys()): + return self._get_special_60m(number) + elif (number < 0 and + self.SPECIAL_MEMORIES_REV[number] in + list(self.SPECIAL_60M.keys())): + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number]) + else: + return FT817Radio.get_memory(self, number) + + def set_memory(self, memory): + if memory.number in list(self.SPECIAL_60M.values()): + return self._set_special_60m(memory) + else: + return FT817Radio.set_memory(self, memory) + + def get_settings(self): + top = FT817Radio.get_settings(self) + basic = top[0] + rs = RadioSetting("emergency", "Emergency", + RadioSettingValueBoolean( + self._memobj.settings.emergency)) + basic.append(rs) + return top diff --git a/chirp/drivers/ft818.py b/chirp/drivers/ft818.py new file mode 100755 index 0000000..c7a0a32 --- /dev/null +++ b/chirp/drivers/ft818.py @@ -0,0 +1,342 @@ +# +# Copyright 2012 Filippi Marco +# Copyright 2018 Vinny Stipo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""FT818 management module""" + +from chirp.drivers import ft817 +from chirp import chirp_common, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +import os +import logging +from textwrap import dedent +from chirp.util import safe_charset_string + +LOG = logging.getLogger(__name__) + + +@directory.register +class FT818Radio(ft817.FT817Radio): + + """Yaesu FT-818""" + BAUD_RATE = 9600 + MODEL = "FT-818" + _model = "" + _memsize = 6573 + _block_lengths = [2, 40, 208, 208, 208, 208, 198, 53, 130, 118, 130] + + MEM_FORMAT = """ + struct mem_struct { + u8 tag_on_off:1, + tag_default:1, + unknown1:3, + mode:3; + u8 duplex:2, + is_duplex:1, + is_cwdig_narrow:1, + is_fm_narrow:1, + freq_range:3; + u8 skip:1, + unknown2:1, + ipo:1, + att:1, + unknown3:4; + u8 ssb_step:2, + am_step:3, + fm_step:3; + u8 unknown4:6, + tmode:2; + u8 unknown5:2, + tx_mode:3, + tx_freq_range:3; + u8 unknown6:1, + unknown_toneflag:1, + tone:6; + u8 unknown7:1, + dcs:7; + ul16 rit; + u32 freq; + u32 offset; + u8 name[8]; + }; + + #seekto 0x4; + struct { + u8 fst:1, + lock:1, + nb:1, + pbt:1, + unknownb:1, + dsp:1, + agc:2; + u8 vox:1, + vlt:1, + bk:1, + kyr:1, + unknown5:1, + cw_paddle:1, + pwr_meter_mode:2; + u8 vfob_band_select:4, + vfoa_band_select:4; + u8 unknowna; + u8 backlight:2, + color:2, + contrast:4; + u8 beep_freq:1, + beep_volume:7; + u8 arts_beep:2, + main_step:1, + cw_id:1, + scope:1, + pkt_rate:1, + resume_scan:2; + u8 op_filter:2, + lock_mode:2, + cw_pitch:4; + u8 sql_rf_gain:1, + ars_144:1, + ars_430:1, + cw_weight:5; + u8 cw_delay; + u8 unknown8:1, + sidetone:7; + u8 batt_chg:2, + cw_speed:6; + u8 disable_amfm_dial:1, + vox_gain:7; + u8 cat_rate:2, + emergency:1, + vox_delay:5; + u8 dig_mode:3, + mem_group:1, + unknown9:1, + apo_time:3; + u8 dcs_inv:2, + unknown10:1, + tot_time:5; + u8 mic_scan:1, + ssb_mic:7; + u8 mic_key:1, + am_mic:7; + u8 unknown11:1, + fm_mic:7; + u8 unknown12:1, + dig_mic:7; + u8 extended_menu:1, + pkt_mic:7; + u8 unknown14:1, + pkt9600_mic:7; + il16 dig_shift; + il16 dig_disp; + i8 r_lsb_car; + i8 r_usb_car; + i8 t_lsb_car; + i8 t_usb_car; + u8 unknown15:2, + menu_item:6; + u8 unknown16:4, + menu_sel:4; + u16 unknown17; + u8 art:1, + scn_mode:2, + dw:1, + pri:1, + unknown18:1, + tx_power:2; + u8 spl:1, + unknown:1, + uhf_antenna:1, + vhf_antenna:1, + air_antenna:1, + bc_antenna:1, + sixm_antenna:1, + hf_antenna:1; + } settings; + + #seekto 0x2A; + struct mem_struct vfoa[16]; + struct mem_struct vfob[16]; + struct mem_struct home[4]; + struct mem_struct qmb; + struct mem_struct mtqmb; + struct mem_struct mtune; + + #seekto 0x431; + u8 visible[25]; + u8 pmsvisible; + + #seekto 0x44B; + u8 filled[25]; + u8 pmsfilled; + + #seekto 0x465; + struct mem_struct memory[200]; + struct mem_struct pms[2]; + + #seekto 0x1903; + u8 callsign[7]; + + #seekto 0x19AD; + struct mem_struct sixtymeterchannels[5]; + """ + + SPECIAL_MEMORIES = { + "VFOa-1.8M": -37, + "VFOa-3.5M": -36, + "VFOa-5M": -35, + "VFOa-7M": -34, + "VFOa-10M": -33, + "VFOa-14M": -32, + "VFOa-18M": -31, + "VFOa-21M": -30, + "VFOa-24M": -29, + "VFOa-28M": -28, + "VFOa-50M": -27, + "VFOa-FM": -26, + "VFOa-AIR": -25, + "VFOa-144": -24, + "VFOa-430": -23, + "VFOa-HF": -22, + "VFOb-1.8M": -21, + "VFOb-3.5M": -20, + "VFOb-5M": -19, + "VFOb-7M": -18, + "VFOb-10M": -17, + "VFOb-14M": -16, + "VFOb-18M": -15, + "VFOb-21M": -14, + "VFOb-24M": -13, + "VFOb-28M": -12, + "VFOb-50M": -11, + "VFOb-FM": -10, + "VFOb-AIR": -9, + "VFOb-144": -8, + "VFOb-430": -7, + "VFOb-HF": -6, + "HOME HF": -5, + "HOME 50M": -4, + "HOME 144": -3, + "HOME 430": -2, + "QMB": -1, + } + FIRST_VFOB_INDEX = -6 + LAST_VFOB_INDEX = -21 + FIRST_VFOA_INDEX = -22 + LAST_VFOA_INDEX = -37 + + SPECIAL_PMS = { + "PMS-L": -39, + "PMS-U": -38, + } + LAST_PMS_INDEX = -39 + + SPECIAL_MEMORIES.update(SPECIAL_PMS) + + SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(), + SPECIAL_MEMORIES.keys())) + + +@directory.register +class FT818NDUSRadio(FT818Radio): + + """Yaesu FT-818ND (US version)""" + MODEL = "FT-818ND (US)" + + _model = "" + _US_model = True + _memsize = 6703 + + _block_lengths = [2, 40, 208, 208, 208, 208, 198, 53, 130, 118, 130, 130] + + SPECIAL_60M = { + "M-601": -44, + "M-602": -43, + "M-603": -42, + "M-604": -41, + "M-605": -40, + } + LAST_SPECIAL60M_INDEX = -44 + + SPECIAL_MEMORIES = dict(FT818Radio.SPECIAL_MEMORIES) + SPECIAL_MEMORIES.update(SPECIAL_60M) + + SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(), + SPECIAL_MEMORIES.keys())) + + def _get_special_60m(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_60M[number] + mem.extd_number = number + + _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX + + mem.number] + + mem = self._get_memory(mem, _mem) + + mem.immutable = ["number", "rtone", "ctone", + "extd_number", "name", "dtcs", "tmode", "cross_mode", + "dtcs_polarity", "power", "duplex", "offset", + "comment", "empty"] + + return mem + + def _set_special_60m(self, mem): + if mem.empty: + # can't delete 60M memories! + raise Exception("Sorry, 60M memory can't be deleted") + + cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number]) + + for key in cur_mem.immutable: + if cur_mem.__dict__[key] != mem.__dict__[key]: + raise errors.RadioError("Editing field `%s' " % key + + "is not supported on M-60x channels") + + if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]: + raise errors.RadioError("Mode {mode} is not valid " + "in 60m channels".format(mode=mem.mode)) + _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX + + mem.number] + self._set_memory(mem, _mem) + + def get_memory(self, number): + if number in self.SPECIAL_60M.keys(): + return self._get_special_60m(number) + elif number < 0 and \ + self.SPECIAL_MEMORIES_REV[number] in self.SPECIAL_60M.keys(): + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number]) + else: + return FT818Radio.get_memory(self, number) + + def set_memory(self, memory): + if memory.number in self.SPECIAL_60M.values(): + return self._set_special_60m(memory) + else: + return FT818Radio.set_memory(self, memory) + + def get_settings(self): + top = FT818Radio.get_settings(self) + basic = top[0] + rs = RadioSetting("emergency", "Emergency", + RadioSettingValueBoolean( + self._memobj.settings.emergency)) + basic.append(rs) + return top diff --git a/chirp/drivers/ft857.py b/chirp/drivers/ft857.py new file mode 100644 index 0000000..a58c52b --- /dev/null +++ b/chirp/drivers/ft857.py @@ -0,0 +1,1202 @@ +# +# Copyright 2012 Filippi Marco +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""FT857 - FT857/US management module""" + +from chirp.drivers import ft817 +from chirp import chirp_common, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +import os +import logging +from textwrap import dedent +from chirp.util import safe_charset_string + +LOG = logging.getLogger(__name__) + + +@directory.register +class FT857Radio(ft817.FT817Radio): + """Yaesu FT-857/897""" + MODEL = "FT-857/897" + _model = "" + + TMODES = { + 0x04: "Tone", + 0x05: "TSQL", + # 0x08 : "DTCS Enc", not supported in UI yet + 0x0a: "DTCS", + 0xff: "Cross", + 0x00: "", + } + TMODES_REV = dict(list(zip(list(TMODES.values()), list(TMODES.keys())))) + + CROSS_MODES = { + 0x01: "->Tone", + 0x02: "->DTCS", + 0x04: "Tone->", + 0x05: "Tone->Tone", + 0x06: "Tone->DTCS", + 0x08: "DTCS->", + 0x09: "DTCS->Tone", + 0x0a: "DTCS->DTCS", + } + CROSS_MODES_REV = dict(list(zip(list(CROSS_MODES.values()), + list(CROSS_MODES.keys())))) + + _memsize = 7341 + # block 9 (140 Bytes long) is to be repeted 40 times + # should be 42 times but this way I can use original 817 functions + _block_lengths = [2, 82, 252, 196, 252, 196, 212, 55, 140, 140, 140, + 38, 176] + # warning ranges has to be in this exact order + VALID_BANDS = [(100000, 33000000), (33000000, 56000000), + (76000000, 108000000), (108000000, 137000000), + (137000000, 164000000), (420000000, 470000000)] + + CHARSET = list(chirp_common.CHARSET_ASCII) + for i in "\\{|}": + CHARSET.remove(i) + + MEM_FORMAT = """ + struct mem_struct{ + u8 tag_on_off:1, + tag_default:1, + unknown1:3, + mode:3; + u8 duplex:2, + is_duplex:1, + is_cwdig_narrow:1, + is_fm_narrow:1, + freq_range:3; + u8 skip:1, + unknokwn1_1:1, + ipo:1, + att:1, + unknown2:4; + u8 ssb_step:2, + am_step:3, + fm_step:3; + u8 unknown3:3, + is_split_tone:1, + tmode:4; + u8 unknown4:2, + tx_mode:3, + tx_freq_range:3; + u8 unknown5:1, + unknown_toneflag:1, + tone:6; + u8 unknown6:1, + unknown_rxtoneflag:1, + rxtone:6; + u8 unknown7:1, + dcs:7; + u8 unknown8:1, + rxdcs:7; + ul16 rit; + u32 freq; + u32 offset; + u8 name[8]; + }; + + #seekto 0x00; + struct { + u16 radioconfig; + u8 mem_vfo:2, + m_tune:1, + home:1, + pms_tune:1, + qmb:1, + mt_qmb:1, + vfo_ab:1; + u8 unknown; + u8 fst:1, + lock:1, + nb:1, + unknown1:2, + disp:1, + agc:2; + u8 vox:1, + unknown2:1, + bk:1, + kyr:1, + cw_speed_unit:1, + cw_key_rev:1, + pwr_meter_mode:2; + u8 vfo_b_freq_range:4, + vfo_a_freq_range:4; + u8 unknown3; + u8 disp_mode:2, + unknown4:2, + disp_contrast:4; + u8 unknown5:4, + clar_dial_sel:2, + beep_tone:2; + u8 arts_beep:2, + dial_step:1, + arts_id:1, + unknown6:1, + pkt_rate:1, + unknown7:2; + u8 unknown8:2, + lock_mode:2, + unknown9:1, + cw_pitch:3; + u8 sql_rf_gain:1, + ars_144:1, + ars_430:1, + cw_weight:5; + u8 cw_delay; + u8 cw_delay_hi:1 + cw_sidetone:7; + u8 unknown10:2, + cw_speed:6; + u8 disable_amfm_dial:1, + vox_gain:7; + u8 cat_rate:2, + emergency:1, + vox_delay:5; + u8 dig_mode:3, + mem_group:1, + unknown11:1, + apo_time:3; + u8 dcs_inv:2, + unknown12:1, + tot_time:5; + u8 mic_scan:1, + ssb_mic:7; + u8 cw_paddle:1, + am_mic:7; + u8 unknown13:1, + fm_mic:7; + u8 unknown14:1, + dig_mic:7; + u8 extended_menu:1, + pkt1200:7; + u8 unknown15:1, + pkt9600:7; + i16 dig_shift; + i16 dig_disp; + i8 r_lsb_car; + i8 r_usb_car; + i8 t_lsb_car; + i8 t_usb_car; + u8 unknown16:1, + menu_item:7; + u8 unknown17[5]; + u8 unknown18:1, + mtr_peak_hold:1, + mic_sel:2, + cat_lin_tun:2, + unknown19:1, + split_tone:1; + u8 unknown20:1, + beep_vol:7; + u8 unknown21:1, + dig_vox:7; + u8 ext_menu:1, + home_vfo:1, + scan_mode:2, + scan_resume:4; + u8 cw_auto_mode:1, + cw_training:2, + cw_qsk:3, + cw_bfo:2; + u8 dsp_nr:4, + dsp_bpf:2, + dsp_mic_eq:2; + u8 unknown22:3, + dsp_lpf:5; + u8 mtr_atx_sel:3, + unknown23:1, + dsp_hpf:4; + u8 unknown24:2, + disp_intensity:2, + unknown25:1, + disp_color:3; + u8 unknown26:1, + disp_color_vfo:1, + disp_color_mtr:1, + disp_color_mode:1, + disp_color_memgrp:1, + unknown27:1, + disp_color_band:1, + disp_color_arts:1; + u8 unknown28:3, + disp_color_fix:5; + u8 unknown29:1, + nb_level:7; + u8 unknown30:1, + proc_level:7; + u8 unknown31:1, + rf_power_hf:7; + u8 unknown32:2, + tuner_atas:3, + mem_vfo_dial_mode:3; + u8 pg_a; + u8 pg_b; + u8 pg_c; + u8 pg_acc; + u8 pg_p1; + u8 pg_p2; + u8 unknown33:3, + xvtr_sel:2, + unknown33_1:2, + op_filter1:1; + u8 unknown34:6, + tx_if_filter:2; + u8 unknown35:3, + xvtr_a_negative:1, + xvtr_b_negative:1, + mtr_arx_sel:3; + u8 beacon_time; + u8 unknown36[2]; + u8 dig_vox_enable:1, + unknown37:2, + scope_peakhold:1, + scope_width:2, + proc:1, + unknown38:1; + u8 unknown39:1, + rf_power_6m:7; + u8 unknown40:1, + rf_power_vhf:7; + u8 unknown41:1, + rf_power_uhf:7; + } settings; + + #seekto 0x54; + struct mem_struct vfoa[16]; + struct mem_struct vfob[16]; + struct mem_struct home[4]; + struct mem_struct qmb; + struct mem_struct mtqmb; + struct mem_struct mtune; + + #seekto 0x4a9; + u8 visible[25]; + ul16 pmsvisible; + + #seekto 0x4c4; + u8 filled[25]; + ul16 pmsfilled; + + #seekto 0x4df; + struct mem_struct memory[200]; + struct mem_struct pms[10]; + + #seekto 0x1bf3; + u8 arts_idw[10]; + u8 beacon_text1[40]; + u8 beacon_text2[40]; + u8 beacon_text3[40]; + u32 xvtr_a_offset; + u32 xvtr_b_offset; + u8 op_filter1_name[4]; + u8 op_filter2_name[4]; + + #seekto 0x1CAD; + struct mem_struct sixtymeterchannels[5]; + + """ + + _CALLSIGN_CHARSET = [chr(x) for x in list(range(ord("0"), ord("9") + 1)) + + list(range(ord("A"), ord("Z") + 1))] + [" ", "/"] + _CALLSIGN_CHARSET_REV = dict(list(zip(_CALLSIGN_CHARSET, + list(range(0, len(_CALLSIGN_CHARSET)))))) + _BEACON_CHARSET = _CALLSIGN_CHARSET + ["+", "."] + _BEACON_CHARSET_REV = dict(list(zip(_BEACON_CHARSET, + list(range(0, len(_BEACON_CHARSET)))))) + + # WARNING Index are hard wired in memory management code !!! + SPECIAL_MEMORIES = { + "VFOa-1.8M": -37, + "VFOa-3.5M": -36, + "VFOa-5M": -35, + "VFOa-7M": -34, + "VFOa-10M": -33, + "VFOa-14M": -32, + "VFOa-18M": -31, + "VFOa-21M": -30, + "VFOa-24M": -29, + "VFOa-28M": -28, + "VFOa-50M": -27, + "VFOa-FM": -26, + "VFOa-AIR": -25, + "VFOa-144": -24, + "VFOa-430": -23, + "VFOa-HF": -22, + "VFOb-1.8M": -21, + "VFOb-3.5M": -20, + "VFOb-5M": -19, + "VFOb-7M": -18, + "VFOb-10M": -17, + "VFOb-14M": -16, + "VFOb-18M": -15, + "VFOb-21M": -14, + "VFOb-24M": -13, + "VFOb-28M": -12, + "VFOb-50M": -11, + "VFOb-FM": -10, + "VFOb-AIR": -9, + "VFOb-144M": -8, + "VFOb-430M": -7, + "VFOb-HF": -6, + "HOME HF": -5, + "HOME 50M": -4, + "HOME 144M": -3, + "HOME 430M": -2, + "QMB": -1, + } + FIRST_VFOB_INDEX = -6 + LAST_VFOB_INDEX = -21 + FIRST_VFOA_INDEX = -22 + LAST_VFOA_INDEX = -37 + + SPECIAL_PMS = { + "PMS-1L": -47, + "PMS-1U": -46, + "PMS-2L": -45, + "PMS-2U": -44, + "PMS-3L": -43, + "PMS-3U": -42, + "PMS-4L": -41, + "PMS-4U": -40, + "PMS-5L": -39, + "PMS-5U": -38, + } + LAST_PMS_INDEX = -47 + + SPECIAL_MEMORIES.update(SPECIAL_PMS) + + SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()), + list(SPECIAL_MEMORIES.keys())))) + + FILTERS = ["CFIL", "FIL1", "FIL2"] + PROGRAMMABLEOPTIONS = [ + "MFa:A/B", "MFa:A=B", "MFa:SPL", + "MFb:MW", "MFb:SKIP/MCLR", "MFb:TAG", + "MFc:STO", "MFc:RCL", "MFc:PROC", + "MFd:RPT", "MFd:REV", "MFd:VOX", + "MFe:TON/ENC", "MFe:TON/DEC", "MFe:TDCH", + "MFf:ARTS", "MFf:SRCH", "MFf:PMS", + "MFg:SCN", "MFg:PRI", "MFg:DW", + "MFh:SCOP", "MFh:WID", "MFh:STEP", + "MFi:MTR", "MFi:SWR", "MFi:DISP", + "MFj:SPOT", "MFj:BK", "MFj:KYR", + "MFk:TUNE", "MFk:DOWN", "MFk:UP", + "MFl:NB", "MFl:AGC", "MFl:AGC SEL", + "MFm:IPO", "MFm:ATT", "MFm:NAR", + "MFn:CFIL", "MFn:FIL1", "MFn:FIL2", + "MFo:PLY1", "MFo:PLY2", "MFo:PLY3", + "MFp:DNR", "MFp:DNF", "MFp:DBF", + "01:EXT MENU", "02:144MHz ARS", "03:430MHz ARS", + "04:AM&FM DIAL", "05:AM MIC GAIN", "06:AM STEP", + "07:APO TIME", "08:ARTS BEEP", "09:ARTS ID", + "10:ARTS IDW", "11:BEACON TEXT", "12:BEACON TIME", + "13:BEEP TONE", "14:BEEP VOL", "15:CAR LSB R", + "16:CAR LSB T", "17:CAR USB R", "18:CAR USB T", + "19:CAT RATE", "20:CAT/LIN/TUN", "21:CLAR DIAL SEL", + "22:CW AUTO MODE", "23:CW BFO", "24:CW DELAY", + "25:CW KEY REV", "26:CW PADDLE", "27:CW PITCH", + "28:CW QSK", "29:CW SIDE TONE", "30:CW SPEED", + "31:CW TRAINING", "32:CW WEIGHT", "33:DCS CODE", + "34:DCS INV", "35:DIAL STEP", "36:DIG DISP", + "37:DIG GAIN", "38:DIG MODE", "39:DIG SHIFT", + "40:DIG VOX", "41:DISP COLOR", "42:DISP CONTRAST", + "43:DISP INTENSITY", "44:DISP MODE", "45:DSP BPF WIDTH", + "46:DSP HPF CUTOFF", "47:DSP LPF CUTOFF", "48:DSP MIC EQ", + "49:DSP NR LEVEL", "50:EMERGENCY", "51:FM MIC GAIN", + "52:FM STEP", "53:HOME->VFO", "54:LOCK MODE", + "55:MEM GROUP", "56:MEM TAG", "57:MEM/VFO DIAL MODE", + "58:MIC SCAN", "59:MIC SEL", "60:MTR ARX", + "61:MTR ATX", "62:MTR PEAK HOLD", "63:NB LEVEL", + "64:OP FILTER", "71:PKT 1200", "72:PKT 9600", + "73:PKT RATE", "74:PROC LEVEL", "75:RF POWER SET", + "76:RPT SHIFT", "77:SCAN MODE", "78:SCAN RESUME", + "79:SPLIT TONE", "80:SQL/RF GAIN", "81:SSB MIC GAIN", + "82:SSB STEP", "83:TONE FREQ", "84:TX TIME", + "85:TUNER/ATAS", "86:TX IF FILTER", "87:VOX DELAY", + "88:VOX GAIN", "89:XVTR A FREQ", "90:XVTR B FREQ", + "91:XVTR SEL", + "MONI", "Q.SPL", "TCALL", "ATC", "USER"] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to CAT/LINEAR jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. After clicking OK, + press the [C](SEND) key to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. Press the [A](RCV) key ("receiving" will appear on the LCD).""" + )) + return rp + + def get_features(self): + rf = ft817.FT817Radio.get_features(self) + rf.has_cross = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.valid_tmodes = list(self.TMODES_REV.keys()) + rf.valid_cross_modes = list(self.CROSS_MODES_REV.keys()) + return rf + + def _get_duplex(self, mem, _mem): + # radio set is_duplex only for + and - but not for split + # at the same time it does not complain if we set it same way 817 does + # (so no set_duplex here) + mem.duplex = self.DUPLEX[_mem.duplex] + + def _get_tmode(self, mem, _mem): + if not _mem.is_split_tone: + mem.tmode = self.TMODES[int(_mem.tmode)] + else: + mem.tmode = "Cross" + mem.cross_mode = self.CROSS_MODES[int(_mem.tmode)] + + if mem.tmode == "Tone": + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + elif mem.tmode == "TSQL": + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + elif mem.tmode == "DTCS Enc": # UI does not support it yet but + # this code has alreay been tested + mem.dtcs = mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dcs] + elif mem.tmode == "DTCS": + mem.dtcs = mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.dcs] + elif mem.tmode == "Cross": + mem.ctone = chirp_common.TONES[_mem.rxtone] + # don't want to fail for this + try: + mem.rtone = chirp_common.TONES[_mem.tone] + except IndexError: + mem.rtone = chirp_common.TONES[0] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.rx_dtcs = chirp_common.DTCS_CODES[_mem.rxdcs] + + def _set_tmode(self, mem, _mem): + if mem.tmode != "Cross": + _mem.is_split_tone = 0 + _mem.tmode = self.TMODES_REV[mem.tmode] + else: + _mem.tmode = self.CROSS_MODES_REV[mem.cross_mode] + _mem.is_split_tone = 1 + + if mem.tmode == "Tone": + _mem.tone = _mem.rxtone = chirp_common.TONES.index(mem.rtone) + elif mem.tmode == "TSQL": + _mem.tone = _mem.rxtone = chirp_common.TONES.index(mem.ctone) + elif mem.tmode == "DTCS Enc": # UI does not support it yet but + # this code has alreay been tested + _mem.dcs = _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.dtcs) + elif mem.tmode == "DTCS": + _mem.dcs = _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs) + elif mem.tmode == "Cross": + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.rxtone = chirp_common.TONES.index(mem.ctone) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.rxdcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs) + # have to put this bit to 0 otherwise we get strange display in tone + # frequency (menu 83). See bug #88 and #163 + _mem.unknown_toneflag = 0 + # dunno if there's the same problem here but to be safe ... + _mem.unknown_rxtoneflag = 0 + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + cw = RadioSettingGroup("cw", "CW") + packet = RadioSettingGroup("packet", "Digital & packet") + panel = RadioSettingGroup("panel", "Panel settings") + extended = RadioSettingGroup("extended", "Extended") + panelcontr = RadioSettingGroup("panelcontr", "Panel controls") + + top = RadioSettings(basic, cw, packet, + panelcontr, panel, extended) + + rs = RadioSetting("extended_menu", "Extended menu", + RadioSettingValueBoolean(_settings.extended_menu)) + extended.append(rs) + rs = RadioSetting("ars_144", "144MHz ARS", + RadioSettingValueBoolean(_settings.ars_144)) + basic.append(rs) + rs = RadioSetting("ars_430", "430MHz ARS", + RadioSettingValueBoolean(_settings.ars_430)) + basic.append(rs) + options = ["enable", "disable"] + rs = RadioSetting("disable_amfm_dial", "AM&FM Dial", + RadioSettingValueList(options, + options[ + _settings.disable_amfm_dial + ])) + panel.append(rs) + rs = RadioSetting("am_mic", "AM mic gain", + RadioSettingValueInteger(0, 100, _settings.am_mic)) + basic.append(rs) + options = ["OFF", "1h", "2h", "3h", "4h", "5h", "6h"] + rs = RadioSetting("apo_time", "APO time", + RadioSettingValueList(options, + options[_settings.apo_time])) + basic.append(rs) + options = ["OFF", "Range", "All"] + rs = RadioSetting("arts_beep", "ARTS beep", + RadioSettingValueList(options, + options[_settings.arts_beep])) + basic.append(rs) + rs = RadioSetting("arts_id", "ARTS ID", + RadioSettingValueBoolean(_settings.arts_id)) + extended.append(rs) + st = RadioSettingValueString(0, 10, + safe_charset_string( + self._memobj.arts_idw, + self._CALLSIGN_CHARSET) + ) + st.set_charset(self._CALLSIGN_CHARSET) + rs = RadioSetting("arts_idw", "ARTS IDW", st) + extended.append(rs) + st = RadioSettingValueString(0, 40, + safe_charset_string( + self._memobj.beacon_text1, + self._BEACON_CHARSET) + ) + st.set_charset(self._BEACON_CHARSET) + rs = RadioSetting("beacon_text1", "Beacon text1", st) + extended.append(rs) + st = RadioSettingValueString(0, 40, + safe_charset_string( + self._memobj.beacon_text2, + self._BEACON_CHARSET) + ) + st.set_charset(self._BEACON_CHARSET) + rs = RadioSetting("beacon_text2", "Beacon text2", st) + extended.append(rs) + st = RadioSettingValueString(0, 40, + safe_charset_string( + self._memobj.beacon_text3, + self._BEACON_CHARSET) + ) + st.set_charset(self._BEACON_CHARSET) + rs = RadioSetting("beacon_text3", "Beacon text3", st) + extended.append(rs) + options = ["OFF"] + ["%i sec" % i for i in range(1, 256)] + rs = RadioSetting("beacon_time", "Beacon time", + RadioSettingValueList(options, + options[_settings.beacon_time]) + ) + extended.append(rs) + options = ["440Hz", "880Hz", "1760Hz"] + rs = RadioSetting("beep_tone", "Beep tone", + RadioSettingValueList(options, + options[_settings.beep_tone])) + panel.append(rs) + rs = RadioSetting("beep_vol", "Beep volume", + RadioSettingValueInteger(0, 100, _settings.beep_vol)) + panel.append(rs) + rs = RadioSetting("r_lsb_car", "LSB Rx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.r_lsb_car)) + extended.append(rs) + rs = RadioSetting("r_usb_car", "USB Rx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.r_usb_car)) + extended.append(rs) + rs = RadioSetting("t_lsb_car", "LSB Tx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.t_lsb_car)) + extended.append(rs) + rs = RadioSetting("t_usb_car", "USB Tx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.t_usb_car)) + extended.append(rs) + options = ["4800", "9600", "38400"] + rs = RadioSetting("cat_rate", "CAT rate", + RadioSettingValueList(options, + options[_settings.cat_rate])) + basic.append(rs) + options = ["CAT", "Linear", "Tuner"] + rs = RadioSetting("cat_lin_tun", "CAT/LIN/TUN selection", + RadioSettingValueList(options, + options[_settings.cat_lin_tun]) + ) + extended.append(rs) + options = ["MAIN", "VFO/MEM", "CLAR"] + # TODO test the 3 options on non D radio + # which have only SEL and MAIN + rs = RadioSetting("clar_dial_sel", "Clarifier dial selection", + RadioSettingValueList(options, + options[ + _settings.clar_dial_sel])) + panel.append(rs) + rs = RadioSetting("cw_auto_mode", "CW Automatic mode", + RadioSettingValueBoolean(_settings.cw_auto_mode)) + cw.append(rs) + options = ["USB", "LSB", "AUTO"] + rs = RadioSetting("cw_bfo", "CW BFO", + RadioSettingValueList(options, + options[_settings.cw_bfo])) + cw.append(rs) + options = ["FULL"] + ["%i ms" % (i * 10) for i in range(3, 301)] + val = (_settings.cw_delay + _settings.cw_delay_hi * 256) - 2 + rs = RadioSetting("cw_delay", "CW delay", + RadioSettingValueList(options, options[val])) + cw.append(rs) + options = ["Normal", "Reverse"] + rs = RadioSetting("cw_key_rev", "CW key reverse", + RadioSettingValueList(options, + options[_settings.cw_key_rev])) + cw.append(rs) + rs = RadioSetting("cw_paddle", "CW paddle", + RadioSettingValueBoolean(_settings.cw_paddle)) + cw.append(rs) + options = ["%i Hz" % i for i in range(400, 801, 100)] + rs = RadioSetting("cw_pitch", "CW pitch", + RadioSettingValueList(options, + options[_settings.cw_pitch])) + cw.append(rs) + options = ["%i ms" % i for i in range(5, 31, 5)] + rs = RadioSetting("cw_qsk", "CW QSK", + RadioSettingValueList(options, + options[_settings.cw_qsk])) + cw.append(rs) + rs = RadioSetting("cw_sidetone", "CW sidetone volume", + RadioSettingValueInteger(0, 100, + _settings.cw_sidetone)) + cw.append(rs) + options = ["%i wpm" % i for i in range(4, 61)] + rs = RadioSetting("cw_speed", "CW speed", + RadioSettingValueList(options, + options[_settings.cw_speed])) + cw.append(rs) + options = ["Numeric", "Alphabet", "AlphaNumeric"] + rs = RadioSetting("cw_training", "CW trainig", + RadioSettingValueList(options, + options[_settings.cw_training]) + ) + cw.append(rs) + options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)] + rs = RadioSetting("cw_weight", "CW weight", + RadioSettingValueList(options, + options[_settings.cw_weight])) + cw.append(rs) + options = ["Tn-Rn", "Tn-Riv", "Tiv-Rn", "Tiv-Riv"] + rs = RadioSetting("dcs_inv", "DCS inv", + RadioSettingValueList(options, + options[_settings.dcs_inv])) + extended.append(rs) + options = ["Fine", "Coarse"] + rs = RadioSetting("dial_step", "Dial step", + RadioSettingValueList(options, + options[_settings.dial_step])) + panel.append(rs) + rs = RadioSetting("dig_disp", "Dig disp (*10 Hz)", + RadioSettingValueInteger(-300, 300, + _settings.dig_disp)) + packet.append(rs) + rs = RadioSetting("dig_mic", "Dig gain", + RadioSettingValueInteger(0, 100, + _settings.dig_mic)) + packet.append(rs) + options = ["RTTYL", "RTTYU", "PSK31-L", "PSK31-U", "USER-L", "USER-U"] + rs = RadioSetting("dig_mode", "Dig mode", + RadioSettingValueList(options, + options[_settings.dig_mode])) + packet.append(rs) + rs = RadioSetting("dig_shift", "Dig shift (*10 Hz)", + RadioSettingValueInteger(-300, 300, + _settings.dig_shift)) + packet.append(rs) + rs = RadioSetting("dig_vox", "Dig vox", + RadioSettingValueInteger(0, 100, _settings.dig_vox)) + packet.append(rs) + options = ["ARTS", "BAND", "FIX", "MEMGRP", "MODE", "MTR", "VFO"] + rs = RadioSetting("disp_color", "Display color mode", + RadioSettingValueList(options, + options[_settings.disp_color])) + panel.append(rs) + rs = RadioSetting("disp_color_arts", "Display color ARTS set", + RadioSettingValueInteger(0, 1, + _settings.disp_color_arts)) + panel.append(rs) + rs = RadioSetting("disp_color_band", "Display color band set", + RadioSettingValueInteger(0, 1, + _settings.disp_color_band)) + panel.append(rs) + rs = RadioSetting("disp_color_memgrp", + "Display color memory group set", + RadioSettingValueInteger(0, 1, + _settings.disp_color_memgrp) + ) + panel.append(rs) + rs = RadioSetting("disp_color_mode", "Display color mode set", + RadioSettingValueInteger(0, 1, + _settings.disp_color_mode)) + panel.append(rs) + rs = RadioSetting("disp_color_mtr", "Display color meter set", + RadioSettingValueInteger(0, 1, + _settings.disp_color_mtr)) + panel.append(rs) + rs = RadioSetting("disp_color_vfo", "Display color VFO set", + RadioSettingValueInteger(0, 1, + _settings.disp_color_vfo)) + panel.append(rs) + rs = RadioSetting("disp_color_fix", "Display color fix set", + RadioSettingValueInteger(1, 32, + _settings.disp_color_fix + 1 + )) + panel.append(rs) + rs = RadioSetting("disp_contrast", "Contrast", + RadioSettingValueInteger(3, 15, + _settings.disp_contrast + 2) + ) + panel.append(rs) + rs = RadioSetting("disp_intensity", "Intensity", + RadioSettingValueInteger(1, 3, + _settings.disp_intensity)) + panel.append(rs) + options = ["OFF", "Auto1", "Auto2", "ON"] + rs = RadioSetting("disp_mode", "Display backlight mode", + RadioSettingValueList(options, + options[_settings.disp_mode])) + panel.append(rs) + options = ["60Hz", "120Hz", "240Hz"] + rs = RadioSetting("dsp_bpf", "Dsp band pass filter", + RadioSettingValueList(options, + options[_settings.dsp_bpf])) + cw.append(rs) + options = ["100Hz", "160Hz", "220Hz", "280Hz", "340Hz", "400Hz", + "460Hz", "520Hz", "580Hz", "640Hz", "720Hz", "760Hz", + "820Hz", "880Hz", "940Hz", "1000Hz"] + rs = RadioSetting("dsp_hpf", "Dsp hi pass filter cut off", + RadioSettingValueList(options, + options[_settings.dsp_hpf])) + basic.append(rs) + options = ["1000Hz", "1160Hz", "1320Hz", "1480Hz", "1650Hz", "1800Hz", + "1970Hz", "2130Hz", "2290Hz", "2450Hz", "2610Hz", "2770Hz", + "2940Hz", "3100Hz", "3260Hz", "3420Hz", "3580Hz", "3740Hz", + "3900Hz", "4060Hz", "4230Hz", "4390Hz", "4550Hz", "4710Hz", + "4870Hz", "5030Hz", "5190Hz", "5390Hz", "5520Hz", "5680Hz", + "5840Hz", "6000Hz"] + rs = RadioSetting("dsp_lpf", "Dsp low pass filter cut off", + RadioSettingValueList(options, + options[_settings.dsp_lpf])) + basic.append(rs) + options = ["OFF", "LPF", "HPF", "BOTH"] + rs = RadioSetting("dsp_mic_eq", "Dsp mic equalization", + RadioSettingValueList(options, + options[_settings.dsp_mic_eq])) + basic.append(rs) + rs = RadioSetting("dsp_nr", "DSP noise reduction level", + RadioSettingValueInteger(1, 16, + _settings.dsp_nr + 1)) + basic.append(rs) + # emergency only for US model + rs = RadioSetting("fm_mic", "FM mic gain", + RadioSettingValueInteger(0, 100, _settings.fm_mic)) + basic.append(rs) + rs = RadioSetting("home_vfo", "Enable HOME to VFO moving", + RadioSettingValueBoolean(_settings.home_vfo)) + panel.append(rs) + options = ["Dial", "Freq", "Panel", "All"] + rs = RadioSetting("lock_mode", "Lock mode", + RadioSettingValueList(options, + options[_settings.lock_mode])) + panel.append(rs) + rs = RadioSetting("mem_group", "Mem group", + RadioSettingValueBoolean(_settings.mem_group)) + basic.append(rs) + options = ["CW SIDETONE", "CW SPEED", "MHz/MEM GRP", "MIC GAIN", + "NB LEVEL", "RF POWER", "STEP"] + rs = RadioSetting("mem_vfo_dial_mode", "Mem/VFO dial mode", + RadioSettingValueList(options, + options[ + _settings.mem_vfo_dial_mode + ])) + panel.append(rs) + rs = RadioSetting("mic_scan", "Mic scan", + RadioSettingValueBoolean(_settings.mic_scan)) + basic.append(rs) + options = ["NOR", "RMT", "CAT"] + rs = RadioSetting("mic_sel", "Mic selection", + RadioSettingValueList(options, + options[_settings.mic_sel])) + extended.append(rs) + options = ["SIG", "CTR", "VLT", "N/A", "FS", "OFF"] + rs = RadioSetting("mtr_arx_sel", "Meter receive selection", + RadioSettingValueList(options, + options[_settings.mtr_arx_sel]) + ) + extended.append(rs) + options = ["PWR", "ALC", "MOD", "SWR", "VLT", "N/A", "OFF"] + rs = RadioSetting("mtr_atx_sel", "Meter transmit selection", + RadioSettingValueList(options, + options[_settings.mtr_atx_sel]) + ) + extended.append(rs) + rs = RadioSetting("mtr_peak_hold", "Meter peak hold", + RadioSettingValueBoolean(_settings.mtr_peak_hold)) + extended.append(rs) + rs = RadioSetting("nb_level", "Noise blanking level", + RadioSettingValueInteger(0, 100, _settings.nb_level)) + basic.append(rs) + st = RadioSettingValueString(0, 4, + safe_charset_string( + self._memobj.op_filter1_name, + self._CALLSIGN_CHARSET) + ) + st.set_charset(self._CALLSIGN_CHARSET) + rs = RadioSetting("op_filter1_name", "Optional filter1 name", st) + extended.append(rs) + st = RadioSettingValueString(0, 4, + safe_charset_string( + self._memobj.op_filter2_name, + self._CALLSIGN_CHARSET) + ) + st.set_charset(self._CALLSIGN_CHARSET) + rs = RadioSetting("op_filter2_name", "Optional filter2 name", st) + extended.append(rs) + rs = RadioSetting("pg_a", "Programmable key MFq:A", + RadioSettingValueList(self.PROGRAMMABLEOPTIONS, + self.PROGRAMMABLEOPTIONS[ + _settings.pg_a])) + extended.append(rs) + rs = RadioSetting("pg_b", "Programmable key MFq:B", + RadioSettingValueList(self.PROGRAMMABLEOPTIONS, + self.PROGRAMMABLEOPTIONS[ + _settings.pg_b])) + extended.append(rs) + rs = RadioSetting("pg_c", "Programmable key MFq:C", + RadioSettingValueList(self.PROGRAMMABLEOPTIONS, + self.PROGRAMMABLEOPTIONS[ + _settings.pg_c])) + extended.append(rs) + rs = RadioSetting("pg_acc", "Programmable mic key ACC", + RadioSettingValueList(self.PROGRAMMABLEOPTIONS, + self.PROGRAMMABLEOPTIONS[ + _settings.pg_acc])) + extended.append(rs) + rs = RadioSetting("pg_p1", "Programmable mic key P1", + RadioSettingValueList(self.PROGRAMMABLEOPTIONS, + self.PROGRAMMABLEOPTIONS[ + _settings.pg_p1])) + extended.append(rs) + rs = RadioSetting("pg_p2", "Programmable mic key P2", + RadioSettingValueList(self.PROGRAMMABLEOPTIONS, + self.PROGRAMMABLEOPTIONS[ + _settings.pg_p2])) + extended.append(rs) + rs = RadioSetting("pkt1200", "Packet 1200 gain level", + RadioSettingValueInteger(0, 100, + _settings.pkt1200)) + packet.append(rs) + rs = RadioSetting("pkt9600", "Packet 9600 gain level", + RadioSettingValueInteger(0, 100, _settings.pkt9600)) + packet.append(rs) + options = ["1200", "9600"] + rs = RadioSetting("pkt_rate", "Packet rate", + RadioSettingValueList(options, + options[_settings.pkt_rate])) + packet.append(rs) + rs = RadioSetting("proc_level", "Proc level", + RadioSettingValueInteger(0, 100, + _settings.proc_level)) + basic.append(rs) + rs = RadioSetting("rf_power_hf", "Rf power set HF", + RadioSettingValueInteger(5, 100, + _settings.rf_power_hf)) + basic.append(rs) + rs = RadioSetting("rf_power_6m", "Rf power set 6m", + RadioSettingValueInteger(5, 100, + _settings.rf_power_6m)) + basic.append(rs) + rs = RadioSetting("rf_power_vhf", "Rf power set VHF", + RadioSettingValueInteger(5, 50, + _settings.rf_power_vhf)) + basic.append(rs) + rs = RadioSetting("rf_power_uhf", "Rf power set UHF", + RadioSettingValueInteger(2, 20, + _settings.rf_power_uhf)) + basic.append(rs) + options = ["TIME", "BUSY", "STOP"] + rs = RadioSetting("scan_mode", "Scan mode", + RadioSettingValueList(options, + options[_settings.scan_mode])) + basic.append(rs) + rs = RadioSetting("scan_resume", "Scan resume", + RadioSettingValueInteger(1, 10, + _settings.scan_resume)) + basic.append(rs) + rs = RadioSetting("split_tone", "Split tone enable", + RadioSettingValueBoolean(_settings.split_tone)) + extended.append(rs) + options = ["RF-Gain", "Squelch"] + rs = RadioSetting("sql_rf_gain", "Squelch/RF-Gain", + RadioSettingValueList(options, + options[_settings.sql_rf_gain]) + ) + panel.append(rs) + rs = RadioSetting("ssb_mic", "SSB Mic gain", + RadioSettingValueInteger(0, 100, _settings.ssb_mic)) + basic.append(rs) + options = ["Off"] + ["%i" % i for i in range(1, 21)] + rs = RadioSetting("tot_time", "Time-out timer", + RadioSettingValueList(options, + options[_settings.tot_time])) + basic.append(rs) + options = ["OFF", "ATAS(HF)", "ATAS(HF&50)", "ATAS(ALL)", "TUNER"] + rs = RadioSetting("tuner_atas", "Tuner/ATAS device", + RadioSettingValueList(options, + options[_settings.tuner_atas])) + extended.append(rs) + rs = RadioSetting("tx_if_filter", "Transmit IF filter", + RadioSettingValueList(self.FILTERS, + self.FILTERS[ + _settings.tx_if_filter])) + basic.append(rs) + rs = RadioSetting("vox_delay", "VOX delay (*100 ms)", + RadioSettingValueInteger(1, 30, _settings.vox_delay)) + basic.append(rs) + rs = RadioSetting("vox_gain", "VOX Gain", + RadioSettingValueInteger(1, 100, _settings.vox_gain)) + basic.append(rs) + rs = RadioSetting("xvtr_a", "Xvtr A displacement", + RadioSettingValueInteger( + -4294967295, 4294967295, + self._memobj.xvtr_a_offset * + (-1 if _settings.xvtr_a_negative else 1))) + + extended.append(rs) + rs = RadioSetting("xvtr_b", "Xvtr B displacement", + RadioSettingValueInteger( + -4294967295, 4294967295, + self._memobj.xvtr_b_offset * + (-1 if _settings.xvtr_b_negative else 1))) + extended.append(rs) + options = ["OFF", "XVTR A", "XVTR B"] + rs = RadioSetting("xvtr_sel", "Transverter function selection", + RadioSettingValueList(options, + options[_settings.xvtr_sel])) + extended.append(rs) + + rs = RadioSetting("disp", "Display large", + RadioSettingValueBoolean(_settings.disp)) + panel.append(rs) + rs = RadioSetting("nb", "Noise blanker", + RadioSettingValueBoolean(_settings.nb)) + panelcontr.append(rs) + options = ["Auto", "Fast", "Slow", "Off"] + rs = RadioSetting("agc", "AGC", + RadioSettingValueList(options, + options[_settings.agc])) + panelcontr.append(rs) + options = ["PWR", "ALC", "SWR", "MOD"] + rs = RadioSetting("pwr_meter_mode", "Power meter mode", + RadioSettingValueList(options, + options[ + _settings.pwr_meter_mode])) + panelcontr.append(rs) + rs = RadioSetting("vox", "Vox", + RadioSettingValueBoolean(_settings.vox)) + panelcontr.append(rs) + rs = RadioSetting("bk", "Semi break-in", + RadioSettingValueBoolean(_settings.bk)) + cw.append(rs) + rs = RadioSetting("kyr", "Keyer", + RadioSettingValueBoolean(_settings.kyr)) + cw.append(rs) + options = ["enabled", "disabled"] + rs = RadioSetting("fst", "Fast", + RadioSettingValueList(options, options[_settings.fst] + )) + panelcontr.append(rs) + options = ["enabled", "disabled"] + rs = RadioSetting("lock", "Lock", + RadioSettingValueList(options, + options[_settings.lock])) + panelcontr.append(rs) + rs = RadioSetting("scope_peakhold", "Scope max hold", + RadioSettingValueBoolean(_settings.scope_peakhold)) + panelcontr.append(rs) + options = ["21", "31", "127"] + rs = RadioSetting("scope_width", "Scope width (channels)", + RadioSettingValueList(options, + options[_settings.scope_width]) + ) + panelcontr.append(rs) + rs = RadioSetting("proc", "Speech processor", + RadioSettingValueBoolean(_settings.proc)) + panelcontr.append(rs) + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + try: + LOG.debug("Setting %s(%s) <= %s" % (setting, + getattr(obj, setting), element.value)) + except AttributeError: + LOG.debug("Setting %s <= %s" % (setting, element.value)) + if setting == "arts_idw": + self._memobj.arts_idw = \ + [self._CALLSIGN_CHARSET_REV[x] + for x in str(element.value)] + elif setting in ["beacon_text1", "beacon_text2", + "beacon_text3", "op_filter1_name", + "op_filter2_name"]: + setattr(self._memobj, setting, + [self._BEACON_CHARSET_REV[x] + for x in str(element.value)]) + elif setting == "cw_delay": + val = int(element.value) + 2 + setattr(obj, "cw_delay_hi", val / 256) + setattr(obj, setting, val & 0xff) + elif setting == "dig_vox": + val = int(element.value) + setattr(obj, "dig_vox_enable", int(val > 0)) + setattr(obj, setting, val) + elif setting in ["disp_color_fix", "dsp_nr"]: + setattr(obj, setting, int(element.value) - 1) + elif setting == "disp_contrast": + setattr(obj, setting, int(element.value) - 2) + elif setting in ["xvtr_a", "xvtr_b"]: + val = int(element.value) + setattr(obj, setting + "_negative", int(val < 0)) + setattr(self._memobj, setting + "_offset", abs(val)) + else: + setattr(obj, setting, element.value) + except: + LOG.debug(element.get_name()) + raise + + +@directory.register +class FT857USRadio(FT857Radio): + """Yaesu FT857/897 (US version)""" + # seems that radios configured for 5MHz operations send one paket more + # than others so we have to distinguish sub models + MODEL = "FT-857/897 (US)" + + _model = "" + _US_model = True + _memsize = 7481 + # block 9 (140 Bytes long) is to be repeted 40 times + # should be 42 times but this way I can use original 817 functions + _block_lengths = [2, 82, 252, 196, 252, 196, 212, 55, 140, 140, 140, 38, + 176, 140] + + SPECIAL_60M = { + "M-601": -52, + "M-602": -51, + "M-603": -50, + "M-604": -49, + "M-605": -48, + } + LAST_SPECIAL60M_INDEX = -52 + + SPECIAL_MEMORIES = dict(FT857Radio.SPECIAL_MEMORIES) + SPECIAL_MEMORIES.update(SPECIAL_60M) + + SPECIAL_MEMORIES_REV = dict(list(zip(list(SPECIAL_MEMORIES.values()), + list(SPECIAL_MEMORIES.keys())))) + + # this is identical to the one in FT817ND_US_Radio but we inherit from 857 + def _get_special_60m(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_60M[number] + mem.extd_number = number + + _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX + + mem.number] + + mem = self._get_memory(mem, _mem) + + mem.immutable = ["number", "rtone", "ctone", + "extd_number", "name", "dtcs", "tmode", "cross_mode", + "dtcs_polarity", "power", "duplex", "offset", + "comment", "empty"] + + return mem + + # this is identical to the one in FT817ND_US_Radio but we inherit from 857 + def _set_special_60m(self, mem): + if mem.empty: + # can't delete 60M memories! + raise Exception("Sorry, 60M memory can't be deleted") + + cur_mem = self._get_special_60m(self.SPECIAL_MEMORIES_REV[mem.number]) + + for key in cur_mem.immutable: + if cur_mem.__dict__[key] != mem.__dict__[key]: + raise errors.RadioError("Editing field `%s' " % key + + "is not supported on M-60x channels") + + if mem.mode not in ["USB", "LSB", "CW", "CWR", "NCW", "NCWR", "DIG"]: + raise errors.RadioError("Mode {mode} is not valid " + "in 60m channels".format(mode=mem.mode)) + _mem = self._memobj.sixtymeterchannels[-self.LAST_SPECIAL60M_INDEX + + mem.number] + self._set_memory(mem, _mem) + + def get_memory(self, number): + if number in list(self.SPECIAL_60M.keys()): + return self._get_special_60m(number) + elif number < 0 and ( + self.SPECIAL_MEMORIES_REV[number] in + list(self.SPECIAL_60M.keys())): + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special_60m(self.SPECIAL_MEMORIES_REV[number]) + else: + return FT857Radio.get_memory(self, number) + + def set_memory(self, memory): + if memory.number in list(self.SPECIAL_60M.values()): + return self._set_special_60m(memory) + else: + return FT857Radio.set_memory(self, memory) + + def get_settings(self): + top = FT857Radio.get_settings(self) + basic = top[0] + rs = RadioSetting("emergency", "Emergency", + RadioSettingValueBoolean( + self._memobj.settings.emergency)) + basic.append(rs) + return top diff --git a/chirp/drivers/ft90.py b/chirp/drivers/ft90.py new file mode 100644 index 0000000..cc22a14 --- /dev/null +++ b/chirp/drivers/ft90.py @@ -0,0 +1,675 @@ +# Copyright 2011 Dan Smith +# Copyright 2013 Jens Jensen AF5MI +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, bitwise, memmap, directory, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + +import time +import os +import traceback +import string +import re +import logging + +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +CMD_ACK = chr(0x06) + +FT90_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] +FT90_MODES = ["AM", "FM", "Auto"] +# idx 3 (Bell) not supported yet +FT90_TMODES = ["", "Tone", "TSQL", "", "DTCS"] +FT90_TONES = list(chirp_common.TONES) +for tone in [165.5, 171.3, 177.3]: + FT90_TONES.remove(tone) +FT90_POWER_LEVELS_VHF = [chirp_common.PowerLevel("Hi", watts=50), + chirp_common.PowerLevel("Mid1", watts=20), + chirp_common.PowerLevel("Mid2", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + +FT90_POWER_LEVELS_UHF = [chirp_common.PowerLevel("Hi", watts=35), + chirp_common.PowerLevel("Mid1", watts=20), + chirp_common.PowerLevel("Mid2", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + +FT90_DUPLEX = ["", "-", "+", "split"] +FT90_CWID_CHARS = list(string.digits) + list(string.uppercase) + list(" ") +FT90_DTMF_CHARS = list("0123456789ABCD*#") +FT90_SPECIAL = ["vfo_vhf", "home_vhf", "vfo_uhf", "home_uhf", + "pms_1L", "pms_1U", "pms_2L", "pms_2U"] + + +@directory.register +class FT90Radio(yaesu_clone.YaesuCloneModeRadio): + VENDOR = "Yaesu" + MODEL = "FT-90" + ID = "\x8E\xF6" + + _memsize = 4063 + # block 03 (200 Bytes long) repeats 18 times; channel memories + _block_lengths = [2, 232, 24] + ([200] * 18) + [205] + + mem_format = """ +u16 id; +#seekto 0x22; +struct { + u8 dtmf_active; + u8 dtmf1_len; + u8 dtmf2_len; + u8 dtmf3_len; + u8 dtmf4_len; + u8 dtmf5_len; + u8 dtmf6_len; + u8 dtmf7_len; + u8 dtmf8_len; + u8 dtmf1[8]; + u8 dtmf2[8]; + u8 dtmf3[8]; + u8 dtmf4[8]; + u8 dtmf5[8]; + u8 dtmf6[8]; + u8 dtmf7[8]; + u8 dtmf8[8]; + char cwid[7]; + u8 unk1; + u8 scan1:2, + beep:1, + unk3:3, + rfsqlvl:2; + u8 unk4:2, + scan2:1, + cwid_en:1, + txnarrow:1, + dtmfspeed:1, + pttlock:2; + u8 dtmftxdelay:3, + fancontrol:2, + unk5:3; + u8 dimmer:3, + unk6:1, + lcdcontrast:4; + u8 dcsmode:2, + unk16:2, + tot:4; + u8 unk14; + u8 unk8:1, + ars:1, + lock:1, + txpwrsave:1, + apo:4; + u8 unk15; + u8 unk9:4, + key_lt:4; + u8 unk10:4, + key_rt:4; + u8 unk11:4, + key_p1:4; + u8 unk12:4, + key_p2:4; + u8 unk13:4, + key_acc:4; +} settings; + +struct mem_struct { + u8 mode:2, + isUhf1:1, + unknown1:2, + step:3; + u8 artsmode:2, + unknown2:1, + isUhf2:1 + power:2, + shift:2; + u8 skip:1, + showname:1, + unknown3:1, + isUhfHi:1, + unknown4:1, + tmode:3; + u32 rxfreq; + u32 txfreqoffset; + u8 UseDefaultName:1, + ars:1, + tone:6; + u8 packetmode:1, + unknown5:1, + dcstone:6; + char name[7]; +}; + +#seekto 0x86; +struct mem_struct vfo_vhf; +struct mem_struct home_vhf; +struct mem_struct vfo_uhf; +struct mem_struct home_uhf; + +#seekto 0xEB; +u8 chan_enable[23]; + +#seekto 0x101; +struct { + u8 pms_2U_enable:1, + pms_2L_enable:1, + pms_1U_enable:1, + pms_1L_enable:1, + unknown6:4; +} special_enables; + +#seekto 0x102; +struct mem_struct memory[180]; + +#seekto 0xf12; +struct mem_struct pms_1L; +struct mem_struct pms_1U; +struct mem_struct pms_2L; +struct mem_struct pms_2U; + +#seekto 0x0F7B; +struct { + char demomsg1[50]; + char demomsg2[50]; +} demomsg; +""" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect mic and hold [ACC] on mic while powering on. + ("CLONE" will appear on the display) +3. Replace mic with PC programming cable. +4. After clicking OK, press the [SET] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect mic and hold [ACC] on mic while powering on. + ("CLONE" will appear on the display) +3. Replace mic with PC programming cable. +4. Press the [DISP/SS] key + ("R" will appear on the lower left of LCD).""")) + rp.display_pre_upload_prompt_before_opening_port = False + return rp + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_ctone = False + rf.has_bank = False + rf.has_dtcs_polarity = False + rf.has_dtcs = True + rf.valid_modes = FT90_MODES + rf.valid_tmodes = FT90_TMODES + rf.valid_duplexes = FT90_DUPLEX + rf.valid_tuning_steps = FT90_STEPS + rf.valid_power_levels = FT90_POWER_LEVELS_VHF + rf.valid_name_length = 7 + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_skips = ["", "S"] + rf.valid_special_chans = FT90_SPECIAL + rf.memory_bounds = (1, 180) + rf.valid_bands = [(100000000, 230000000), + (300000000, 530000000), + (810000000, 999975000)] + + return rf + + def _read(self, blocksize, blocknum): + data = self.pipe.read(blocksize+2) + + # chew echo'd ack + self.pipe.write(CMD_ACK) + time.sleep(0.02) + self.pipe.read(1) # chew echoed ACK from 1-wire serial + + if len(data) == blocksize + 2 and data[0] == chr(blocknum): + checksum = yaesu_clone.YaesuChecksum(1, blocksize) + if checksum.get_existing(data) != checksum.get_calculated(data): + raise Exception("Checksum Failed [%02X<>%02X] block %02X, " + "data len: %i" % + (checksum.get_existing(data), + checksum.get_calculated(data), + blocknum, len(data))) + data = data[1:blocksize + 1] # Chew blocknum and checksum + + else: + raise Exception("Unable to read blocknum %02X " + "expected blocksize %i got %i." % + (blocknum, blocksize+2, len(data))) + + return data + + def _clone_in(self): + # Be very patient with the radio + self.pipe.timeout = 4 + start = time.time() + + data = "" + blocknum = 0 + status = chirp_common.Status() + status.msg = "Cloning..." + self.status_fn(status) + status.max = len(self._block_lengths) + for blocksize in self._block_lengths: + data += self._read(blocksize, blocknum) + blocknum += 1 + status.cur = blocknum + self.status_fn(status) + + LOG.info("Clone completed in %i seconds, blocks read: %i" % + (time.time() - start, blocknum)) + + return memmap.MemoryMap(data) + + def _clone_out(self): + looppredelay = 0.4 + looppostdelay = 1.9 + start = time.time() + + blocknum = 0 + pos = 0 + status = chirp_common.Status() + status.msg = "Cloning to radio..." + self.status_fn(status) + status.max = len(self._block_lengths) + + for blocksize in self._block_lengths: + checksum = yaesu_clone.YaesuChecksum(pos, pos+blocksize-1) + blocknumbyte = chr(blocknum) + payloadbytes = self.get_mmap()[pos:pos+blocksize] + checksumbyte = chr(checksum.get_calculated(self.get_mmap())) + LOG.debug("Block %i - will send from %i to %i byte " % + (blocknum, pos, pos + blocksize)) + LOG.debug(util.hexprint(blocknumbyte)) + LOG.debug(util.hexprint(payloadbytes)) + LOG.debug(util.hexprint(checksumbyte)) + # send wrapped bytes + time.sleep(looppredelay) + self.pipe.write(blocknumbyte) + self.pipe.write(payloadbytes) + self.pipe.write(checksumbyte) + tmp = self.pipe.read(blocksize + 2) # chew echo + LOG.debug("bytes echoed: ") + LOG.debug(util.hexprint(tmp)) + # radio is slow to write/ack: + time.sleep(looppostdelay) + buf = self.pipe.read(1) + LOG.debug("ack recd:") + LOG.debug(util.hexprint(buf)) + if buf != CMD_ACK: + raise Exception("Radio did not ack block %i" % blocknum) + pos += blocksize + blocknum += 1 + status.cur = blocknum + self.status_fn(status) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + def sync_in(self): + try: + self._mmap = self._clone_in() + except errors.RadioError: + raise + except Exception, e: + trace = traceback.format_exc() + raise errors.RadioError( + "Failed to communicate with radio: %s" % trace) + self.process_mmap() + + def sync_out(self): + try: + self._clone_out() + except errors.RadioError: + raise + except Exception, e: + trace = traceback.format_exc() + raise errors.RadioError( + "Failed to communicate with radio: %s" % trace) + + def process_mmap(self): + self._memobj = bitwise.parse(self.mem_format, self._mmap) + + def _get_chan_enable(self, number): + number = number - 1 + bytepos = number // 8 + bitpos = number % 8 + chan_enable = self._memobj.chan_enable[bytepos] + if chan_enable & (1 << bitpos): + return True + else: + return False + + def _set_chan_enable(self, number, enable): + number = number - 1 + bytepos = number // 8 + bitpos = number % 8 + chan_enable = self._memobj.chan_enable[bytepos] + if enable: + chan_enable = chan_enable | (1 << bitpos) # enable + else: + chan_enable = chan_enable & ~(1 << bitpos) # disable + self._memobj.chan_enable[bytepos] = chan_enable + + def get_memory(self, number): + mem = chirp_common.Memory() + if isinstance(number, str): + # special channel + _mem = getattr(self._memobj, number) + mem.number = - len(FT90_SPECIAL) + FT90_SPECIAL.index(number) + mem.extd_number = number + if re.match('^pms', mem.extd_number): + # enable pms_XY channel flag + _special_enables = self._memobj.special_enables + mem.empty = not getattr(_special_enables, + mem.extd_number + "_enable") + else: + # regular memory + _mem = self._memobj.memory[number-1] + mem.number = number + mem.empty = not self._get_chan_enable(number) + if mem.empty: + return mem # bail out, do not parse junk + mem.freq = _mem.rxfreq * 10 + mem.offset = _mem.txfreqoffset * 10 + if not _mem.tmode < len(FT90_TMODES): + _mem.tmode = 0 + mem.tmode = FT90_TMODES[_mem.tmode] + mem.rtone = FT90_TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcstone] + mem.mode = FT90_MODES[_mem.mode] + mem.duplex = FT90_DUPLEX[_mem.shift] + if mem.freq / 1000000 > 300: + mem.power = FT90_POWER_LEVELS_UHF[_mem.power] + else: + mem.power = FT90_POWER_LEVELS_VHF[_mem.power] + + # radio has a known bug with 5khz step and squelch + if _mem.step == 0 or _mem.step > len(FT90_STEPS)-1: + _mem.step = 2 + mem.tuning_step = FT90_STEPS[_mem.step] + mem.skip = _mem.skip and "S" or "" + if not all(char in chirp_common.CHARSET_ASCII + for char in str(_mem.name)): + # dont display blank/junk name + mem.name = "" + else: + mem.name = str(_mem.name) + return mem + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def set_memory(self, mem): + if mem.number < 0: # special channels + _mem = getattr(self._memobj, mem.extd_number) + if re.match('^pms', mem.extd_number): + # enable pms_XY channel flag + _special_enables = self._memobj.special_enables + setattr(_special_enables, mem.extd_number + "_enable", True) + else: + _mem = self._memobj.memory[mem.number - 1] + self._set_chan_enable(mem.number, not mem.empty) + _mem.skip = mem.skip == "S" + # radio has a known bug with 5khz step and dead squelch + if not mem.tuning_step or mem.tuning_step == FT90_STEPS[0]: + _mem.step = 2 + else: + _mem.step = FT90_STEPS.index(mem.tuning_step) + _mem.rxfreq = mem.freq / 10 + # vfo will unlock if not in right band? + if mem.freq > 300000000: + # uhf + _mem.isUhf1 = 1 + _mem.isUhf2 = 1 + if mem.freq > 810000000: + # uhf hiband + _mem.isUhfHi = 1 + else: + _mem.isUhfHi = 0 + else: + # vhf + _mem.isUhf1 = 0 + _mem.isUhf2 = 0 + _mem.isUhfHi = 0 + _mem.txfreqoffset = mem.offset / 10 + _mem.tone = FT90_TONES.index(mem.rtone) + _mem.tmode = FT90_TMODES.index(mem.tmode) + _mem.mode = FT90_MODES.index(mem.mode) + _mem.shift = FT90_DUPLEX.index(mem.duplex) + _mem.dcstone = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.step = FT90_STEPS.index(mem.tuning_step) + _mem.shift = FT90_DUPLEX.index(mem.duplex) + if mem.power: + _mem.power = FT90_POWER_LEVELS_VHF.index(mem.power) + else: + _mem.power = 3 # default to low power + if (len(mem.name) == 0): + _mem.name = bytearray.fromhex("80ffffffffffff") + _mem.showname = 0 + else: + _mem.name = str(mem.name).ljust(7) + _mem.showname = 1 + _mem.UseDefaultName = 0 + + def _decode_cwid(self, cwidarr): + cwid = "" + LOG.debug("@ +_decode_cwid:") + for byte in cwidarr.get_value(): + char = int(byte) + LOG.debug(char) + # bitwise wraps in quotes! get rid of those + if char < len(FT90_CWID_CHARS): + cwid += FT90_CWID_CHARS[char] + return cwid + + def _encode_cwid(self, cwidarr): + cwid = "" + LOG.debug("@ _encode_cwid:") + for char in cwidarr.get_value(): + cwid += chr(FT90_CWID_CHARS.index(char)) + LOG.debug(cwid) + return cwid + + def _bbcd2dtmf(self, bcdarr, strlen=16): + # doing bbcd, but with support for ABCD*# + LOG.debug(bcdarr.get_value()) + string = ''.join("%02X" % b for b in bcdarr) + LOG.debug("@_bbcd2dtmf, received: %s" % string) + string = string.replace('E', '*').replace('F', '#') + if strlen <= 16: + string = string[:strlen] + return string + + def _dtmf2bbcd(self, dtmf): + dtmfstr = dtmf.get_value() + dtmfstr = dtmfstr.replace('*', 'E').replace('#', 'F') + dtmfstr = str.ljust(dtmfstr.strip(), 16, "0") + bcdarr = list(bytearray.fromhex(dtmfstr)) + LOG.debug("@_dtmf2bbcd, sending: %s" % bcdarr) + return bcdarr + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + autodial = RadioSettingGroup("autodial", "AutoDial") + keymaps = RadioSettingGroup("keymaps", "KeyMaps") + + top = RadioSettings(basic, keymaps, autodial) + + rs = RadioSetting( + "beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + rs = RadioSetting( + "lock", "Lock", + RadioSettingValueBoolean(_settings.lock)) + basic.append(rs) + rs = RadioSetting( + "ars", "Auto Repeater Shift", + RadioSettingValueBoolean(_settings.ars)) + basic.append(rs) + rs = RadioSetting( + "txpwrsave", "TX Power Save", + RadioSettingValueBoolean(_settings.txpwrsave)) + basic.append(rs) + rs = RadioSetting( + "txnarrow", "TX Narrow", + RadioSettingValueBoolean(_settings.txnarrow)) + basic.append(rs) + options = ["Off", "S-3", "S-5", "S-Full"] + rs = RadioSetting( + "rfsqlvl", "RF Squelch Level", + RadioSettingValueList(options, options[_settings.rfsqlvl])) + basic.append(rs) + options = ["Off", "Band A", "Band B", "Both"] + rs = RadioSetting( + "pttlock", "PTT Lock", + RadioSettingValueList(options, options[_settings.pttlock])) + basic.append(rs) + + rs = RadioSetting( + "cwid_en", "CWID Enable", + RadioSettingValueBoolean(_settings.cwid_en)) + basic.append(rs) + + cwid = RadioSettingValueString(0, 7, self._decode_cwid(_settings.cwid)) + cwid.set_charset(FT90_CWID_CHARS) + rs = RadioSetting("cwid", "CWID", cwid) + basic.append(rs) + + options = ["OFF"] + map(str, range(1, 12+1)) + rs = RadioSetting( + "apo", "APO time (hrs)", + RadioSettingValueList(options, options[_settings.apo])) + basic.append(rs) + + options = ["Off"] + map(str, range(1, 60+1)) + rs = RadioSetting( + "tot", "Time Out Timer (mins)", + RadioSettingValueList(options, options[_settings.tot])) + basic.append(rs) + + options = ["off", "Auto/TX", "Auto", "TX"] + rs = RadioSetting( + "fancontrol", "Fan Control", + RadioSettingValueList(options, options[_settings.fancontrol])) + basic.append(rs) + + keyopts = ["Scan Up", "Scan Down", "Repeater", "Reverse", "Tone Burst", + "Tx Power", "Home Ch", "VFO/MR", "Tone", "Priority"] + rs = RadioSetting( + "key_lt", "Left Key", + RadioSettingValueList(keyopts, keyopts[_settings.key_lt])) + keymaps.append(rs) + rs = RadioSetting( + "key_rt", "Right Key", + RadioSettingValueList(keyopts, keyopts[_settings.key_rt])) + keymaps.append(rs) + rs = RadioSetting( + "key_p1", "P1 Key", + RadioSettingValueList(keyopts, keyopts[_settings.key_p1])) + keymaps.append(rs) + rs = RadioSetting( + "key_p2", "P2 Key", + RadioSettingValueList(keyopts, keyopts[_settings.key_p2])) + keymaps.append(rs) + rs = RadioSetting( + "key_acc", "ACC Key", + RadioSettingValueList(keyopts, keyopts[_settings.key_acc])) + keymaps.append(rs) + + options = map(str, range(0, 12+1)) + rs = RadioSetting( + "lcdcontrast", "LCD Contrast", + RadioSettingValueList(options, options[_settings.lcdcontrast])) + basic.append(rs) + + options = ["off", "d4", "d3", "d2", "d1"] + rs = RadioSetting( + "dimmer", "Dimmer", + RadioSettingValueList(options, options[_settings.dimmer])) + basic.append(rs) + + options = ["TRX Normal", "RX Reverse", "TX Reverse", "TRX Reverse"] + rs = RadioSetting( + "dcsmode", "DCS Mode", + RadioSettingValueList(options, options[_settings.dcsmode])) + basic.append(rs) + + options = ["50 ms", "100 ms"] + rs = RadioSetting( + "dtmfspeed", "DTMF Speed", + RadioSettingValueList(options, options[_settings.dtmfspeed])) + autodial.append(rs) + + options = ["50 ms", "250 ms", "450 ms", "750 ms", "1 sec"] + rs = RadioSetting( + "dtmftxdelay", "DTMF TX Delay", + RadioSettingValueList(options, options[_settings.dtmftxdelay])) + autodial.append(rs) + + options = map(str, range(1, 8 + 1)) + rs = RadioSetting( + "dtmf_active", "DTMF Active", + RadioSettingValueList(options, options[_settings.dtmf_active])) + autodial.append(rs) + + # setup 8 dtmf autodial entries + for i in map(str, range(1, 9)): + objname = "dtmf" + i + dtmfsetting = getattr(_settings, objname) + dtmflen = getattr(_settings, objname + "_len") + dtmfstr = self._bbcd2dtmf(dtmfsetting, dtmflen) + dtmf = RadioSettingValueString(0, 16, dtmfstr) + dtmf.set_charset(FT90_DTMF_CHARS + list(" ")) + rs = RadioSetting(objname, objname.upper(), dtmf) + autodial.append(rs) + + return top + + def set_settings(self, uisettings): + _settings = self._memobj.settings + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + oldval = getattr(_settings, setting) + newval = element.value + if setting == "cwid": + newval = self._encode_cwid(newval) + if re.match('dtmf\d', setting): + # set dtmf length field and then get bcd dtmf + dtmfstrlen = len(str(newval).strip()) + setattr(_settings, setting + "_len", dtmfstrlen) + newval = self._dtmf2bbcd(newval) + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception, e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/ftm3200d.py b/chirp/drivers/ftm3200d.py new file mode 100644 index 0000000..1b27012 --- /dev/null +++ b/chirp/drivers/ftm3200d.py @@ -0,0 +1,201 @@ +# Copyright 2010 Dan Smith +# Copyright 2017 Wade Simmons +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from textwrap import dedent + +from chirp.drivers import yaesu_clone, ft1d +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSettings + +LOG = logging.getLogger(__name__) + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=30), + chirp_common.PowerLevel("Hi", watts=65)] + +TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", None, None, "Pager", "Cross"] +CROSS_MODES = [None, "DTCS->", "Tone->DTCS", "DTCS->Tone"] + +MODES = ["FM", "NFM"] +STEPS = [0, 5, 6.25, 10, 12.5, 15, 20, 25, 50, 100] # 0 = auto +RFSQUELCH = ["OFF", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"] + +# Charset is subset of ASCII + some unknown chars \x80-\x86 +VALID_CHARS = ["%i" % int(x) for x in range(0, 10)] + \ + list(":>=After clicking OK, press the [REV(DW)] key + to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to DATA terminal. + 3. Press and hold in the [MHz(SETUP)] key while turning the radio + on ("CLONE" will appear on the display). + 4. Press the [MHz(SETUP)] key + ("-WAIT-" will appear on the LCD).""")) + return rp + + def process_mmap(self): + mem_format = ft1d.MEM_FORMAT + MEM_FORMAT + self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.valid_modes = list(MODES) + rf.valid_tmodes = [x for x in TMODES if x is not None] + rf.valid_cross_modes = [x for x in CROSS_MODES if x is not None] + rf.valid_duplexes = list(ft1d.DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(136000000, 174000000)] + # rf.valid_skips = SKIPS + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(VALID_CHARS) + rf.valid_name_length = 8 + rf.memory_bounds = (1, 199) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_bank = False + rf.has_bank_names = False + # disable until implemented + rf.has_settings = False + return rf + + def _decode_label(self, mem): + # TODO preserve the unknown \x80-x86 chars? + return str(mem.label).rstrip("\xFF").decode('ascii', 'replace') + + def _encode_label(self, mem): + label = mem.name.rstrip().encode('ascii', 'ignore') + return self._add_ff_pad(label, 16) + + def _encode_charsetbits(self, mem): + # TODO this is a setting to decide if the memory should be displayed + # as a name or frequency. Should we expose this setting to the user + # instead of autoselecting it (and losing their preference)? + if mem.name.rstrip() == '': + return [0x00, 0x00] + return [0x00, 0x80] + + def _decode_power_level(self, mem): + return POWER_LEVELS[mem.power - 1] + + def _encode_power_level(self, mem): + return POWER_LEVELS.index(mem.power) + 1 + + def _decode_mode(self, mem): + return MODES[mem.mode_alt] + + def _encode_mode(self, mem): + return MODES.index(mem.mode) + + def _get_tmode(self, mem, _mem): + if _mem.tone_mode > 8: + tmode = "Cross" + mem.cross_mode = CROSS_MODES[_mem.tone_mode - 8] + else: + tmode = TMODES[_mem.tone_mode] + + if tmode == "Pager": + # TODO chirp_common does not allow 'Pager' + # Expose as a different setting? + mem.tmode = "" + else: + mem.tmode = tmode + + def _set_tmode(self, _mem, mem): + if mem.tmode == "Cross": + _mem.tone_mode = 8 + CROSS_MODES.index(mem.cross_mode) + else: + _mem.tone_mode = TMODES.index(mem.tmode) + + def _set_mode(self, _mem, mem): + _mem.mode_alt = self._encode_mode(mem) + + def get_bank_model(self): + return None + + def _debank(self, mem): + return + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8), + yaesu_clone.YaesuChecksum(0x06CA, 0x0748), + yaesu_clone.YaesuChecksum(0x074A, 0x07C8), + yaesu_clone.YaesuChecksum(0x07CA, 0x0848), + yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)] + + def _get_settings(self): + # TODO + top = RadioSettings() + return top + + @classmethod + def _wipe_memory(cls, mem): + mem.set_raw("\x00" * (mem.size() / 8)) + + def sync_out(self): + # Need to give enough time for the radio to ACK after writes + self.pipe.timeout = 1 + return super(FTM3200Radio, self).sync_out() diff --git a/chirp/drivers/ftm350.py b/chirp/drivers/ftm350.py new file mode 100644 index 0000000..ea5bc2f --- /dev/null +++ b/chirp/drivers/ftm350.py @@ -0,0 +1,443 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import os +import logging + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, errors, util, bitwise, memmap +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings +from chirp.settings import RadioSettingValueInteger, RadioSettingValueString + +LOG = logging.getLogger(__name__) + +mem_format = """ +struct mem { + u8 used:1, + skip:2, + unknown1:5; + u8 unknown2:1, + mode:3, + unknown8:1, + oddsplit:1, + duplex:2; + bbcd freq[3]; + u8 unknownA:1, + tmode:3, + unknownB:4; + bbcd split[3]; + u8 power:2, + tone:6; + u8 unknownC:1, + dtcs:7; + u8 showalpha:1, + unknown5:7; + u8 unknown6; + u8 offset; + u8 unknown7[2]; +}; + +struct lab { + u8 string[8]; +}; + +#seekto 0x0508; +struct { + char call[6]; + u8 ssid; +} aprs_my_callsign; + +#seekto 0x0480; +struct mem left_memory_zero; +#seekto 0x04A0; +struct lab left_label_zero; +#seekto 0x04C0; +struct mem right_memory_zero; +#seekto 0x04E0; +struct lab right_label_zero; + +#seekto 0x0800; +struct mem left_memory[500]; + +#seekto 0x2860; +struct mem right_memory[500]; + +#seekto 0x48C0; +struct lab left_label[518]; +struct lab right_label[518]; +""" + +_TMODES = ["", "Tone", "TSQL", "-RVT", "DTCS", "-PR", "-PAG"] +TMODES = ["", "Tone", "TSQL", "", "DTCS", "", ""] +MODES = ["FM", "AM", "NFM", "", "WFM"] +DUPLEXES = ["", "", "-", "+", "split"] +# TODO: add japaneese characters (viewable in special menu, scroll backwards) +CHARSET = \ + ('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!"' + + '#$%&`()*+,-./:;<=>?@[\\]^_`{|}~?????? ' + '?' * 91) + +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=50), + chirp_common.PowerLevel("Mid", watts=20), + chirp_common.PowerLevel("Low", watts=5)] + +SKIPS = ["", "S", "P"] + + +def aprs_call_to_str(_call): + call = "" + for i in str(_call): + if i == "\xca": + break + call += i + return call + + +def _safe_read(radio, length): + data = "" + while len(data) < length: + data += radio.pipe.read(length - len(data)) + return data + + +def _clone_in(radio): + data = "" + + radio.pipe.timeout = 1 + attempts = 30 + + data = memmap.MemoryMap("\x00" * (radio._memsize + 128)) + length = 0 + last_addr = 0 + while length < radio._memsize: + frame = radio.pipe.read(131) + if length and not frame: + raise errors.RadioError("Radio not responding") + + if not frame: + attempts -= 1 + if attempts <= 0: + raise errors.RadioError("Radio not responding") + + if frame: + addr, = struct.unpack(">H", frame[0:2]) + checksum = ord(frame[130]) + block = frame[2:130] + + cs = 0 + for i in frame[:-1]: + cs = (cs + ord(i)) % 256 + if cs != checksum: + LOG.debug("Calc: %02x Real: %02x Len: %i" % + (cs, checksum, len(block))) + raise errors.RadioError("Block failed checksum") + + radio.pipe.write("\x06") + time.sleep(0.05) + + if (last_addr + 128) != addr: + LOG.debug("Gap, expecting %04x, got %04x" % + (last_addr+128, addr)) + last_addr = addr + data[addr] = block + length += len(block) + + status = chirp_common.Status() + status.cur = length + status.max = radio._memsize + status.msg = "Cloning from radio" + radio.status_fn(status) + + return data + + +def _clone_out(radio): + radio.pipe.timeout = 1 + + # Seriously, WTF Yaesu? + ranges = [ + (0x0000, 0x0000), + (0x0100, 0x0380), + (0x0480, 0xFF80), + (0x0080, 0x0080), + (0xFFFE, 0xFFFE), + ] + + for start, end in ranges: + for i in range(start, end+1, 128): + block = radio._mmap[i:i + 128] + frame = struct.pack(">H", i) + block + cs = 0 + for byte in frame: + cs += ord(byte) + frame += chr(cs % 256) + radio.pipe.write(frame) + ack = radio.pipe.read(1) + if ack != "\x06": + raise errors.RadioError("Radio refused block %i" % (i / 128)) + time.sleep(0.05) + + status = chirp_common.Status() + status.cur = i + 128 + status.max = radio._memsize + status.msg = "Cloning to radio" + radio.status_fn(status) + + +def get_freq(rawfreq): + """Decode a frequency that may include a fractional step flag""" + # Ugh. The 0x80 and 0x40 indicate values to add to get the + # real frequency. Gross. + if rawfreq > 8000000000: + rawfreq = (rawfreq - 8000000000) + 5000 + + if rawfreq > 4000000000: + rawfreq = (rawfreq - 4000000000) + 2500 + + if rawfreq > 2000000000: + rawfreq = (rawfreq - 2000000000) + 1250 + + return rawfreq + + +def set_freq(freq, obj, field): + """Encode a frequency with any necessary fractional step flags""" + obj[field] = freq / 10000 + frac = freq % 10000 + + if frac >= 5000: + frac -= 5000 + obj[field][0].set_bits(0x80) + + if frac >= 2500: + frac -= 2500 + obj[field][0].set_bits(0x40) + + if frac >= 1250: + frac -= 1250 + obj[field][0].set_bits(0x20) + + return freq + + +@directory.register +class FTM350Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu FTM-350""" + BAUD_RATE = 48000 + VENDOR = "Yaesu" + MODEL = "FTM-350" + + _model = "" + _memsize = 65536 + _vfo = "" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_ctone = False + rf.has_settings = self._vfo == "left" + rf.has_tuning_step = False + rf.has_dtcs_polarity = False + rf.has_sub_devices = self.VARIANT == "" + rf.valid_skips = [] # FIXME: Finish this + rf.valid_tmodes = [""] + [x for x in TMODES if x] + rf.valid_modes = [x for x in MODES if x] + rf.valid_duplexes = DUPLEXES + rf.valid_skips = SKIPS + rf.valid_name_length = 8 + rf.valid_characters = CHARSET + rf.memory_bounds = (0, 500) + rf.valid_power_levels = POWER_LEVELS + rf.valid_bands = [(500000, 1800000), + (76000000, 250000000), + (30000000, 1000000000)] + rf.can_odd_split = True + rf.valid_tuning_steps = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, + 25.0, 50.0, 100.0, 200.0] + + return rf + + def get_sub_devices(self): + return [FTM350RadioLeft(self._mmap), FTM350RadioRight(self._mmap)] + + def sync_in(self): + try: + self._mmap = _clone_in(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to download from radio (%s)" % e) + self.process_mmap() + + def sync_out(self): + try: + _clone_out(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to upload to radio (%s)" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_raw_memory(self, number): + + def identity(o): + return o + + def indexed(o): + return o[number - 1] + + if number == 0: + suffix = "_zero" + fn = identity + else: + suffix = "" + fn = indexed + return (repr(fn(self._memory_obj(suffix))) + + repr(fn(self._label_obj(suffix)))) + + def _memory_obj(self, suffix=""): + return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix)) + + def _label_obj(self, suffix=""): + return getattr(self._memobj, "%s_label%s" % (self._vfo, suffix)) + + def get_memory(self, number): + if number == 0: + _mem = self._memory_obj("_zero") + _lab = self._label_obj("_zero") + else: + _mem = self._memory_obj()[number - 1] + _lab = self._label_obj()[number - 1] + mem = chirp_common.Memory() + mem.number = number + + if not _mem.used: + mem.empty = True + return mem + + mem.freq = get_freq(int(_mem.freq) * 10000) + mem.rtone = chirp_common.TONES[_mem.tone] + mem.tmode = TMODES[_mem.tmode] + + if _mem.oddsplit: + mem.duplex = "split" + mem.offset = get_freq(int(_mem.split) * 10000) + else: + mem.duplex = DUPLEXES[_mem.duplex] + mem.offset = int(_mem.offset) * 50000 + + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.mode = MODES[_mem.mode] + mem.skip = SKIPS[_mem.skip] + mem.power = POWER_LEVELS[_mem.power] + + for char in _lab.string: + if char == 0xCA: + break + try: + mem.name += CHARSET[char] + except IndexError: + mem.name += "?" + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + if mem.number == 0: + _mem = self._memory_obj("_zero") + _lab = self._label_obj("_zero") + else: + _mem = self._memory_obj()[mem.number - 1] + _lab = self._label_obj()[mem.number - 1] + _mem.used = not mem.empty + if mem.empty: + return + + set_freq(mem.freq, _mem, 'freq') + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tmode = TMODES.index(mem.tmode) + _mem.mode = MODES.index(mem.mode) + _mem.skip = SKIPS.index(mem.skip) + + _mem.oddsplit = 0 + _mem.duplex = 0 + if mem.duplex == "split": + set_freq(mem.offset, _mem, 'split') + _mem.oddsplit = 1 + else: + _mem.offset = mem.offset / 50000 + _mem.duplex = DUPLEXES.index(mem.duplex) + + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + for i in range(0, 8): + try: + char = CHARSET.index(mem.name[i]) + except IndexError: + char = 0xCA + _lab.string[i] = char + _mem.showalpha = mem.name.strip() != "" + + @classmethod + def match_model(self, filedata, filename): + return filedata.startswith("AH033$") + + def get_settings(self): + top = RadioSettings() + + aprs = RadioSettingGroup("aprs", "APRS") + top.append(aprs) + + myc = self._memobj.aprs_my_callsign + rs = RadioSetting("aprs_my_callsign.call", "APRS My Callsign", + RadioSettingValueString(0, 6, + aprs_call_to_str(myc.call))) + aprs.append(rs) + + rs = RadioSetting("aprs_my_callsign.ssid", "APRS My SSID", + RadioSettingValueInteger(0, 15, myc.ssid)) + aprs.append(rs) + + return top + + def set_settings(self, settings): + for setting in settings: + if not isinstance(setting, RadioSetting): + self.set_settings(setting) + continue + + # Quick hack to make these work + if setting.get_name() == "aprs_my_callsign.call": + self._memobj.aprs_my_callsign.call = \ + setting.value.get_value().upper().replace(" ", "\xCA") + elif setting.get_name() == "aprs_my_callsign.ssid": + self._memobj.aprs_my_callsign.ssid = setting.value + + +class FTM350RadioLeft(FTM350Radio): + VARIANT = "Left" + _vfo = "left" + + +class FTM350RadioRight(FTM350Radio): + VARIANT = "Right" + _vfo = "right" diff --git a/chirp/drivers/generic_csv.py b/chirp/drivers/generic_csv.py new file mode 100644 index 0000000..0c60108 --- /dev/null +++ b/chirp/drivers/generic_csv.py @@ -0,0 +1,471 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import csv +import logging + +from chirp import chirp_common, errors, directory + +LOG = logging.getLogger(__name__) + + +class OmittedHeaderError(Exception): + """Internal exception to signal that a column has been omitted""" + pass + + +def get_datum_by_header(headers, data, header): + """Return the column corresponding to @headers[@header] from @data""" + if header not in headers: + raise OmittedHeaderError("Header %s not provided" % header) + + try: + return data[headers.index(header)] + except IndexError: + raise OmittedHeaderError("Header %s not provided on this line" % + header) + + +def write_memory(writer, mem): + """Write @mem using @writer if not empty""" + if mem.empty: + return + writer.writerow(mem.to_csv()) + + +@directory.register +class CSVRadio(chirp_common.FileBackedRadio, chirp_common.IcomDstarSupport): + """A driver for Generic CSV files""" + VENDOR = "Generic" + MODEL = "CSV" + FILE_EXTENSION = "csv" + + ATTR_MAP = { + "Location": (int, "number"), + "Name": (str, "name"), + "Frequency": (chirp_common.parse_freq, "freq"), + "Duplex": (str, "duplex"), + "Offset": (chirp_common.parse_freq, "offset"), + "Tone": (str, "tmode"), + "rToneFreq": (float, "rtone"), + "cToneFreq": (float, "ctone"), + "DtcsCode": (int, "dtcs"), + "DtcsPolarity": (str, "dtcs_polarity"), + "Mode": (str, "mode"), + "TStep": (float, "tuning_step"), + "Skip": (str, "skip"), + "URCALL": (str, "dv_urcall"), + "RPT1CALL": (str, "dv_rpt1call"), + "RPT2CALL": (str, "dv_rpt2call"), + "Comment": (str, "comment"), + } + + def _blank(self): + self.errors = [] + self.memories = [] + for i in range(0, 1000): + mem = chirp_common.Memory() + mem.number = i + mem.empty = True + self.memories.append(mem) + + def __init__(self, pipe): + chirp_common.FileBackedRadio.__init__(self, None) + self.memories = [] + self.file_has_rTone = None # Set in load(), used in _clean_tmode() + self.file_has_cTone = None + + self._filename = pipe + if self._filename and os.path.exists(self._filename): + self.load() + else: + self._blank() + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.requires_call_lists = False + rf.has_implicit_calls = False + rf.memory_bounds = (0, len(self.memories)) + rf.has_infinite_number = True + rf.has_nostep_tuning = True + rf.has_comment = True + rf.can_odd_split = True + + rf.valid_modes = list(chirp_common.MODES) + rf.valid_tmodes = list(chirp_common.TONE_MODES) + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS) + rf.valid_bands = [(1, 10000000000)] + rf.valid_skips = ["", "S"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 999 + + return rf + + def _clean(self, headers, line, mem): + """Runs post-processing functions on new mem objects. + + This is useful for parsing other CSV dialects when multiple columns + convert to a single Chirp column.""" + + for attr in dir(mem): + fname = "_clean_%s" % attr + if hasattr(self, fname): + mem = getattr(self, fname)(headers, line, mem) + + return mem + + def _clean_tmode(self, headers, line, mem): + """ If there is exactly one of [rToneFreq, cToneFreq] columns in the + csv file, use it for both rtone & ctone. Makes TSQL use friendlier.""" + + if self.file_has_rTone and not self.file_has_cTone: + mem.ctone = mem.rtone + elif self.file_has_cTone and not self.file_has_rTone: + mem.rtone = mem.ctone + + return mem + + def _parse_csv_data_line(self, headers, line): + mem = chirp_common.Memory() + try: + if get_datum_by_header(headers, line, "Mode") == "DV": + mem = chirp_common.DVMemory() + except OmittedHeaderError: + pass + + for header in headers: + try: + typ, attr = self.ATTR_MAP[header] + except KeyError: + continue + try: + val = get_datum_by_header(headers, line, header) + if not val and typ == int: + val = None + else: + val = typ(val) + if hasattr(mem, attr): + setattr(mem, attr, val) + except OmittedHeaderError as e: + pass + except Exception as e: + raise Exception("[%s] %s" % (attr, e)) + + return self._clean(headers, line, mem) + + def load(self, filename=None): + if filename is None and self._filename is None: + raise errors.RadioError("Need a location to load from") + + if filename: + self._filename = filename + + self._blank() + + with open(self._filename, "rU") as f: + header = f.readline().strip() + f.seek(0, 0) + return self._load(f) + + def _load(self, f): + reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"') + + good = 0 + lineno = 0 + for line in reader: + lineno += 1 + if lineno == 1: + header = line + self.file_has_rTone = "rToneFreq" in header + self.file_has_cTone = "cToneFreq" in header + continue + + if len(header) > len(line): + LOG.error("Line %i has %i columns, expected %i", + lineno, len(line), len(header)) + self.errors.append("Column number mismatch on line %i" % + lineno) + continue + + try: + mem = self._parse_csv_data_line(header, line) + if mem.number is None: + raise Exception("Invalid Location field" % lineno) + except Exception as e: + LOG.error("Line %i: %s", lineno, e) + self.errors.append("Line %i: %s" % (lineno, e)) + continue + + self._grow(mem.number) + self.memories[mem.number] = mem + good += 1 + + if not good: + LOG.error(self.errors) + raise errors.InvalidDataError("No channels found") + + def save(self, filename=None): + if filename is None and self._filename is None: + raise errors.RadioError("Need a location to save to") + + if filename: + self._filename = filename + + with open(self._filename, "w") as f: + writer = csv.writer(f, delimiter=chirp_common.SEPCHAR) + writer.writerow(chirp_common.Memory.CSV_FORMAT) + + for mem in self.memories: + write_memory(writer, mem) + + # MMAP compatibility + def save_mmap(self, filename): + return self.save(filename) + + def load_mmap(self, filename): + return self.load(filename) + + def get_memories(self, lo=0, hi=999): + return [x for x in self.memories if x.number >= lo and x.number <= hi] + + def get_memory(self, number): + try: + return self.memories[number] + except: + raise errors.InvalidMemoryLocation("No such memory %s" % number) + + def _grow(self, target): + delta = target - len(self.memories) + if delta < 0: + return + + delta += 1 + + for i in range(len(self.memories), len(self.memories) + delta + 1): + mem = chirp_common.Memory() + mem.empty = True + mem.number = i + self.memories.append(mem) + + def set_memory(self, newmem): + self._grow(newmem.number) + self.memories[newmem.number] = newmem + + def erase_memory(self, number): + mem = chirp_common.Memory() + mem.number = number + mem.empty = True + self.memories[number] = mem + + def get_raw_memory(self, number): + return ",".join(chirp_common.Memory.CSV_FORMAT) + \ + os.linesep + \ + ",".join(self.memories[number].to_csv()) + + @classmethod + def match_model(cls, filedata, filename): + """Match files ending in .CSV""" + try: + filedata = filedata.decode() + except UnicodeDecodeError: + # CSV files are text + return False + return filename.lower().endswith("." + cls.FILE_EXTENSION) and \ + (filedata.startswith("Location,") or filedata == "") + + +@directory.register +class CommanderCSVRadio(CSVRadio): + """A driver for reading CSV files generated by KG-UV Commander software""" + VENDOR = "Commander" + MODEL = "KG-UV" + FILE_EXTENSION = "csv" + + MODE_MAP = { + "NARR": "NFM", + "WIDE": "FM", + } + + SCAN_MAP = { + "ON": "", + "OFF": "S" + } + + ATTR_MAP = { + "#": (int, "number"), + "Name": (str, "name"), + "RX Freq": (chirp_common.parse_freq, "freq"), + "Scan": (lambda v: CommanderCSVRadio.SCAN_MAP.get(v), "skip"), + "TX Dev": (lambda v: CommanderCSVRadio.MODE_MAP.get(v), "mode"), + "Group/Notes": (str, "comment"), + } + + def _clean_number(self, headers, line, mem): + if mem.number == 0: + for memory in self.memories: + if memory.empty: + mem.number = memory.number + break + return mem + + def _clean_duplex(self, headers, line, mem): + try: + txfreq = chirp_common.parse_freq( + get_datum_by_header(headers, line, "TX Freq")) + except ValueError: + mem.duplex = "off" + return mem + + if mem.freq == txfreq: + mem.duplex = "" + elif txfreq: + mem.duplex = "split" + mem.offset = txfreq + + return mem + + def _clean_tmode(self, headers, line, mem): + rtone = get_datum_by_header(headers, line, "Encode") + ctone = get_datum_by_header(headers, line, "Decode") + if rtone == "OFF": + rtone = None + else: + rtone = float(rtone) + + if ctone == "OFF": + ctone = None + else: + ctone = float(ctone) + + if rtone: + mem.tmode = "Tone" + if ctone: + mem.tmode = "TSQL" + + mem.rtone = rtone or 88.5 + mem.ctone = ctone or mem.rtone + + return mem + + @classmethod + def match_model(cls, filedata, filename): + """Match files ending in .csv and using Commander column names.""" + return filename.lower().endswith("." + cls.FILE_EXTENSION) and \ + filedata.startswith("Name,RX Freq,TX Freq,Decode,Encode,TX Pwr," + "Scan,TX Dev,Busy Lck,Group/Notes") or \ + filedata.startswith('"#","Name","RX Freq","TX Freq","Decode",' + '"Encode","TX Pwr","Scan","TX Dev",' + '"Busy Lck","Group/Notes"') + + +@directory.register +class RTCSVRadio(CSVRadio): + """A driver for reading CSV files generated by RT Systems software""" + VENDOR = "RT Systems" + MODEL = "CSV" + FILE_EXTENSION = "csv" + + DUPLEX_MAP = { + "Minus": "-", + "Plus": "+", + "Simplex": "", + "Split": "split", + } + + SKIP_MAP = { + "Off": "", + "On": "S", + "P Scan": "P", + "Skip": "S", + } + + TMODE_MAP = { + "None": "", + "T Sql": "TSQL", + } + + BOOL_MAP = { + "Off": False, + "On": True, + } + + ATTR_MAP = { + "Channel Number": (int, "number"), + "Receive Frequency": (chirp_common.parse_freq, "freq"), + "Offset Frequency": (chirp_common.parse_freq, "offset"), + "Offset Direction": (lambda v: + RTCSVRadio.DUPLEX_MAP.get(v, v), "duplex"), + "Operating Mode": (str, "mode"), + "Name": (str, "name"), + "Tone Mode": (lambda v: + RTCSVRadio.TMODE_MAP.get(v, v), "tmode"), + "CTCSS": (lambda v: + float(v.split(" ")[0]), "rtone"), + "DCS": (int, "dtcs"), + "Skip": (lambda v: + RTCSVRadio.SKIP_MAP.get(v, v), "skip"), + "Step": (lambda v: + float(v.split(" ")[0]), "tuning_step"), + "Mask": (lambda v: + RTCSVRadio.BOOL_MAP.get(v, v), "empty",), + "Comment": (str, "comment"), + } + + def _clean_duplex(self, headers, line, mem): + if mem.duplex == "split": + try: + val = get_datum_by_header(headers, line, "Transmit Frequency") + val = chirp_common.parse_freq(val) + mem.offset = val + except OmittedHeaderError: + pass + + return mem + + def _clean_mode(self, headers, line, mem): + if mem.mode == "FM": + try: + val = get_datum_by_header(headers, line, "Half Dev") + if self.BOOL_MAP[val]: + mem.mode = "FMN" + except OmittedHeaderError: + pass + + return mem + + def _clean_ctone(self, headers, line, mem): + # RT Systems only stores a single tone value + mem.ctone = mem.rtone + return mem + + @classmethod + def match_model(cls, filedata, filename): + """Match files ending in .csv and using RT Systems column names.""" + # RT Systems provides a different set of columns for each radio. + # We attempt to match only the first few columns, hoping they are + # consistent across radio models. + try: + filedata = filedata.decode() + except UnicodeDecodeError: + # CSV files are text + return False + return filename.lower().endswith("." + cls.FILE_EXTENSION) and \ + filedata.startswith("Channel Number,Receive Frequency," + "Transmit Frequency,Offset Frequency," + "Offset Direction,Operating Mode," + "Name,Tone Mode,CTCSS,DCS") diff --git a/chirp/drivers/generic_tpe.py b/chirp/drivers/generic_tpe.py new file mode 100644 index 0000000..dced1c5 --- /dev/null +++ b/chirp/drivers/generic_tpe.py @@ -0,0 +1,60 @@ +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +try: + import UserDict +except ImportError: + from collections import UserDict +from chirp import chirp_common, directory +from chirp.drivers import generic_csv + + +@directory.register +class TpeRadio(generic_csv.CSVRadio): + """Generic ARRL Travel Plus""" + VENDOR = "ARRL" + MODEL = "Travel Plus" + FILE_EXTENSION = "tpe" + + ATTR_MAP = { + "Sequence Number": (int, "number"), + "Location": (str, "comment"), + "Call Sign": (str, "name"), + "Output Frequency": (chirp_common.parse_freq, "freq"), + "Input Frequency": (str, "duplex"), + "CTCSS Tones": (lambda v: float(v) + if v and float(v) in chirp_common.TONES + else 88.5, "rtone"), + "Repeater Notes": (str, "comment"), + } + + def _clean_tmode(self, headers, line, mem): + try: + val = generic_csv.get_datum_by_header(headers, line, "CTCSS Tones") + if val and float(val) in chirp_common.TONES: + mem.tmode = "Tone" + except generic_csv.OmittedHeaderError: + pass + + return mem + + def _clean_ctone(self, headers, line, mem): + # TPE only stores a single tone value + mem.ctone = mem.rtone + return mem + + @classmethod + def match_model(cls, filedata, filename): + return filename.lower().endswith("." + cls.FILE_EXTENSION) diff --git a/chirp/drivers/gmrsuv1.py b/chirp/drivers/gmrsuv1.py new file mode 100644 index 0000000..7bb1d2f --- /dev/null +++ b/chirp/drivers/gmrsuv1.py @@ -0,0 +1,1049 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp.drivers import baofeng_common +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, \ + InvalidValueError +from textwrap import dedent + +##### MAGICS ######################################################### + +# BTECH GMRS-V1 magic string +MSTRING_GMRSV1 = "\x50\x5F\x20\x15\x12\x15\x4D" + +##### ID strings ##################################################### + +# BTECH GMRS-V1 +GMRSV1_fp1 = "US32411" # original +GMRSV1_fp2 = "US32416" # original +GMRSV1_fp3 = "US32418" # new rules +GMRSV1_fp4 = "US32412" # original + +DTMF_CHARS = "0123456789 *#ABCD" +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] + +LIST_AB = ["A", "B"] +LIST_ALMOD = ["Off", "Site", "Tone", "Code"] +LIST_BANDWIDTH = ["Wide", "Narrow"] +LIST_COLOR = ["Off", "Blue", "Orange", "Purple"] +LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"] +LIST_MODE = ["Channel", "Name", "Frequency"] +LIST_OFF1TO9 = ["Off"] + list("123456789") +LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"] +LIST_OFFAB = ["Off"] + LIST_AB +LIST_RESUME = ["TO", "CO", "SE"] +LIST_PONMSG = ["Full", "Message"] +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_SCODE = ["%s" % x for x in range(1, 16)] +LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)] +LIST_RTONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SHIFTD = ["Off", "+", "-"] +LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)] +LIST_STEP = [str(x) for x in STEPS] +LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)] +LIST_TXPOWER = ["High", "Low"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_WORKMODE = ["Frequency", "Channel"] + +GMRS_FREQS1 = [462.5625, 462.5875, 462.6125, 462.6375, 462.6625, + 462.6875, 462.7125] +GMRS_FREQS2 = [467.5625, 467.5875, 467.6125, 467.6375, 467.6625, + 467.6875, 467.7125] +GMRS_FREQS3 = [462.5500, 462.5750, 462.6000, 462.6250, 462.6500, + 462.6750, 462.7000, 462.7250] +GMRS_FREQS_ORIG = GMRS_FREQS1 + GMRS_FREQS3 * 2 +GMRS_FREQS_2017 = GMRS_FREQS1 + GMRS_FREQS2 + GMRS_FREQS3 * 2 + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x1EF0:0x1EF7] + + if rid in cls._fileid: + return True + + return False + + +@directory.register +class GMRSV1(baofeng_common.BaofengCommonHT): + """BTech GMRS-V1""" + VENDOR = "BTECH" + MODEL = "GMRS-V1" + + _fileid = [GMRSV1_fp3, GMRSV1_fp2, GMRSV1_fp1, ] + _is_orig = [GMRSV1_fp2, GMRSV1_fp1, GMRSV1_fp4, ] + + _magic = [MSTRING_GMRSV1, ] + _magic_response_length = 8 + _fw_ver_start = 0x1EF0 + _recv_block_size = 0x40 + _mem_size = 0x2000 + _ack_block = True + + _ranges = [(0x0000, 0x0DF0), + (0x0E00, 0x1800), + (0x1EE0, 0x1EF0), + (0x1F60, 0x1F70), + (0x1F80, 0x1F90), + (0x1FC0, 0x1FD0)] + _send_block_size = 0x10 + + MODES = ["NFM", "FM"] + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "!@#$%^&*()+-=[]:\";'<>?,./" + LENGTH_NAME = 7 + SKIP_VALUES = ["", "S"] + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Low", watts=2.00)] + VALID_BANDS = [(130000000, 180000000), + (400000000, 521000000)] + PTTID_LIST = LIST_PTTID + SCODE_LIST = LIST_SCODE + + + def get_features(self): + """Get the radio's features""" + + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = False + rf.has_name = True + rf.has_offset = False + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_name_length = self.LENGTH_NAME + rf.valid_duplexes = ["", "-", "+"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = self.SKIP_VALUES + rf.valid_dtcs_codes = self.DTCS_CODES + rf.memory_bounds = (0, 127) + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_bands = self.VALID_BANDS + rf.valid_tuning_steps = STEPS + + return rf + + + MEM_FORMAT = """ + #seekto 0x0000; + struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1; + u8 unknown2:7, + lowpower:1; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; + } memory[128]; + + #seekto 0x0B00; + struct { + u8 code[5]; + u8 unused[11]; + } pttid[15]; + + #seekto 0x0CAA; + struct { + u8 code[5]; + u8 unused1:6, + aniid:2; + u8 unknown[2]; + u8 dtmfon; + u8 dtmfoff; + } ani; + + #seekto 0x0E20; + struct { + u8 unused01:4, + squelch:4; + u8 unused02; + u8 unused03; + u8 unused04:5, + save:3; + u8 unused05:4, + vox:4; + u8 unused06; + u8 unused07:4, + abr:4; + u8 unused08:7, + tdr:1; + u8 unused09:7, + beep:1; + u8 unused10:2, + timeout:6; + u8 unused11[4]; + u8 unused12:6, + voice:2; + u8 unused13; + u8 unused14:6, + dtmfst:2; + u8 unused15; + u8 unused16:6, + screv:2; + u8 unused17:6, + pttid:2; + u8 unused18:2, + pttlt:6; + u8 unused19:6, + mdfa:2; + u8 unused20:6, + mdfb:2; + u8 unused21; + u8 unused22:7, + sync:1; + u8 unused23[4]; + u8 unused24:6, + wtled:2; + u8 unused25:6, + rxled:2; + u8 unused26:6, + txled:2; + u8 unused27:6, + almod:2; + u8 unused28:7, + dbptt:1; + u8 unused29:6, + tdrab:2; + u8 unused30:7, + ste:1; + u8 unused31:4, + rpste:4; + u8 unused32:4, + rptrl:4; + u8 unused33:7, + ponmsg:1; + u8 unused34:7, + roger:1; + u8 unused35:6, + rtone:2; + u8 unused36; + u8 unused37:6, + rogerrx:2; + u8 unused38; + u8 displayab:1, + unknown1:2, + fmradio:1, + alarm:1, + unknown2:1, + reset:1, + menu:1; + u8 unused39; + u8 workmode; + u8 keylock; + u8 cht; + } settings; + + #seekto 0x0E76; + struct { + u8 unused1:1, + mrcha:7; + u8 unused2:1, + mrchb:7; + } wmchannel; + + struct vfo { + u8 unknown0[8]; + u8 freq[8]; + u8 unknown1; + u8 offset[4]; + u8 unknown2; + ul16 rxtone; + ul16 txtone; + u8 unused1:7, + band:1; + u8 unknown3; + u8 unused2:2, + sftd:2, + scode:4; + u8 unknown4; + u8 unused3:1 + step:3, + unused4:4; + u8 txpower:1, + widenarr:1, + unknown5:4, + txpower3:2; + }; + + #seekto 0x0F00; + struct { + struct vfo a; + struct vfo b; + } vfo; + + #seekto 0x0F4E; + u16 fm_presets; + + #seekto 0x1000; + struct { + char name[7]; + u8 unknown1[9]; + } names[128]; + + #seekto 0x1ED0; + struct { + char line1[7]; + char line2[7]; + } sixpoweron_msg; + + #seekto 0x1EE0; + struct { + char line1[7]; + char line2[7]; + } poweron_msg; + + #seekto 0x1EF0; + struct { + char line1[7]; + char line2[7]; + } firmware_msg; + + struct squelch { + u8 sql0; + u8 sql1; + u8 sql2; + u8 sql3; + u8 sql4; + u8 sql5; + u8 sql6; + u8 sql7; + u8 sql8; + u8 sql9; + }; + + #seekto 0x1F60; + struct { + struct squelch vhf; + u8 unknown1[6]; + u8 unknown2[16]; + struct squelch uhf; + } squelch; + + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The BTech GMRS-V1 driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def validate_memory(self, mem): + msgs = baofeng_common.BaofengCommonHT.validate_memory(self, mem) + + _mem = self._memobj.memory[mem.number] + _msg_freq = 'Memory location cannot change frequency' + _msg_nfm = 'Memory location only supports NFM' + _msg_txp = 'Memory location only supports Low' + + # Original GMRS-V1 models + if str(self._memobj.firmware_msg.line1) in self._is_orig: + # range of memories with values permanently set by FCC rules + if mem.number <= 22: + if mem.freq != int(GMRS_FREQS_ORIG[mem.number] * 1000000): + # warn user can't change frequency + msgs.append(chirp_common.ValidationError(_msg_freq)) + + if mem.number <= 6: + if mem.mode == "FM": + # warn user can't change mode + msgs.append(chirp_common.ValidationError(_msg_nfm)) + + # GMRS-V1 models supporting 2017 GMRS rules + else: + # range of memories with values permanently set by FCC rules + if mem.number >= 1 and mem.number <= 30: + if mem.freq != int(GMRS_FREQS_2017[mem.number - 1] * 1000000): + # warn user can't change frequency + msgs.append(chirp_common.ValidationError(_msg_freq)) + + if mem.number >= 8 and mem.number <= 14: + if mem.mode == "FM": + # warn user can't change mode + msgs.append(chirp_common.ValidationError(_msg_nfm)) + + if str(mem.power) == "High": + # warn user can't change power level + msgs.append(chirp_common.ValidationError(_msg_txp)) + + return msgs + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + # TX freq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset != 0: + if offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + for char in _nam.name: + if str(char) == "\xFF": + char = " " # The OEM software may have 0xFF mid-name + mem.name += str(char) + mem.name = mem.name.rstrip() + + dtcs_pol = ["N", "N"] + + if _mem.txtone in [0, 0xFFFF]: + txmode = "" + elif _mem.txtone >= 0x0258: + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + elif _mem.txtone <= 0x0258: + txmode = "DTCS" + if _mem.txtone > 0x69: + index = _mem.txtone - 0x6A + dtcs_pol[0] = "R" + else: + index = _mem.txtone - 1 + mem.dtcs = self.DTCS_CODES[index] + else: + LOG.warn("Bug: txtone is %04x" % _mem.txtone) + + if _mem.rxtone in [0, 0xFFFF]: + rxmode = "" + elif _mem.rxtone >= 0x0258: + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + elif _mem.rxtone <= 0x0258: + rxmode = "DTCS" + if _mem.rxtone >= 0x6A: + index = _mem.rxtone - 0x6A + dtcs_pol[1] = "R" + else: + index = _mem.rxtone - 1 + mem.rx_dtcs = self.DTCS_CODES[index] + else: + LOG.warn("Bug: rxtone is %04x" % _mem.rxtone) + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + if not _mem.scan: + mem.skip = "S" + + levels = self.POWER_LEVELS + try: + mem.power = levels[_mem.lowpower] + except IndexError: + LOG.error("Radio reported invalid power level %s (in %s)" % + (_mem.power, levels)) + mem.power = levels[0] + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(self.PTTID_LIST, + self.PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("scode", "S-CODE", + RadioSettingValueList(self.SCODE_LIST, + self.SCODE_LIST[_mem.scode])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _nam = self._memobj.names[mem.number] + + if mem.empty: + _mem.set_raw("\xff" * 16) + _nam.set_raw("\xff" * 16) + return + + _mem.set_raw("\x00" * 16) + + _mem.rxfreq = mem.freq / 10 + + if str(self._memobj.firmware_msg.line1) in self._is_orig: + if mem.number > 22: + _mem.txfreq = mem.freq / 10 + else: + if mem.number < 1 or mem.number > 22: + _mem.txfreq = mem.freq / 10 + + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _nam.name[i] = mem.name[i] + except IndexError: + _nam.name[i] = "\xFF" + + rxmode = txmode = "" + if mem.tmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + _mem.rxtone = 0 + elif mem.tmode == "TSQL": + _mem.txtone = int(mem.ctone * 10) + _mem.rxtone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + rxmode = txmode = "DTCS" + _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + _mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1 + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + elif txmode == "DTCS": + _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + else: + _mem.txtone = 0 + if rxmode == "Tone": + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCS": + _mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1 + else: + _mem.rxtone = 0 + else: + _mem.rxtone = 0 + _mem.txtone = 0 + + if txmode == "DTCS" and mem.dtcs_polarity[0] == "R": + _mem.txtone += 0x69 + if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R": + _mem.rxtone += 0x69 + + _mem.scan = mem.skip != "S" + _mem.wide = mem.mode == "FM" + + if mem.power: + _mem.lowpower = self.POWER_LEVELS.index(mem.power) + else: + _mem.lowpower = 0 + + # extra settings + if len(mem.extra) > 0: + # there are setting, parse + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + fm_preset = RadioSettingGroup("fm_preset", "FM Preset") + dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings") + service = RadioSettingGroup("service", "Service Settings") + top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe, + service) + + # Basic settings + if _mem.settings.squelch > 0x09: + val = 0x00 + else: + val = _mem.settings.squelch + rs = RadioSetting("settings.squelch", "Squelch", + RadioSettingValueList( + LIST_OFF1TO9, LIST_OFF1TO9[val])) + basic.append(rs) + + if _mem.settings.save > 0x04: + val = 0x00 + else: + val = _mem.settings.save + rs = RadioSetting("settings.save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, LIST_SAVE[val])) + basic.append(rs) + + if _mem.settings.vox > 0x0A: + val = 0x00 + else: + val = _mem.settings.vox + rs = RadioSetting("settings.vox", "Vox", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + if _mem.settings.abr > 0x0A: + val = 0x00 + else: + val = _mem.settings.abr + rs = RadioSetting("settings.abr", "Backlight Timeout", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + rs = RadioSetting("settings.tdr", "Dual Watch", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(rs) + + rs = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(rs) + + if _mem.settings.timeout > 0x27: + val = 0x03 + else: + val = _mem.settings.timeout + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + LIST_TIMEOUT, LIST_TIMEOUT[val])) + basic.append(rs) + + if _mem.settings.voice > 0x02: + val = 0x01 + else: + val = _mem.settings.voice + rs = RadioSetting("settings.voice", "Voice Prompt", + RadioSettingValueList( + LIST_VOICE, LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("settings.dtmfst", "DTMF Sidetone", + RadioSettingValueList(LIST_DTMFST, LIST_DTMFST[ + _mem.settings.dtmfst])) + basic.append(rs) + + if _mem.settings.screv > 0x02: + val = 0x01 + else: + val = _mem.settings.screv + rs = RadioSetting("settings.screv", "Scan Resume", + RadioSettingValueList( + LIST_RESUME, LIST_RESUME[val])) + basic.append(rs) + + rs = RadioSetting("settings.pttid", "When to send PTT ID", + RadioSettingValueList(LIST_PTTID, LIST_PTTID[ + _mem.settings.pttid])) + basic.append(rs) + + if _mem.settings.pttlt > 0x1E: + val = 0x05 + else: + val = _mem.settings.pttlt + rs = RadioSetting("pttlt", "PTT ID Delay", + RadioSettingValueInteger(0, 50, val)) + basic.append(rs) + + rs = RadioSetting("settings.mdfa", "Display Mode (A)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("settings.mdfb", "Display Mode (B)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("settings.sync", "Sync A & B", + RadioSettingValueBoolean(_mem.settings.sync)) + basic.append(rs) + + rs = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.wtled])) + basic.append(rs) + + rs = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.rxled])) + basic.append(rs) + + rs = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.txled])) + basic.append(rs) + + val = _mem.settings.almod + rs = RadioSetting("settings.almod", "Alarm Mode", + RadioSettingValueList( + LIST_ALMOD, LIST_ALMOD[val])) + basic.append(rs) + + rs = RadioSetting("settings.dbptt", "Double PTT", + RadioSettingValueBoolean(_mem.settings.dbptt)) + basic.append(rs) + + if _mem.settings.tdrab > 0x02: + val = 0x00 + else: + val = _mem.settings.tdrab + rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority", + RadioSettingValueList( + LIST_OFFAB, LIST_OFFAB[val])) + basic.append(rs) + + rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)", + RadioSettingValueBoolean(_mem.settings.ste)) + basic.append(rs) + + if _mem.settings.rpste > 0x0A: + val = 0x00 + else: + val = _mem.settings.rpste + rs = RadioSetting("settings.rpste", + "Squelch Tail Eliminate (repeater)", + RadioSettingValueList( + LIST_RPSTE, LIST_RPSTE[val])) + basic.append(rs) + + if _mem.settings.rptrl > 0x0A: + val = 0x00 + else: + val = _mem.settings.rptrl + rs = RadioSetting("settings.rptrl", "STE Repeater Delay", + RadioSettingValueList( + LIST_STEDELAY, LIST_STEDELAY[val])) + basic.append(rs) + + rs = RadioSetting("settings.ponmsg", "Power-On Message", + RadioSettingValueList(LIST_PONMSG, LIST_PONMSG[ + _mem.settings.ponmsg])) + basic.append(rs) + + rs = RadioSetting("settings.roger", "Roger Beep", + RadioSettingValueBoolean(_mem.settings.roger)) + basic.append(rs) + + rs = RadioSetting("settings.rtone", "Tone Burst Frequency", + RadioSettingValueList(LIST_RTONE, LIST_RTONE[ + _mem.settings.rtone])) + basic.append(rs) + + rs = RadioSetting("settings.rogerrx", "Roger Beep (RX)", + RadioSettingValueList( + LIST_OFFAB, LIST_OFFAB[ + _mem.settings.rogerrx])) + basic.append(rs) + + # Advanced settings + rs = RadioSetting("settings.reset", "RESET Menu", + RadioSettingValueBoolean(_mem.settings.reset)) + advanced.append(rs) + + rs = RadioSetting("settings.menu", "All Menus", + RadioSettingValueBoolean(_mem.settings.menu)) + advanced.append(rs) + + rs = RadioSetting("settings.fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_mem.settings.fmradio)) + advanced.append(rs) + + rs = RadioSetting("settings.alarm", "Alarm Sound", + RadioSettingValueBoolean(_mem.settings.alarm)) + advanced.append(rs) + + # Other settings + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = _mem.firmware_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val) + other.append(rs) + + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val) + other.append(rs) + + _msg = _mem.sixpoweron_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val) + other.append(rs) + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val) + other.append(rs) + + _msg = _mem.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + # Work mode settings + rs = RadioSetting("settings.displayab", "Display", + RadioSettingValueList( + LIST_AB, LIST_AB[_mem.settings.displayab])) + work.append(rs) + + rs = RadioSetting("settings.workmode", "VFO/MR Mode", + RadioSettingValueList( + LIST_WORKMODE, + LIST_WORKMODE[_mem.settings.workmode])) + work.append(rs) + + rs = RadioSetting("settings.keylock", "Keypad Lock", + RadioSettingValueBoolean(_mem.settings.keylock)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrcha)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrchb)) + work.append(rs) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + value = chirp_common.parse_freq(value) + msg = ("Can't be less than %i.0000") + if value > 99000000 and value < 130 * 1000000: + raise InvalidValueError(msg % (130)) + msg = ("Can't be between %i.9975-%i.0000") + if (179 + 1) * 1000000 <= value and value < 400 * 1000000: + raise InvalidValueError(msg % (179, 400)) + msg = ("Can't be greater than %i.9975") + if value > 99000000 and value > (520 + 1) * 1000000: + raise InvalidValueError(msg % (520)) + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.a.freq)) + val1a.set_validate_callback(my_validate) + rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a) + rs.set_apply_callback(apply_freq, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.b.freq)) + val1b.set_validate_callback(my_validate) + rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b) + rs.set_apply_callback(apply_freq, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.step", "VFO A Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.a.step])) + work.append(rs) + rs = RadioSetting("vfo.b.step", "VFO B Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.b.step])) + work.append(rs) + + # broadcast FM settings + _fm_presets = self._memobj.fm_presets + if _fm_presets <= 108.0 * 10 - 650: + preset = _fm_presets / 10.0 + 65 + elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10: + preset = _fm_presets / 10.0 + else: + preset = 76.0 + rs = RadioSetting("fm_presets", "FM Preset(MHz)", + RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)) + fm_preset.append(rs) + + # DTMF settings + def apply_code(setting, obj, length): + code = [] + for j in range(0, length): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + pttid = RadioSetting("pttid/%i.code" % i, + "Signal Code %i" % (i + 1), val) + pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5) + dtmfe.append(pttid) + + if _mem.ani.dtmfon > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + if _mem.ani.dtmfoff > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.code", "ANI Code", val) + rs.set_apply_callback(apply_code, self._memobj.ani, 5) + dtmfe.append(rs) + + rs = RadioSetting("ani.aniid", "When to send ANI ID", + RadioSettingValueList(LIST_PTTID, + LIST_PTTID[_mem.ani.aniid])) + dtmfe.append(rs) + + # Service settings + for band in ["vhf", "uhf"]: + for index in range(0, 10): + key = "squelch.%s.sql%i" % (band, index) + if band == "vhf": + _obj = self._memobj.squelch.vhf + elif band == "uhf": + _obj = self._memobj.squelch.uhf + val = RadioSettingValueInteger(0, 123, + getattr(_obj, "sql%i" % (index))) + if index == 0: + val.set_mutable(False) + name = "%s Squelch %i" % (band.upper(), index) + rs = RadioSetting(key, name, val) + service.append(rs) + + return top + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == 0x2008: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/h777.py b/chirp/drivers/h777.py new file mode 100644 index 0000000..f3353be --- /dev/null +++ b/chirp/drivers/h777.py @@ -0,0 +1,642 @@ +# -*- coding: utf-8 -*- +# Copyright 2013 Andrew Morgan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import unittest +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown3:1, + unknown2:1, + unknown1:1, + skip:1, + highpower:1, + narrow:1, + beatshift:1, + bcl:1; + u8 unknown4[3]; +} memory[16]; +#seekto 0x02B0; +struct { + u8 voiceprompt; + u8 voicelanguage; + u8 scan; + u8 vox; + u8 voxlevel; + u8 voxinhibitonrx; + u8 lowvolinhibittx; + u8 highvolinhibittx; + u8 alarm; + u8 fmradio; +} settings; +#seekto 0x03C0; +struct { + u8 unused:6, + batterysaver:1, + beep:1; + u8 squelchlevel; + u8 sidekeyfunction; + u8 timeouttimer; + u8 unused2[3]; + u8 unused3:7, + scanmode:1; +} settings2; +""" + +CMD_ACK = b"\x06" +BLOCK_SIZE = 0x08 +UPLOAD_BLOCKS = [list(range(0x0000, 0x0110, 8)), + list(range(0x02b0, 0x02c0, 8)), + list(range(0x0380, 0x03e0, 8))] + +# TODO: Is it 1 watt? +H777_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), + chirp_common.PowerLevel("High", watts=5.00)] +VOICE_LIST = ["English", "Chinese"] +TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds", + "120 seconds", "150 seconds", "180 seconds", + "210 seconds", "240 seconds", "270 seconds", + "300 seconds"] +SCANMODE_LIST = ["Carrier", "Time"] + +SETTING_LISTS = { + "voice": VOICE_LIST, +} + + +def _h777_enter_programming_mode(radio): + serial = radio.pipe + + try: + serial.write(b"\x02") + time.sleep(0.1) + serial.write(b"PROGRAM") + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if not ack: + raise errors.RadioError("No response from radio") + elif ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + original_timeout = serial.timeout + try: + serial.write(b"\x02") + # At least one version of the Baofeng BF-888S has a consistent + # ~0.33s delay between sending the first five bytes of the + # version data and the last three bytes. We need to raise the + # timeout so that the read doesn't finish early. + serial.timeout = 0.5 + ident = serial.read(8) + except: + raise errors.RadioError("Error communicating with radio") + finally: + serial.timeout = original_timeout + + if not ident.startswith(b"P3107"): + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + +def _h777_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write(b"E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _h777_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", b'R', block_addr, BLOCK_SIZE) + expectedresponse = b"W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + BLOCK_SIZE) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _h777_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", b'W', block_addr, BLOCK_SIZE) + data = radio.get_mmap().get_byte_compatible()[block_addr:block_addr + 8] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + original_timeout = serial.timeout + try: + serial.write(cmd + data) + # Time required to write data blocks varies between individual + # radios of the Baofeng BF-888S model. The longest seen is + # ~0.31s. + serial.timeout = 0.5 + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + finally: + serial.timeout = original_timeout + + +def do_download(radio): + LOG.debug("download") + _h777_enter_programming_mode(radio) + + data = b"" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + + block = _h777_read_block(radio, addr, BLOCK_SIZE) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _h777_exit_programming_mode(radio) + + return memmap.MemoryMapBytes(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _h777_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + _h777_write_block(radio, addr, BLOCK_SIZE) + + _h777_exit_programming_mode(radio) + + +class ArcshellAR5(chirp_common.Alias): + VENDOR = 'Arcshell' + MODEL = 'AR-5' + + +class ArcshellAR6(chirp_common.Alias): + VENDOR = 'Arcshell' + MODEL = 'AR-6' + + +class GV8SAlias(chirp_common.Alias): + VENDOR = 'Greaval' + MODEL = 'GV-8S' + + +class GV9SAlias(chirp_common.Alias): + VENDOR = 'Greaval' + MODEL = 'GV-9S' + + +class A8SAlias(chirp_common.Alias): + VENDOR = 'Ansoko' + MODEL = 'A-8S' + + +class TenwayTW325Alias(chirp_common.Alias): + VENDOR = 'Tenway' + MODEL = 'TW-325' + + +@directory.register +class H777Radio(chirp_common.CloneModeRadio): + """HST H-777""" + # VENDOR = "Heng Shun Tong (恒顺通)" + # MODEL = "H-777" + VENDOR = "Baofeng" + MODEL = "BF-888" + BAUD_RATE = 9600 + NEEDS_COMPAT_SERIAL = False + + ALIASES = [ArcshellAR5, ArcshellAR6, GV8SAlias, GV9SAlias, A8SAlias, + TenwayTW325Alias] + SIDEKEYFUNCTION_LIST = ["Off", "Monitor", "Transmit Power", "Alarm"] + + # This code currently requires that ranges start at 0x0000 + # and are continious. In the original program 0x0388 and 0x03C8 + # are only written (all bytes 0xFF), not read. + # _ranges = [ + # (0x0000, 0x0110), + # (0x02B0, 0x02C0), + # (0x0380, 0x03E0) + # ] + # Memory starts looping at 0x1000... But not every 0x1000. + + _ranges = [ + (0x0000, 0x0110), + (0x02B0, 0x02C0), + (0x0380, 0x03E0), + ] + _memsize = 0x03E0 + _has_fm = True + _has_sidekey = True + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.can_odd_split = True + rf.has_rx_dtcs = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.has_tuning_step = False + rf.has_bank = False + rf.has_name = False + rf.memory_bounds = (1, 16) + rf.valid_bands = [(400000000, 470000000)] + rf.valid_power_levels = H777_POWER_LEVELS + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, + 50.0, 100.0] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + do_upload(self) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _decode_tone(self, val): + val = int(val) + if val == 16665: + return '', None, None + elif val >= 12000: + return 'DTCS', val - 12000, 'R' + elif val >= 8000: + return 'DTCS', val - 8000, 'N' + else: + return 'Tone', val / 10.0, None + + def _encode_tone(self, memval, mode, value, pol): + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if _mem.txfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = not _mem.narrow and "FM" or "NFM" + mem.power = H777_POWER_LEVELS[_mem.highpower] + + mem.skip = _mem.skip and "S" or "" + + txtone = self._decode_tone(_mem.txtone) + rxtone = self._decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.extra = RadioSettingGroup("Extra", "extra") + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueBoolean(not _mem.bcl)) + mem.extra.append(rs) + rs = RadioSetting("beatshift", "Beat Shift(scramble)", + RadioSettingValueBoolean(not _mem.beatshift)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() // 8)) + return + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + txtone, rxtone = chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.txtone, *txtone) + self._encode_tone(_mem.rxtone, *rxtone) + + _mem.narrow = 'N' in mem.mode + _mem.highpower = mem.power == H777_POWER_LEVELS[1] + _mem.skip = mem.skip == "S" + + for setting in mem.extra: + # NOTE: Only two settings right now, both are inverted + setattr(_mem, setting.get_name(), not int(setting.value)) + + # When set to one, official programming software (BF-480) shows always + # "WFM", even if we choose "NFM". Therefore, for compatibility + # purposes, we will set these to zero. + _mem.unknown1 = 0 + _mem.unknown2 = 0 + _mem.unknown3 = 0 + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + # TODO: Check that all these settings actually do what they + # say they do. + + rs = RadioSetting("voiceprompt", "Voice prompt", + RadioSettingValueBoolean(_settings.voiceprompt)) + basic.append(rs) + + rs = RadioSetting("voicelanguage", "Voice language", + RadioSettingValueList( + VOICE_LIST, + VOICE_LIST[_settings.voicelanguage])) + basic.append(rs) + + rs = RadioSetting("scan", "Scan", + RadioSettingValueBoolean(_settings.scan)) + basic.append(rs) + + rs = RadioSetting("settings2.scanmode", "Scan mode", + RadioSettingValueList( + SCANMODE_LIST, + SCANMODE_LIST[self._memobj.settings2.scanmode])) + basic.append(rs) + + rs = RadioSetting("vox", "VOX", + RadioSettingValueBoolean(_settings.vox)) + basic.append(rs) + + rs = RadioSetting("voxlevel", "VOX level", + RadioSettingValueInteger( + 1, 5, _settings.voxlevel + 1)) + basic.append(rs) + + rs = RadioSetting("voxinhibitonrx", "Inhibit VOX on receive", + RadioSettingValueBoolean(_settings.voxinhibitonrx)) + basic.append(rs) + + rs = RadioSetting("lowvolinhibittx", "Low voltage inhibit transmit", + RadioSettingValueBoolean(_settings.lowvolinhibittx)) + basic.append(rs) + + rs = RadioSetting("highvolinhibittx", "High voltage inhibit transmit", + RadioSettingValueBoolean(_settings.highvolinhibittx)) + basic.append(rs) + + rs = RadioSetting("alarm", "Alarm", + RadioSettingValueBoolean(_settings.alarm)) + basic.append(rs) + + # TODO: This should probably be called “FM Broadcast Band Radio” + # or something. I'm not sure if the model actually has one though. + if self._has_fm: + rs = RadioSetting("fmradio", "FM function", + RadioSettingValueBoolean(_settings.fmradio)) + basic.append(rs) + + rs = RadioSetting("settings2.beep", "Beep", + RadioSettingValueBoolean( + self._memobj.settings2.beep)) + basic.append(rs) + + rs = RadioSetting("settings2.batterysaver", "Battery saver", + RadioSettingValueBoolean( + self._memobj.settings2.batterysaver)) + basic.append(rs) + + rs = RadioSetting("settings2.squelchlevel", "Squelch level", + RadioSettingValueInteger( + 0, 9, self._memobj.settings2.squelchlevel)) + basic.append(rs) + + if self._has_sidekey: + rs = RadioSetting("settings2.sidekeyfunction", "Side key function", + RadioSettingValueList( + self.SIDEKEYFUNCTION_LIST, + self.SIDEKEYFUNCTION_LIST[ + self._memobj.settings2.sidekeyfunction])) + basic.append(rs) + + rs = RadioSetting("settings2.timeouttimer", "Timeout timer", + RadioSettingValueList( + TIMEOUTTIMER_LIST, + TIMEOUTTIMER_LIST[ + self._memobj.settings2.timeouttimer])) + basic.append(rs) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "voxlevel": + setattr(obj, setting, int(element.value) - 1) + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception as e: + LOG.debug(element.get_name()) + raise + + +class H777TestCase(unittest.TestCase): + + def setUp(self): + self.driver = H777Radio(None) + self.testdata = bitwise.parse("lbcd foo[2];", + memmap.MemoryMap("\x00\x00")) + + def test_decode_tone_dtcs_normal(self): + mode, value, pol = self.driver._decode_tone(8023) + self.assertEqual('DTCS', mode) + self.assertEqual(23, value) + self.assertEqual('N', pol) + + def test_decode_tone_dtcs_rev(self): + mode, value, pol = self.driver._decode_tone(12023) + self.assertEqual('DTCS', mode) + self.assertEqual(23, value) + self.assertEqual('R', pol) + + def test_decode_tone_tone(self): + mode, value, pol = self.driver._decode_tone(885) + self.assertEqual('Tone', mode) + self.assertEqual(88.5, value) + self.assertEqual(None, pol) + + def test_decode_tone_none(self): + mode, value, pol = self.driver._decode_tone(16665) + self.assertEqual('', mode) + self.assertEqual(None, value) + self.assertEqual(None, pol) + + def test_encode_tone_dtcs_normal(self): + self.driver._encode_tone(self.testdata.foo, 'DTCS', 23, 'N') + self.assertEqual(8023, int(self.testdata.foo)) + + def test_encode_tone_dtcs_rev(self): + self.driver._encode_tone(self.testdata.foo, 'DTCS', 23, 'R') + self.assertEqual(12023, int(self.testdata.foo)) + + def test_encode_tone(self): + self.driver._encode_tone(self.testdata.foo, 'Tone', 88.5, 'N') + self.assertEqual(885, int(self.testdata.foo)) + + def test_encode_tone_none(self): + self.driver._encode_tone(self.testdata.foo, '', 67.0, 'N') + self.assertEqual(16665, int(self.testdata.foo)) + + +@directory.register +class ROGA2SRadio(H777Radio): + VENDOR = "Radioddity" + MODEL = "GA-2S" + _has_fm = False + SIDEKEYFUNCTION_LIST = ["Off", "Monitor", "Unused", "Alarm"] + + @classmethod + def match_model(cls, filedata, filename): + # This model is only ever matched via metadata + return False diff --git a/chirp/drivers/hobbypcb.py b/chirp/drivers/hobbypcb.py new file mode 100644 index 0000000..7d7779b --- /dev/null +++ b/chirp/drivers/hobbypcb.py @@ -0,0 +1,327 @@ +# Copyright 2016 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import time + +from chirp import chirp_common, directory, memmap, errors +from chirp import bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + + +LOG = logging.getLogger(__name__) +BAUDS = [1200, 4800, 9600, 19200, 38400, 57600] +POWER_LEVELS = [chirp_common.PowerLevel('Low', dBm=10), + chirp_common.PowerLevel('High', dBm=24)] +TONE_MODES = ['', 'Tone', 'TSQL', ''] + + +def detect_baudrate(radio): + bauds = list(BAUDS) + bauds.remove(radio.pipe.getBaudrate()) + bauds.insert(0, radio.pipe.getBaudrate()) + for baud in bauds: + radio.pipe.setBaudrate(baud) + radio.pipe.setTimeout(0.5) + radio.pipe.write('\rFW?\r') + resp = radio.pipe.read(2) + if resp.strip().startswith('FW'): + resp += radio.pipe.read(16) + LOG.info('HobbyPCB %s at baud rate %i' % (resp.strip(), baud)) + return baud + + +@directory.register +class HobbyPCBRSUV3Radio(chirp_common.LiveRadio): + """HobbyPCB RS-UV3""" + VENDOR = "HobbyPCB" + MODEL = "RS-UV3" + BAUD_RATE = 19200 + + def __init__(self, *args, **kwargs): + super(HobbyPCBRSUV3Radio, self).__init__(*args, **kwargs) + if self.pipe: + baud = detect_baudrate(self) + if not baud: + errors.RadioError('Radio did not respond') + + def _cmd(self, command, rsize=None): + LOG.debug('> %s' % command) + self.pipe.write('%s\r' % command) + resp = '' + + if rsize is None: + complete = lambda: False + elif rsize == 0: + rsize = 1 + complete = lambda: resp.endswith('\r') + else: + complete = lambda: len(resp) >= rsize + + while not complete(): + chunk = self.pipe.read(rsize) + if not chunk: + break + resp += chunk + LOG.debug('< %r [%i]' % (resp, len(resp))) + return resp.strip() + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_name = False + rf.has_cross = False + rf.has_dtcs = False + rf.has_rx_dtcs = False + rf.has_dtcs_polarity = False + rf.has_tuning_step = False + rf.has_mode = False + rf.has_settings = True + rf.memory_bounds = (1, 9) # This radio supports memories 0-9 + rf.valid_bands = [(144000000, 148000000), + (220000000, 222000000), + (440000000, 450000000), + ] + rf.valid_tmodes = TONE_MODES + rf.valid_power_levels = POWER_LEVELS + return rf + + def get_memory(self, number): + _mem = self._cmd('CP%i' % number, 33).split('\r') + LOG.debug('Memory elements: %s' % _mem) + mem = chirp_common.Memory() + mem.number = number + mem.freq = int(_mem[0]) * 1000 + txfreq = int(_mem[1]) * 1000 + mem.offset = abs(txfreq - mem.freq) + if mem.freq < txfreq: + mem.duplex = '+' + elif mem.freq > txfreq: + mem.duplex = '-' + else: + mem.duplex = '' + mem.ctone = int(_mem[2]) / 100.0 + mem.rtone = mem.ctone + mem.tmode = TONE_MODES[int(_mem[3])] + mem.power = POWER_LEVELS[int(_mem[5])] + return mem + + def set_memory(self, mem): + if mem.tmode in ['', 'Tone']: + tone = mem.rtone * 100 + else: + tone = mem.ctone * 100 + if mem.duplex == '+': + self._cmd('FT%06i' % ((mem.freq + mem.offset) / 1000)) + self._cmd('FR%06i' % (mem.freq / 1000)) + elif mem.duplex == '-': + self._cmd('FT%06i' % ((mem.freq - mem.offset) / 1000)) + self._cmd('FR%06i' % (mem.freq / 1000)) + else: + self._cmd('FS%06i' % (mem.freq / 1000)) + self._cmd('TM%i' % TONE_MODES.index(mem.tmode)) + self._cmd('TF%05i' % tone) + self._cmd('PW%i' % POWER_LEVELS.index(mem.power)) + time.sleep(1) + self._cmd('ST%i' % mem.number) + + def get_settings(self): + def _get(cmd): + return self._cmd('%s?' % cmd, 0).split(':')[1].strip() + + cw = RadioSettingGroup('beacon', 'Beacon Settings') + cl = RadioSetting('CL%15s', 'CW Callsign', + RadioSettingValueString(0, 15, + _get('CL'))) + cw.append(cl) + + cf = RadioSetting('CF%4i', 'CW Audio Frequency', + RadioSettingValueInteger(400, 1300, + int(_get('CF')))) + cw.append(cf) + + cs = RadioSetting('CS%02i', 'CW Speed', + RadioSettingValueInteger(5, 25, + int(_get('CS')))) + cw.append(cs) + + bc = RadioSetting('BC%03i', 'CW Beacon Timer', + RadioSettingValueInteger(0, 600, + int(_get('BC')))) + cw.append(bc) + + bm = RadioSetting('BM%15s', 'Beacon Message', + RadioSettingValueString(0, 15, + _get('BM'))) + cw.append(bm) + + bt = RadioSetting('BT%03i', 'Beacon Timer', + RadioSettingValueInteger(0, 600, + int(_get('BT')))) + cw.append(bt) + + it = RadioSetting('IT%03i', 'CW ID Timer', + RadioSettingValueInteger(0, 500, + int(_get('IT')))) + cw.append(it) + + tg = RadioSetting('TG%7s', 'CW Timeout Message', + RadioSettingValueString(0, 7, + _get('TG'))) + cw.append(tg) + + io = RadioSettingGroup('io', 'IO') + + af = RadioSetting('AF%i', 'Arduino LPF', + RadioSettingValueBoolean(_get('AF') == 'ON')) + io.append(af) + + input_pin = ['OFF', 'SQ OPEN', 'PTT'] + ai = RadioSetting('AI%i', 'Arduino Input Pin', + RadioSettingValueList( + input_pin, + input_pin[int(_get('AI'))])) + io.append(ai) + + output_pin = ['LOW', 'SQ OPEN', 'DTMF DETECT', 'TX ON', 'CTCSS DET', + 'HIGH'] + ao = RadioSetting('AO%i', 'Arduino Output Pin', + RadioSettingValueList( + output_pin, + output_pin[int(_get('AO'))])) + io.append(ao) + + bauds = [str(x) for x in BAUDS] + b1 = RadioSetting('B1%i', 'Arduino Baudrate', + RadioSettingValueList( + bauds, + bauds[int(_get('B1'))])) + io.append(b1) + + b2 = RadioSetting('B2%i', 'Main Baudrate', + RadioSettingValueList( + bauds, + bauds[int(_get('B2'))])) + io.append(b2) + + dtmf = RadioSettingGroup('dtmf', 'DTMF Settings') + + dd = RadioSetting('DD%04i', 'DTMF Tone Duration', + RadioSettingValueInteger(50, 2000, + int(_get('DD')))) + dtmf.append(dd) + + dr = RadioSetting('DR%i', 'DTMF Tone Detector', + RadioSettingValueBoolean(_get('DR') == 'ON')) + dtmf.append(dr) + + gt = RadioSetting('GT%02i', 'DTMF/CW Tone Gain', + RadioSettingValueInteger(0, 15, + int(_get('GT')))) + dtmf.append(gt) + + sd = RadioSetting('SD%i', 'DTMF/CW Side Tone', + RadioSettingValueBoolean(_get('SD') == 'ON')) + dtmf.append(sd) + + general = RadioSettingGroup('general', 'General') + + dp = RadioSetting('DP%i', 'Pre-Emphasis', + RadioSettingValueBoolean(_get('DP') == 'ON')) + general.append(dp) + + fw = RadioSetting('_fw', 'Firmware Version', + RadioSettingValueString(0, 20, + _get('FW'))) + general.append(fw) + + gm = RadioSetting('GM%02i', 'Mic Gain', + RadioSettingValueInteger(0, 15, + int(_get('GM')))) + general.append(gm) + + hp = RadioSetting('HP%i', 'Audio High-Pass Filter', + RadioSettingValueBoolean(_get('HP') == 'ON')) + general.append(hp) + + ht = RadioSetting('HT%04i', 'Hang Time', + RadioSettingValueInteger(0, 5000, + int(_get('HT')))) + general.append(ht) + + ledmode = ['OFF', 'ON', 'SQ OPEN', 'BATT CHG STAT'] + ld = RadioSetting('LD%i', 'LED Mode', + RadioSettingValueList( + ledmode, + ledmode[int(_get('LD'))])) + general.append(ld) + + sq = RadioSetting('SQ%i', 'Squelch Level', + RadioSettingValueInteger(0, 9, + int(_get('SQ')))) + general.append(sq) + + to = RadioSetting('TO%03i', 'Timeout Timer', + RadioSettingValueInteger(0, 600, + int(_get('TO')))) + general.append(to) + + vu = RadioSetting('VU%02i', 'Receiver Audio Volume', + RadioSettingValueInteger(0, 39, + int(_get('VU')))) + general.append(vu) + + rc = RadioSetting('RC%i', 'Current Channel', + RadioSettingValueInteger(0, 9, 0)) + rc.set_doc('Choosing one of these values causes the radio ' + 'to change to the selected channel. The radio ' + 'cannot tell CHIRP what channel is selected.') + general.append(rc) + + return RadioSettings(general, cw, io, dtmf) + + def set_settings(self, settings): + def _set(thing): + # Try to only set something if it's new + query = '%s?' % thing[:2] + cur = self._cmd(query, 0) + if cur.strip(): + cur = cur.split()[1].strip() + new = thing[2:].strip() + if cur in ['ON', 'OFF']: + cur = int(cur == 'ON') + new = int(new) + elif cur.isdigit(): + cur = int(cur) + new = int(new) + if new != cur: + LOG.info('Setting %s (%r != %r)' % (thing, cur, new)) + self._cmd(thing) + time.sleep(1) + + for group in settings: + for setting in group: + if setting.get_name().startswith('_'): + LOG.debug('Skipping %s' % setting) + continue + cmd = setting.get_name() + value = setting.value.get_value() + if hasattr(setting.value, '_options'): + value = setting.value._options.index(value) + fullcmd = (cmd % value).strip() + _set(fullcmd) diff --git a/chirp/drivers/ic208.py b/chirp/drivers/ic208.py new file mode 100644 index 0000000..e972503 --- /dev/null +++ b/chirp/drivers/ic208.py @@ -0,0 +1,263 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, errors, directory, bitwise + +MEM_FORMAT = """ +struct memory { + u24 freq; + u16 offset; + u8 power:2, + rtone:6; + u8 duplex:2, + ctone:6; + u8 unknown1:1, + dtcs:7; + u8 tuning_step:4, + unknown2:4; + u8 unknown3; + u8 alt_mult:1, + unknown4:1, + is_fm:1, + is_wide:1, + unknown5:2, + tmode:2; + u16 dtcs_polarity:2, + usealpha:1, + empty:1, + name1:6, + name2:6; + u24 name3:6, + name4:6, + name5:6, + name6:6; +}; + +struct memory memory[510]; + +struct { + u8 unknown1:1, + empty:1, + pskip:1, + skip:1, + bank:4; +} flags[512]; + +struct memory call[2]; + +""" + +MODES = ["AM", "FM", "NFM", "NAM"] +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "", "-", "+"] +DTCS_POL = ["NN", "NR", "RN", "RR"] +STEPS = [5.0, 10.0, 12.5, 15, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0] +POWER = [chirp_common.PowerLevel("High", watts=50), + chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=15), + ] + +IC208_SPECIAL = [] +for i in range(1, 6): + IC208_SPECIAL.append("%iA" % i) + IC208_SPECIAL.append("%iB" % i) + +CHARSET = dict(list(zip([0x00, 0x08, 0x09, 0x0a, 0x0b, 0x0d, 0x0f], + " ()*+-/")) + + list(zip(list(range(0x10, 0x1a)), "0123456789")) + + [(0x1c, '|'), (0x1d, '=')] + + list(zip(list(range(0x21, 0x3b)), + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) +CHARSET_REV = dict(list(zip(list(CHARSET.values()), list(CHARSET.keys())))) + + +def get_name(_mem): + """Decode the name from @_mem""" + def _get_char(val): + try: + return CHARSET[int(val)] + except KeyError: + return "*" + + name_bytes = [_mem.name1, _mem.name2, _mem.name3, + _mem.name4, _mem.name5, _mem.name6] + name = "" + for val in name_bytes: + name += _get_char(val) + + return name.rstrip() + + +def set_name(_mem, name): + """Encode @name in @_mem""" + def _get_index(char): + try: + return CHARSET_REV[char] + except KeyError: + return CHARSET_REV["*"] + + name = name.ljust(6)[:6] + + _mem.usealpha = bool(name.strip()) + + # The element override calling convention makes this harder to automate. + # It's just six, so do it manually + _mem.name1 = _get_index(name[0]) + _mem.name2 = _get_index(name[1]) + _mem.name3 = _get_index(name[2]) + _mem.name4 = _get_index(name[3]) + _mem.name5 = _get_index(name[4]) + _mem.name6 = _get_index(name[5]) + + +@directory.register +class IC208Radio(icf.IcomCloneModeRadio): + """Icom IC800""" + VENDOR = "Icom" + MODEL = "IC-208H" + + _model = "\x26\x32\x00\x01" + _memsize = 0x2600 + _endframe = "Icom Inc\x2e30" + _can_hispeed = True + + _memories = [] + + _ranges = [(0x0000, 0x2600, 32)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 500) + rf.has_bank = True + rf.valid_tuning_steps = list(STEPS) + rf.valid_tmodes = list(TMODES) + rf.valid_modes = list(MODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_power_levels = list(POWER) + rf.valid_skips = ["", "S", "P"] + rf.valid_bands = [(118000000, 174000000), + (230000000, 550000000), + (810000000, 999995000)] + rf.valid_special_chans = ["C1", "C2"] + sorted(IC208_SPECIAL) + rf.valid_characters = "".join(list(CHARSET.values())) + return rf + + def get_raw_memory(self, number): + _mem, _flg, index = self._get_memory(number) + return repr(_mem) + repr(_flg) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def _get_bank(self, loc): + _flg = self._memobj.flags[loc-1] + if _flg.bank >= 0x0A: + return None + else: + return _flg.bank + + def _set_bank(self, loc, bank): + _flg = self._memobj.flags[loc-1] + if bank is None: + _flg.bank = 0x0A + else: + _flg.bank = bank + + def _get_memory(self, number): + if isinstance(number, str): + if "A" in number or "B" in number: + index = 501 + IC208_SPECIAL.index(number) + _mem = self._memobj.memory[index - 1] + _flg = self._memobj.flags[index - 1] + else: + index = int(number[1]) - 1 + _mem = self._memobj.call[index] + _flg = self._memobj.flags[510 + index] + index = index + -10 + elif number <= 0: + index = 10 - abs(number) + _mem = self._memobj.call[index] + _flg = self._memobj.flags[index + 510] + else: + index = number + _mem = self._memobj.memory[number - 1] + _flg = self._memobj.flags[number - 1] + + return _mem, _flg, index + + def get_memory(self, number): + _mem, _flg, index = self._get_memory(number) + + mem = chirp_common.Memory() + mem.number = index + if isinstance(number, str): + mem.extd_number = number + else: + mem.skip = _flg.pskip and "P" or _flg.skip and "S" or "" + + if _flg.empty: + mem.empty = True + return mem + + mult = _mem.alt_mult and 6250 or 5000 + mem.freq = int(_mem.freq) * mult + mem.offset = int(_mem.offset) * 5000 + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity] + mem.duplex = DUPLEX[_mem.duplex] + mem.tmode = TMODES[_mem.tmode] + mem.mode = ((not _mem.is_wide and "N" or "") + + (_mem.is_fm and "FM" or "AM")) + mem.tuning_step = STEPS[_mem.tuning_step] + mem.name = get_name(_mem) + mem.power = POWER[_mem.power] + + return mem + + def set_memory(self, mem): + _mem, _flg, index = self._get_memory(mem.number) + + if mem.empty: + _flg.empty = True + self._set_bank(mem.number, None) + return + + if _flg.empty: + _mem.set_raw("\x00" * 16) + _flg.empty = False + + _mem.alt_mult = chirp_common.is_fractional_step(mem.freq) + _mem.freq = mem.freq / (_mem.alt_mult and 6250 or 5000) + _mem.offset = mem.offset / 5000 + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.tmode = TMODES.index(mem.tmode) + _mem.is_fm = "FM" in mem.mode + _mem.is_wide = mem.mode[0] != "N" + _mem.tuning_step = STEPS.index(mem.tuning_step) + set_name(_mem, mem.name) + try: + _mem.power = POWER.index(mem.power) + except Exception: + pass + if not isinstance(mem.number, str): + _flg.skip = mem.skip == "S" + _flg.pskip = mem.skip == "P" diff --git a/chirp/drivers/ic2100.py b/chirp/drivers/ic2100.py new file mode 100644 index 0000000..7525ebf --- /dev/null +++ b/chirp/drivers/ic2100.py @@ -0,0 +1,279 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, util, directory, bitwise, memmap +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings + +MEM_FORMAT = """ +struct { + bbcd freq[2]; + u8 freq_10khz:4, + freq_1khz:3, + zero:1; + u8 unknown1; + bbcd offset[2]; + u8 is_12_5:1, + unknownbit1:1, + anm:1, + unknownbit2:1, + duplex:2, + tmode:2; + u8 ctone; + u8 rtone; + char name[6]; + u8 unknown3; +} memory[100]; + +#seekto 0x0640; +struct { + bbcd freq[2]; + u8 freq_10khz:4, + freq_1khz:3, + zero:1; + u8 unknown1; + bbcd offset[2]; + u8 is_12_5:1, + unknownbit1:1, + anm:1, + unknownbit2:1, + duplex:2, + tmode:2; + u8 ctone; + u8 rtone; +} special[7]; + +#seekto 0x0680; +struct { + bbcd freq[2]; + u8 freq_10khz:4, + freq_1khz:3, + zero:1; + u8 unknown1; + bbcd offset[2]; + u8 is_12_5:1, + unknownbit1:1, + anm:1, + unknownbit2:1, + duplex:2, + tmode:2; + u8 ctone; + u8 rtone; +} call[2]; + +#seekto 0x06F0; +struct { + u8 flagbits; +} skipflags[14]; + +#seekto 0x0700; +struct { + u8 flagbits; +} usedflags[14]; + +""" + +TMODES = ["", "Tone", "", "TSQL"] +DUPLEX = ["", "", "+", "-"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0] + + +def _get_special(): + special = {"C": 506} + for i in range(0, 3): + ida = "%iA" % (i + 1) + idb = "%iB" % (i + 1) + num = 500 + (i * 2) + special[ida] = num + special[idb] = num + 1 + + return special + + +def _get_freq(mem): + freq = (int(mem.freq) * 100000) + \ + (mem.freq_10khz * 10000) + \ + (mem.freq_1khz * 1000) + + if mem.is_12_5: + if chirp_common.is_12_5(freq): + pass + elif mem.freq_1khz == 2: + freq += 500 + elif mem.freq_1khz == 5: + freq += 2500 + elif mem.freq_1khz == 7: + freq += 500 + else: + raise Exception("Unable to resolve 12.5kHz: %i" % freq) + + return freq + + +def _set_freq(mem, freq): + mem.freq = freq / 100000 + mem.freq_10khz = (freq / 10000) % 10 + khz = (freq / 1000) % 10 + mem.freq_1khz = khz + mem.is_12_5 = chirp_common.is_12_5(freq) + + +def _get_offset(mem): + raw = memmap.MemoryMap(mem.get_raw()) + if ord(raw[5]) & 0x0A: + raw[5] = ord(raw[5]) & 0xF0 + mem.set_raw(raw.get_packed()) + offset = int(mem.offset) * 1000 + 5000 + raw[5] = ord(raw[5]) | 0x0A + mem.set_raw(raw.get_packed()) + return offset + else: + return int(mem.offset) * 1000 + + +def _set_offset(mem, offset): + if (offset % 10) == 5000: + extra = 0x0A + offset -= 5000 + else: + extra = 0x00 + + mem.offset = offset / 1000 + raw = memmap.MemoryMap(mem.get_raw()) + raw[5] = ord(raw[5]) | extra + mem.set_raw(raw.get_packed()) + + +def _wipe_memory(mem, char): + mem.set_raw(char * (mem.size() / 8)) + + +@directory.register +class IC2100Radio(icf.IcomCloneModeRadio): + """Icom IC-2100""" + VENDOR = "Icom" + MODEL = "IC-2100H" + + _model = "\x20\x88\x00\x01" + _memsize = 2016 + _endframe = "Icom Inc\x2e" + + _ranges = [(0x0000, 0x07E0, 32)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 100) + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_tuning_step = False + rf.has_mode = False + rf.valid_modes = ["FM"] + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(118000000, 174000000)] + rf.valid_skips = ["", "S"] + rf.valid_special_chans = sorted(_get_special().keys()) + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_memory(self, number): + mem = chirp_common.Memory() + + if isinstance(number, str): + if number == "C": + number = _get_special()[number] + _mem = self._memobj.call[0] + else: + number = _get_special()[number] + _mem = self._memobj.special[number - 500] + empty = False + else: + number -= 1 + _mem = self._memobj.memory[number] + _emt = self._memobj.usedflags[number / 8].flagbits + empty = (1 << (number % 8)) & int(_emt) + if not empty: + mem.name = str(_mem.name).rstrip() + _skp = self._memobj.skipflags[number / 8].flagbits + isskip = (1 << (number % 8)) & int(_skp) + + mem.number = number + 1 + + if number <= 100: + mem.skip = isskip and "S" or "" + else: + mem.extd_number = util.get_dict_rev(_get_special(), number) + mem.immutable = ["number", "skip", "extd_number"] + + if empty: + mem.empty = True + return mem + + mem.freq = _get_freq(_mem) + mem.offset = _get_offset(_mem) + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("anm", "Alphanumeric Name", + RadioSettingValueBoolean(_mem.anm)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + if mem.number == "C": + _mem = self._memobj.call[0] + elif isinstance(mem.number, str): + _mem = self._memobj.special[_get_special[mem.number] - 500] + else: + number = mem.number - 1 + _mem = self._memobj.memory[number] + _emt = self._memobj.usedflags[number / 8].flagbits + mask = 1 << (number % 8) + if mem.empty: + _emt |= mask + else: + _emt &= ~mask + _skp = self._memobj.skipflags[number / 8].flagbits + if mem.skip == "S": + _skp |= mask + else: + _skp &= ~mask + _mem.name = mem.name.ljust(6) + _mem.anm = mem.name.strip() != "" + + _set_freq(_mem, mem.freq) + _set_offset(_mem, mem.offset) + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) diff --git a/chirp/drivers/ic2200.py b/chirp/drivers/ic2200.py new file mode 100644 index 0000000..3d42148 --- /dev/null +++ b/chirp/drivers/ic2200.py @@ -0,0 +1,295 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, util, directory, bitwise + +MEM_FORMAT = """ +struct { + ul16 freq; + ul16 offset; + char name[6]; + u8 unknown1:2, + rtone:6; + u8 unknown2:2, + ctone:6; + u8 unknown3:1, + dtcs:7; + u8 unknown4:4, + tuning_step:4; + u8 unknown5[3]; + u8 unknown6:4, + urcall:4; + u8 r1call:4, + r2call:4; + u8 unknown7:1, + digital_code:7; + u8 is_625:1, + unknown8:1, + mode_am:1, + mode_narrow:1, + power:2, + tmode:2; + u8 dtcs_polarity:2, + duplex:2, + unknown10:4; + u8 unknown11; + u8 mode_dv:1, + unknown12:7; +} memory[207]; + +#seekto 0x1370; +struct { + u8 unknown:2, + empty:1, + skip:1, + bank:4; +} flags[207]; + +#seekto 0x15F0; +struct { + char call[8]; +} mycalls[6]; + +struct { + char call[8]; +} urcalls[6]; + +struct { + char call[8]; +} rptcalls[6]; + +""" + +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+"] +DTCSP = ["NN", "NR", "RN", "RR"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0] + +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=65), + chirp_common.PowerLevel("Mid", watts=25), + chirp_common.PowerLevel("MidLow", watts=10), + chirp_common.PowerLevel("Low", watts=5)] + + +def _get_special(): + special = {"C": 206} + for i in range(0, 3): + ida = "%iA" % (i+1) + idb = "%iB" % (i+1) + num = 200 + i * 2 + special[ida] = num + special[idb] = num + 1 + + return special + + +def _wipe_memory(mem, char): + mem.set_raw(char * (mem.size() // 8)) + + +@directory.register +class IC2200Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport): + """Icom IC-2200""" + VENDOR = "Icom" + MODEL = "IC-2200H" + + _model = "\x26\x98\x00\x01" + _memsize = 6848 + _endframe = "Icom Inc\x2eD8" + _can_hispeed = True + + _memories = [] + + _ranges = [(0x0000, 0x1340, 32), + (0x1340, 0x1360, 16), + (0x1360, 0x136B, 8), + + (0x1370, 0x1380, 16), + (0x1380, 0x15E0, 32), + (0x15E0, 0x1600, 16), + (0x1600, 0x1640, 32), + (0x1640, 0x1660, 16), + (0x1660, 0x1680, 32), + + (0x16E0, 0x1860, 32), + + (0x1880, 0x1AB0, 32), + + (0x1AB8, 0x1AC0, 8), + ] + + MYCALL_LIMIT = (0, 6) + URCALL_LIMIT = (0, 6) + RPTCALL_LIMIT = (0, 6) + + def _get_bank(self, loc): + _flag = self._memobj.flags[loc] + if _flag.bank == 0x0A: + return None + else: + return _flag.bank + + def _set_bank(self, loc, bank): + _flag = self._memobj.flags[loc] + if bank is None: + _flag.bank = 0x0A + else: + _flag.bank = bank + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 199) + rf.valid_modes = ["FM", "NFM", "AM", "NAM", "DV"] + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(118000000, 174000000)] + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_special_chans = sorted(_get_special().keys()) + rf.has_settings = True + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_memory(self, number): + if isinstance(number, str): + number = _get_special()[number] + + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number] + + if _mem.mode_dv and not _flag.empty: + mem = chirp_common.DVMemory() + mem.dv_urcall = \ + str(self._memobj.urcalls[_mem.urcall].call).rstrip() + mem.dv_rpt1call = \ + str(self._memobj.rptcalls[_mem.r1call].call).rstrip() + mem.dv_rpt2call = \ + str(self._memobj.rptcalls[_mem.r2call].call).rstrip() + else: + mem = chirp_common.Memory() + + mem.number = number + if number < 200: + mem.skip = _flag.skip and "S" or "" + else: + mem.extd_number = util.get_dict_rev(_get_special(), number) + mem.immutable = ["number", "skip", "bank", "bank_index", + "extd_number"] + + if _flag.empty: + mem.empty = True + mem.power = POWER_LEVELS[0] + return mem + + mult = _mem.is_625 and 6250 or 5000 + mem.freq = (_mem.freq * mult) + mem.offset = (_mem.offset * mult) + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = _mem.mode_dv and "DV" or _mem.mode_am and "AM" or "FM" + if _mem.mode_narrow: + mem.mode = "N%s" % mem.mode + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity] + mem.tuning_step = STEPS[_mem.tuning_step] + mem.name = str(_mem.name).replace("\x0E", "").rstrip() + mem.power = POWER_LEVELS[_mem.power] + + return mem + + def get_memories(self, lo=0, hi=199): + + return [m for m in self._memories if m.number >= lo and m.number <= hi] + + def set_memory(self, mem): + if isinstance(mem.number, str): + number = _get_special()[mem.number] + else: + number = mem.number + + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number] + + was_empty = int(_flag.empty) + + _flag.empty = mem.empty + if mem.empty: + _wipe_memory(_mem, "\xFF") + return + + if was_empty: + _wipe_memory(_mem, "\x00") + + _mem.unknown8 = 0 + _mem.is_625 = chirp_common.is_fractional_step(mem.freq) + mult = _mem.is_625 and 6250 or 5000 + _mem.freq = mem.freq / mult + _mem.offset = mem.offset / mult + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.mode_dv = mem.mode == "DV" + _mem.mode_am = mem.mode.endswith("AM") + _mem.mode_narrow = mem.mode.startswith("N") + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity) + _mem.tuning_step = STEPS.index(mem.tuning_step) + _mem.name = mem.name.ljust(6) + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + if number < 200: + _flag.skip = mem.skip != "" + + if isinstance(mem, chirp_common.DVMemory): + urcalls = self.get_urcall_list() + rptcalls = self.get_repeater_call_list() + _mem.urcall = urcalls.index(mem.dv_urcall) + _mem.r1call = rptcalls.index(mem.dv_rpt1call) + _mem.r2call = rptcalls.index(mem.dv_rpt2call) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_urcall_list(self): + return [str(x.call).rstrip() for x in self._memobj.urcalls] + + def get_repeater_call_list(self): + return [str(x.call).rstrip() for x in self._memobj.rptcalls] + + def get_mycall_list(self): + return [str(x.call).rstrip() for x in self._memobj.mycalls] + + def set_urcall_list(self, calls): + for i in range(*self.URCALL_LIMIT): + self._memobj.urcalls[i].call = calls[i].ljust(8) + + def set_repeater_call_list(self, calls): + for i in range(*self.RPTCALL_LIMIT): + self._memobj.rptcalls[i].call = calls[i].ljust(8) + + def set_mycall_list(self, calls): + for i in range(*self.MYCALL_LIMIT): + self._memobj.mycalls[i].call = calls[i].ljust(8) diff --git a/chirp/drivers/ic2300.py b/chirp/drivers/ic2300.py new file mode 100644 index 0000000..b563e60 --- /dev/null +++ b/chirp/drivers/ic2300.py @@ -0,0 +1,391 @@ +# Copyright 2017 Windsor Schmidt +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, directory, bitwise +from chirp.drivers import icf +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList, RadioSettingValueBoolean, RadioSettings + +# The Icom IC-2300H is a 65W, 144MHz mobile transceiver based on the IC-2200H. +# Unlike the IC-2200H, this model does not accept Icom's UT-118 D-STAR board. +# +# A simple USB interface based on a typical FT232RL breakout board was used +# during development of this module. A schematic diagram is as follows: +# +# +# 3.5mm plug from IC-2300H +# sleeve / ring / tip +# --.______________ +# | | | \ +# |______|___|___/ FT232RL breakout +# --' | | .------------------. +# | +--------| RXD | +# | | D1 | | +# | +--|>|---| TXD | USB/PC +# | | R1 | |--------> +# | +--[_]---| VCC (5V) | +# | | | +# +------------| GND | +# `------------------' +# +# D1: 1N4148 shottky diode +# R1: 10K ohm resistor + +MEM_FORMAT = """ +#seekto 0x0000; // channel memories +struct { + ul16 frequency; + ul16 offset; + char name[6]; + u8 repeater_tone; + u8 ctcss_tone; + u8 dtcs_code; + u8 tuning_step:4, + tone_mode:4; + u8 unknown1:3, + mode_narrow:1, + power:2, + unknown2:2; + u8 dtcs_polarity:2, + duplex:2, + unknown3:1, + reverse_duplex:1, + unknown4:1, + display_style:1; +} memory[200]; +#seekto 0x1340; // channel memory flags +struct { + u8 unknown5:2, + empty:1, + skip:1, + bank:4; +} flags[200]; +#seekto 0x1660; // power-on and regular set menu items +struct { + u8 key_beep; + u8 tx_timeout; + u8 auto_repeater; + u8 auto_power_off; + u8 repeater_lockout; + u8 squelch_delay; + u8 squelch_type; + u8 dtmf_speed; + u8 display_type; + u8 unknown6; + u8 tone_burst; + u8 voltage_display; + u8 unknown7; + u8 display_brightness; + u8 display_color; + u8 auto_dimmer; + u8 display_contrast; + u8 scan_pause_timer; + u8 mic_gain; + u8 scan_resume_timer; + u8 weather_alert; + u8 bank_link_enable; + u8 bank_link[10]; +} settings; +""" + +TUNING_STEPS = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0] +TONE_MODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+"] +DTCSP = ["NN", "NR", "RN", "RR"] +DTCS_POLARITY = ["NN", "NR", "RN", "RR"] + +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=65), + chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("MidLow", watts=10), + chirp_common.PowerLevel("Mid", watts=25)] + + +def _wipe_memory(mem, char): + mem.set_raw(char * (mem.size() // 8)) + + +@directory.register +class IC2300Radio(icf.IcomCloneModeRadio): + """Icom IC-2300""" + VENDOR = "Icom" + MODEL = "IC-2300H" + + _model = "\x32\x51\x00\x01" + _memsize = 6304 + _endframe = "Icom Inc.C5\xfd" + _can_hispeed = True + _ranges = [(0x0000, 0x18a0, 32)] # upload entire memory for now + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 199) + rf.valid_modes = ["FM", "NFM"] + rf.valid_tmodes = list(TONE_MODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(TUNING_STEPS) + rf.valid_bands = [(136000000, 174000000)] # USA tx range: 144-148MHz + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS + rf.has_settings = True + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def _get_bank(self, loc): + _flag = self._memobj.flags[loc] + if _flag.bank == 0x0a: + return None + else: + return _flag.bank + + def _set_bank(self, loc, bank): + _flag = self._memobj.flags[loc] + if bank is None: + _flag.bank = 0x0a + else: + _flag.bank = bank + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number] + mem = chirp_common.Memory() + mem.number = number + if _flag.empty: + mem.empty = True + return mem + mult = int(TUNING_STEPS[_mem.tuning_step] * 1000) + mem.freq = (_mem.frequency * mult) + mem.offset = (_mem.offset * mult) + mem.name = str(_mem.name).rstrip() + mem.rtone = chirp_common.TONES[_mem.repeater_tone] + mem.ctone = chirp_common.TONES[_mem.ctcss_tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs_code] + mem.tuning_step = TUNING_STEPS[_mem.tuning_step] + mem.tmode = TONE_MODES[_mem.tone_mode] + mem.mode = "NFM" if _mem.mode_narrow else "FM" + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity] + mem.duplex = DUPLEX[_mem.duplex] + mem.skip = "S" if _flag.skip else "" + mem.power = POWER_LEVELS[_mem.power] + + # Reverse duplex + mem.extra = RadioSettingGroup("extra", "Extra") + rev = RadioSetting("reverse_duplex", "Reverse duplex", + RadioSettingValueBoolean(bool(_mem.reverse_duplex))) + rev.set_doc("Reverse duplex") + mem.extra.append(rev) + + # Memory display style + opt = ["Frequency", "Label"] + dsp = RadioSetting("display_style", "Display style", + RadioSettingValueList(opt, opt[_mem.display_style])) + dsp.set_doc("Memory display style") + mem.extra.append(dsp) + + return mem + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def set_memory(self, mem): + number = mem.number + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number] + was_empty = int(_flag.empty) + _flag.empty = mem.empty + if mem.empty: + _wipe_memory(_mem, "\xff") + return + if was_empty: + _wipe_memory(_mem, "\x00") + mult = mem.tuning_step * 1000 + _mem.frequency = (mem.freq / mult) + _mem.offset = mem.offset / mult + _mem.name = mem.name.ljust(6) + _mem.repeater_tone = chirp_common.TONES.index(mem.rtone) + _mem.ctcss_tone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs_code = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step) + _mem.tone_mode = TONE_MODES.index(mem.tmode) + _mem.mode_narrow = mem.mode.startswith("N") + _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity) + _mem.duplex = DUPLEX.index(mem.duplex) + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = POWER_LEVELS[0] + _flag.skip = mem.skip != "" + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + front_panel = RadioSettingGroup("front_panel", "Front Panel Settings") + top = RadioSettings(basic, front_panel) + + # Transmit timeout + opt = ['Disabled', '1 minute'] + \ + [s + ' minutes' for s in map(str, range(2, 31))] + rs = RadioSetting("tx_timeout", "Transmit timeout (min)", + RadioSettingValueList(opt, opt[ + _settings.tx_timeout + ])) + basic.append(rs) + + # Auto Repeater (USA model only) + opt = ["Disabled", "Duplex Only", "Duplex and tone"] + rs = RadioSetting("auto_repeater", "Auto repeater", + RadioSettingValueList(opt, opt[ + _settings.auto_repeater + ])) + basic.append(rs) + + # Auto Power Off + opt = ["Disabled", "30 minutes", "60 minutes", "120 minutes"] + rs = RadioSetting("auto_power_off", "Auto power off", + RadioSettingValueList(opt, opt[ + _settings.auto_power_off + ])) + basic.append(rs) + + # Squelch Delay + opt = ["Short", "Long"] + rs = RadioSetting("squelch_delay", "Squelch delay", + RadioSettingValueList(opt, opt[ + _settings.squelch_delay + ])) + basic.append(rs) + + # Squelch Type + opt = ["Noise squelch", "S-meter squelch", "Squelch attenuator"] + rs = RadioSetting("squelch_type", "Squelch type", + RadioSettingValueList(opt, opt[ + _settings.squelch_type + ])) + basic.append(rs) + + # Repeater Lockout + opt = ["Disabled", "Repeater lockout", "Busy lockout"] + rs = RadioSetting("repeater_lockout", "Repeater lockout", + RadioSettingValueList(opt, opt[ + _settings.repeater_lockout + ])) + basic.append(rs) + + # DTMF Speed + opt = ["100ms interval, 5.0 cps", + "200ms interval, 2.5 cps", + "300ms interval, 1.6 cps", + "500ms interval, 1.0 cps"] + rs = RadioSetting("dtmf_speed", "DTMF speed", + RadioSettingValueList(opt, opt[ + _settings.dtmf_speed + ])) + basic.append(rs) + + # Scan pause timer + opt = [s + ' seconds' for s in map(str, range(2, 22, 2))] + ['Hold'] + rs = RadioSetting("scan_pause_timer", "Scan pause timer", + RadioSettingValueList( + opt, opt[_settings.scan_pause_timer])) + basic.append(rs) + + # Scan Resume Timer + opt = ['Immediate'] + \ + [s + ' seconds' for s in map(str, range(1, 6))] + ['Hold'] + rs = RadioSetting("scan_resume_timer", "Scan resume timer", + RadioSettingValueList( + opt, opt[_settings.scan_resume_timer])) + basic.append(rs) + + # Weather Alert (USA model only) + rs = RadioSetting("weather_alert", "Weather alert", + RadioSettingValueBoolean(_settings.weather_alert)) + basic.append(rs) + + # Tone Burst + rs = RadioSetting("tone_burst", "Tone burst", + RadioSettingValueBoolean(_settings.tone_burst)) + basic.append(rs) + + # Memory Display Type + opt = ["Frequency", "Channel", "Name"] + rs = RadioSetting("display_type", "Memory display", + RadioSettingValueList(opt, + opt[_settings.display_type])) + front_panel.append(rs) + + # Display backlight brightness; + opt = ["1 (dimmest)", "2", "3", "4 (brightest)"] + rs = RadioSetting("display_brightness", "Backlight brightness", + RadioSettingValueList( + opt, + opt[_settings.display_brightness])) + front_panel.append(rs) + + # Display backlight color + opt = ["Amber", "Yellow", "Green"] + rs = RadioSetting("display_color", "Backlight color", + RadioSettingValueList(opt, + opt[_settings.display_color])) + front_panel.append(rs) + + # Display contrast + opt = ["1 (lightest)", "2", "3", "4 (darkest)"] + rs = RadioSetting("display_contrast", "Display contrast", + RadioSettingValueList( + opt, + opt[_settings.display_contrast])) + front_panel.append(rs) + + # Auto dimmer + opt = ["Disabled", "Backlight off", "1 (dimmest)", "2", "3"] + rs = RadioSetting("auto_dimmer", "Auto dimmer", + RadioSettingValueList(opt, + opt[_settings.auto_dimmer])) + front_panel.append(rs) + + # Microphone gain + opt = ["Low", "High"] + rs = RadioSetting("mic_gain", "Microphone gain", + RadioSettingValueList(opt, + opt[_settings.mic_gain])) + front_panel.append(rs) + + # Key press beep + rs = RadioSetting("key_beep", "Key press beep", + RadioSettingValueBoolean(_settings.key_beep)) + front_panel.append(rs) + + # Voltage Display; + rs = RadioSetting("voltage_display", "Voltage display", + RadioSettingValueBoolean(_settings.voltage_display)) + front_panel.append(rs) + + # TODO: Add Bank Links settings to GUI + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + setting = element.get_name() + setattr(_settings, setting, element.value) diff --git a/chirp/drivers/ic2720.py b/chirp/drivers/ic2720.py new file mode 100644 index 0000000..30f4a8e --- /dev/null +++ b/chirp/drivers/ic2720.py @@ -0,0 +1,191 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, util, directory, bitwise + +MEM_FORMAT = """ +struct { + u32 freq; + u32 offset; + u8 unknown1:2, + rtone:6; + u8 unknown2:2, + ctone:6; + u8 unknown3:1, + dtcs:7; + u8 unknown4:2, + unknown5:2, + tuning_step:4; + u8 unknown6:2, + tmode:2, + duplex:2, + unknown7:2; + u8 power:2, + is_fm:1, + unknown8:1, + dtcs_polarity:2, + unknown9:2; + u8 unknown[2]; +} memory[200]; + +#seekto 0x0E20; +u8 skips[25]; + +#seekto 0x0EB0; +u8 used[25]; + +#seekto 0x0E40; +struct { + u8 bank_even:4, + bank_odd:4; +} banks[100]; +""" + +TMODES = ["", "Tone", "TSQL", "DTCS"] +POWER = ["High", "Low", "Med"] +DTCS_POLARITY = ["NN", "NR", "RN", "RR"] +STEPS = [5.0, 10.0, 12.5, 15, 20, 25, 30, 50] +MODES = ["FM", "AM"] +DUPLEX = ["", "", "-", "+"] +POWER_LEVELS_VHF = [chirp_common.PowerLevel("High", watts=50), + chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=15)] +POWER_LEVELS_UHF = [chirp_common.PowerLevel("High", watts=35), + chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=15)] + + +@directory.register +class IC2720Radio(icf.IcomCloneModeRadio): + """Icom IC-2720""" + VENDOR = "Icom" + MODEL = "IC-2720H" + + _model = "\x24\x92\x00\x01" + _memsize = 5152 + _endframe = "Icom Inc\x2eA0" + + _ranges = [(0x0000, 0x1400, 32)] + + def _get_bank(self, loc): + _bank = self._memobj.banks[loc / 2] + if loc % 2: + bank = _bank.bank_odd + else: + bank = _bank.bank_even + + if bank == 0x0A: + return None + else: + return bank + + def _set_bank(self, loc, index): + _bank = self._memobj.banks[loc / 2] + if index is None: + index = 0x0A + if loc % 2: + _bank.bank_odd = index + else: + _bank.bank_even = index + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_name = False + rf.memory_bounds = (0, 199) + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(set(DUPLEX)) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(118000000, 999990000)] + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS_VHF + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + bitpos = (1 << (number % 8)) + bytepos = (number / 8) + + _mem = self._memobj.memory[number] + _skp = self._memobj.skips[bytepos] + _usd = self._memobj.used[bytepos] + + mem = chirp_common.Memory() + mem.number = number + + if _usd & bitpos: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) + mem.offset = int(_mem.offset) + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.tmode = TMODES[_mem.tmode] + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity] + mem.tuning_step = STEPS[_mem.tuning_step] + mem.mode = _mem.is_fm and "FM" or "AM" + mem.duplex = DUPLEX[_mem.duplex] + + mem.skip = (_skp & bitpos) and "S" or "" + + if int(mem.freq / 100000000) == 1: + mem.power = POWER_LEVELS_VHF[_mem.power] + else: + mem.power = POWER_LEVELS_UHF[_mem.power] + + return mem + + def set_memory(self, mem): + bitpos = (1 << (mem.number % 8)) + bytepos = (mem.number / 8) + + _mem = self._memobj.memory[mem.number] + _skp = self._memobj.skips[bytepos] + _usd = self._memobj.used[bytepos] + + if mem.empty: + _usd |= bitpos + self._set_bank(mem.number, None) + return + _usd &= ~bitpos + + _mem.freq = mem.freq + _mem.offset = mem.offset + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tmode = TMODES.index(mem.tmode) + _mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity) + _mem.tuning_step = STEPS.index(mem.tuning_step) + _mem.is_fm = mem.mode == "FM" + _mem.duplex = DUPLEX.index(mem.duplex) + + if mem.skip == "S": + _skp |= bitpos + else: + _skp &= ~bitpos + + if mem.power: + _mem.power = POWER_LEVELS_VHF.index(mem.power) + else: + _mem.power = 0 diff --git a/chirp/drivers/ic2730.py b/chirp/drivers/ic2730.py new file mode 100644 index 0000000..3f981e6 --- /dev/null +++ b/chirp/drivers/ic2730.py @@ -0,0 +1,1291 @@ +# Copyright 2018 Rhett Robinson +# Added Settings support, 6/2019 Rick DeWitt +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import logging +from chirp.drivers import icf +from chirp import chirp_common, util, directory, bitwise, memmap +from chirp import errors +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct { + u24 freq_flags:6 + freq:18; + u16 offset; + u8 tune_step:4, + unknown5:2, + mode:2; + u8 unknown6:2, + rtone:6; + u8 unknown7:2, + ctone:6; + u8 unknown8; + u8 dtcs; + u8 tmode:4, + duplex:2, + dtcs_polarity:2; + char name[6]; +} memory[1002]; + +#seekto 0x42c0; +u8 used_flags[125]; + +#seekto 0x433e; +u8 skip_flags[125]; +u8 pskip_flags[125]; + +#seekto 0x4440; +struct { +u8 bank; +u8 index; +} bank_info[1000]; + +#seekto 0x4c40; +struct { +char com[16]; +} comment; + +#seekto 0x4c50; +struct { +char name[6]; +} bank_names[10]; + +#seekto 0x4cc8; +struct{ +u8 codes[24]; +} dtmfcode[16]; + +#seekto 0x4c8c; +struct { +char nam[6]; +} pslnam[10]; + +#seekto 0x4e80; +struct { +u24 loflags:6 + lofreq:18; +u24 hiflags:6 + hifreq:18; +u8 flag:4 + mode:4; +u8 tstp; +char name[6]; +} pgmscanedge[25]; + +#seekto 0x5000; +struct { +u8 aprichn; +u8 bprichn; +u8 autopwr; +u8 unk5003; +u8 autorptr; +u8 rmtmic; // 0x05005 +u8 pttlock; +u8 bcl; +u8 tot; +u8 actband; +u8 unk500a; +u8 dialspdup; +u8 toneburst; +u8 micgain; +u8 unk500e; +u8 civaddr; +u8 civbaud; +u8 civtcvr; +u8 sqlatt; +u8 sqldly; +u8 unk5014a:4 + fanspeed:4; +u8 unk5015; +u8 bthvox; +u8 bthvoxlvl; +u8 bthvoxdly; +u8 bthvoxtot; // 0x05019 +u8 btoothon; +u8 btoothauto; +u8 bthdset; +u8 bthhdpsav; +u8 bth1ptt; // 0x0501e +u8 bthpttbeep; +u8 bthcustbeep; // 0x05020 +u8 ascanpause; +u8 bscanpause; +u8 ascanresume; +u8 bscanresume; +u8 dtmfspd; +u8 unk5026; +u8 awxalert; +u8 bwxalert; +u8 aprgskpscn; +u8 bprgskpscn; +u8 memname; +u8 contrast; +u8 autodimtot; +u8 autodim; +u8 unk502f; +u8 backlight; +u8 unk5031; +u8 unk5032; +u8 openmsg; +u8 beeplvl; // 0x05034 +u8 keybeep; +u8 scanstpbeep; +u8 bandedgbeep; +u8 subandmute; +u8 atmpskiptym; +u8 btmpskiptym; +u32 vfohome; +u8 vfohomeset; +u16 homech; +u8 mickyrxf1; +u8 mickyrxf2; +u8 mickyrxup; +u8 mickyrxdn; // 0x05045 +u8 bthplaykey; +u8 bthfwdkey; +u8 bthrwdkey; +u8 mickytxf1; +u8 mickytxf2; +u8 mickytxup; +u8 mickytxdn; +u8 unk504d; +u8 unk504e; +u8 unk504f; +u8 homebeep; +u8 bthfctn; +u8 unk5052; +u8 unk5053; +u8 ifxchg; +u8 airbandch; +u8 vhfpower; +u8 uhfpower; +u8 unk5058; +u8 unk5059; +u8 unk505a; +u8 unk505b; +u8 unk505c; +u8 unk505d; +u8 unk505e:6 + rpthangup:1 + unk505e2:1; +u8 unk505f; +} settings; + +#seekto 0x5220; +struct { +u16 left_memory; +u16 right_memory; +} initmem; + +#seekto 0x523e; +struct { +u8 awxchan; +u8 bwxchan; +} abwx; + +#seekto 0x5250; +struct { +u8 alnk[2]; +u16 unk5252; +u8 blnk[2]; +} banklink; + +#seekto 0x5258; +struct { +u8 msk[4]; +} pslgrps[25]; + +#seekto 0x5280; +u16 mem_writes_count; +""" + +# Guessing some of these intermediate values are with the Pocket Beep function, +# but I haven't reliably reproduced these. +TMODES = ["", "Tone", "??0", "TSQL", "??1", "DTCS", "TSQL-R", "DTCS-R", + "DTC.OFF", "TON.DTC", "DTC.TSQ", "TON.TSQ"] +DUPLEX = ["", "-", "+"] +MODES = ["FM", "NFM", "AM", "NAM"] +DTCSP = ["NN", "NR", "RN", "RR"] +DTMF_CHARS = list("0123456789ABCD*#") +BANKLINK_CHARS = list("ABCDEFGHIJ") +AUTOREPEATER = ["OFF", "DUP", "DUP.TON"] +MICKEYOPTS = ["Off", "Up", "Down", "Vol Up", "Vol Down", "SQL Up", + "SQL Down", "Monitor", "Call", "MR (Ch 0)", "MR (Ch 1)", + "VFO/MR", "Home Chan", "Band/Bank", "Scan", "Temp Skip", + "Main", "Mode", "Low", "Dup", "Priority", "Tone", "MW", "Mute", + "T-Call", "DTMF Direct"] + + +class IC2730Bank(icf.IcomNamedBank): + """An IC2730 bank""" + def get_name(self): + _banks = self._model._radio._memobj.bank_names + return str(_banks[self.index].name).rstrip() + + def set_name(self, name): + _banks = self._model._radio._memobj.bank_names + _banks[self.index].name = str(name).ljust(6)[:6] + + +def _get_special(): + special = {"C0": -2, "C1": -1} + return special + + +def _resolve_memory_number(number): + if isinstance(number, str): + return _get_special()[number] + else: + return number + + +def _wipe_memory(mem, char): + mem.set_raw(char * (mem.size() // 8)) + + +@directory.register +class IC2730Radio(icf.IcomRawCloneModeRadio): + """Icom IC-2730A""" + VENDOR = "Icom" + MODEL = "IC-2730A" + + _model = "\x35\x98\x00\x01" + _memsize = 21312 # 0x5340 + _endframe = "Icom Inc\x2e4E" + + _ranges = [(0x0000, 0x5300, 64), + (0x5300, 0x5310, 16), + (0x5310, 0x5340, 48)] + + _num_banks = 10 + _bank_class = IC2730Bank + _can_hispeed = True + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = ('Click the Special Channels tab on the main screen to ' + 'access the C0 and C1 frequencies.\n') + + rp.pre_download = _(dedent("""\ + Follow these instructions to download your config: + + 1 - Turn off your radio + 2 - Connect your interface cable to the Speaker-2 jack + 3 - Turn on your radio + 4 - Radio > Download from radio + 5 - Disconnect the interface cable! Otherwise there will be + no right-side audio! + """)) + rp.pre_upload = _(dedent("""\ + Follow these instructions to upload your config: + + 1 - Turn off your radio + 2 - Connect your interface cable to the Speaker-2 jack + 3 - Turn on your radio + 4 - Radio > Upload to radio + 5 - Disconnect the interface cable, otherwise there will be + no right-side audio! + 6 - Cycle power on the radio to exit clone mode + """)) + return rp + + def _get_bank(self, loc): + _bank = self._memobj.bank_info[loc] + _bank.bank = _bank.bank & 0x1F # Bad index filter, fix issue #7031 + if _bank.bank == 0x1F: + return None + else: + return _bank.bank + + def _set_bank(self, loc, bank): + _bank = self._memobj.bank_info[loc] + if bank is None: + _bank.bank = 0x1F + else: + _bank.bank = bank + + def _get_bank_index(self, loc): + _bank = self._memobj.bank_info[loc] + return _bank.index + + def _set_bank_index(self, loc, index): + _bank = self._memobj.bank_info[loc] + _bank.index = index + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank_index = True + rf.has_bank_names = True + rf.requires_call_lists = False + rf.memory_bounds = (0, 999) + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(set(DUPLEX)) + rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS[0:9]) + rf.valid_bands = [(118000000, 174000000), + (375000000, 550000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 6 + rf.valid_special_chans = sorted(_get_special().keys()) + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_memory(self, number): + mem = chirp_common.Memory() + mem.number = _resolve_memory_number(number) + if mem.number >= 0: + _mem = self._memobj.memory[mem.number] + bitpos = (1 << (number % 8)) + bytepos = number / 8 + _used = self._memobj.used_flags[bytepos] + is_used = ((_used & bitpos) == 0) + + _skip = self._memobj.skip_flags[bytepos] + _pskip = self._memobj.pskip_flags[bytepos] + if _skip & bitpos: + mem.skip = "S" + elif _pskip & bitpos: + mem.skip = "P" + if not is_used: + mem.empty = True + return mem + else: # C0, C1 specials + _mem = self._memobj.memory[1002 + mem.number] + + # _mem.freq is stored as a multiple of a tuning step + frequency_flags = int(_mem.freq_flags) + frequency_multiplier = 5000 + offset_multiplier = 5000 + if frequency_flags & 0x08: + frequency_multiplier = 6250 + if frequency_flags & 0x01: + offset_multiplier = 6250 + if frequency_flags & 0x10: + frequency_multiplier = 8333 + if frequency_flags & 0x02: + offset_multiplier = 8333 + + if frequency_flags & 0x10: # fix underflow + val = int(_mem.freq) * frequency_multiplier + mem.freq = round(val) + else: + mem.freq = int(_mem.freq) * frequency_multiplier + if frequency_flags & 0x02: + val = int(_mem.offset) * offset_multiplier + mem.offset = round(val) + else: + mem.offset = int(_mem.offset) * offset_multiplier + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity] + if _mem.tune_step > 8: + mem.tuning_step = 5.0 # Sometimes TS is garbage? + else: + mem.tuning_step = chirp_common.TUNING_STEPS[_mem.tune_step] + mem.name = str(_mem.name).rstrip() + if mem.number == -2: + mem.name = "C0" + mem.extd_number = "C0" + if mem.number == -1: + mem.name = "C1" + mem.extd_number = "C1" + + return mem + + def set_memory(self, mem): + if mem.number >= 0: # Normal + bitpos = (1 << (mem.number % 8)) + bytepos = mem.number / 8 + + _mem = self._memobj.memory[mem.number] + _used = self._memobj.used_flags[bytepos] + + was_empty = _used & bitpos + + skip = self._memobj.skip_flags[bytepos] + pskip = self._memobj.pskip_flags[bytepos] + if mem.skip == "S": + skip |= bitpos + else: + skip &= ~bitpos + if mem.skip == "P": + pskip |= bitpos + else: + pskip &= ~bitpos + + if mem.empty: + _used |= bitpos + _wipe_memory(_mem, "\xFF") + self._set_bank(mem.number, None) + return + + _used &= ~bitpos + if was_empty: + _wipe_memory(_mem, "\x00") + _mem.name = mem.name.ljust(6) + else: # Specials: -2 and -1 + _mem = self._memobj.memory[1002 + mem.number] + + # Common to both types + frequency_flags = 0x00 + frequency_multiplier = 5000 + offset_multiplier = 5000 + if mem.freq % 5000 != 0 and mem.freq % 6250 == 0: + frequency_flags |= 0x08 + frequency_multiplier = 6250 + elif mem.freq % 8333 == 0: + frequency_flags |= 0x10 + frequency_multiplier = 8333 + if mem.offset % 5000 != 0 and mem.offset % 6250 == 0: + frequency_flags |= 0x01 + offset_multiplier = 6250 + elif mem.offset % 8333 == 0: + frequency_flags |= 0x02 + offset_multiplier = 8333 + _mem.freq = mem.freq / frequency_multiplier + _mem.offset = mem.offset / offset_multiplier + _mem.freq_flags = frequency_flags + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.mode = MODES.index(mem.mode) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity) + _mem.tune_step = chirp_common.TUNING_STEPS.index(mem.tuning_step) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_settings(self): + """Translate the MEM_FORMAT structs into settings in the UI""" + # Define mem struct write-back shortcuts + _sets = self._memobj.settings + _cmnt = self._memobj.comment + _wxch = self._memobj.abwx + _dtm = self._memobj.dtmfcode + _pses = self._memobj.pgmscanedge + _bklk = self._memobj.banklink + + basic = RadioSettingGroup("basic", "Basic Settings") + mickey = RadioSettingGroup("mickey", "Microphone Keys") + bluet = RadioSettingGroup("bluet", "Bluetooth") + disp = RadioSettingGroup("disp", "Display") + sound = RadioSettingGroup("sound", "Sounds") + dtmf = RadioSettingGroup("dtmf", "DTMF Codes") + abset = RadioSettingGroup("abset", "A/B Band Settings") + edges = RadioSettingGroup("edges", "Program Scan Edges") + pslnk = RadioSettingGroup("pslnk", "Program Scan Links") + other = RadioSettingGroup("other", "Other Settings") + + group = RadioSettings(basic, disp, sound, mickey, dtmf, + abset, bluet, edges, pslnk, other) + + def mic_keys(setting, obj, atrb): + """ Callback to set subset of mic key options """ + stx = str(setting.value) + value = MICKEYOPTS.index(stx) + setattr(obj, atrb, value) + return + + def hex_val(setting, obj, atrb): + """ Callback to store string as hex values """ + value = int(str(setting.value), 16) + setattr(obj, atrb, value) + return + + def unpack_str(codestr): + """Convert u8 DTMF array to a string: NOT a callback.""" + stx = "" + for i in range(0, 24): # unpack up to ff + if codestr[i] != 0xff: + if codestr[i] == 0x0E: + stx += "#" + elif codestr[i] == 0x0F: + stx += "*" + else: + stx += format(int(codestr[i]), '0X') + return stx + + def pack_chars(setting, obj, atrb, ndx): + """Callback to build 0-9,A-D,*# nibble array from string""" + # String will be f padded to 24 bytes + # Chars are stored as hex values + ary = [] + stx = str(setting.value).upper() + stx = stx.strip() # trim spaces + # Remove illegal characters first + sty = "" + for j in range(0, len(stx)): + if stx[j] in DTMF_CHARS: + sty += stx[j] + for j in range(0, 24): + if j < len(sty): + if sty[j] == "#": + chrv = 0xE + elif sty[j] == "*": + chrv = 0xF + else: + chrv = int(sty[j], 16) + else: # pad to 24 bytes + chrv = 0xFF + ary.append(chrv) # append byte + setattr(obj[ndx], atrb, ary) + return + + def myset_comment(setting, obj, atrb, knt): + """ Callback to create space-padded char array""" + stx = str(setting.value) + for i in range(0, knt): + if i > len(stx): + str.append(0x20) + setattr(obj, atrb, stx) + return + + def myset_psnam(setting, obj, ndx, atrb, knt): + """ Callback to generate space-padded, uppercase char array """ + # This sub also is specific to object arrays + stx = str(setting.value).upper() + for i in range(0, knt): + if i > len(stx): + str.append(0x20) + setattr(obj[ndx], atrb, stx) + return + + def myset_frqflgs(setting, obj, ndx, flg, frq): + """ Callback to gen flag/freq pairs """ + vfrq = float(str(setting.value)) + vfrq = int(vfrq * 1000000) + vflg = 0x10 + if vfrq % 6250 == 0: + vflg = 0x08 + vfrq = int(vfrq / 6250) + elif vfrq % 5000 == 0: + vflg = 0 + vfrq = int(vfrq / 5000) + else: + vfrq = int(vfrq / 8333) + setattr(obj[ndx], flg, vflg) + setattr(obj[ndx], frq, vfrq) + return + + def banklink(ary): + """ Sub to generate A-J string from 2-byte bit pattern """ + stx = "" + for kx in range(0, 10): + if kx < 8: + val = ary[0] + msk = 1 << kx + else: + val = ary[1] + msk = 1 << (kx - 8) + if val & msk: + stx += chr(kx + 65) + else: + stx += "_" + return stx + + def myset_banklink(setting, obj, atrb): + """Callback to create 10-bit, u8[2] array from 10 char string""" + stx = str(setting.value).upper() + ary = [0, 0] + for kx in range(0, 10): + if stx[kx] == chr(kx + 65): + if kx < 8: + ary[0] = ary[0] + (1 << kx) + else: + ary[1] = ary[1] + (1 << (kx - 8)) + setattr(obj, atrb, ary) + return + + def myset_tsopt(setting, obj, ndx, atrb, bx): + """ Callback to set scan Edge tstep """ + stx = str(setting.value) + flg = 0 + if stx == "-": + val = 0xff + else: + if bx == 1: # Air band + if stx == "Auto": + val = 0xe + elif stx == "25k": + val = 8 + elif stx == "8.33k": + val = 2 + else: # VHF or UHF + optx = ["-", "5k", "6.25k", "10k", "12.5k", "15k", + "20k", "25k", "30k", "50k"] + val = optx.index(stx) + 1 + setattr(obj[ndx], atrb, val) + # and set flag + setattr(obj[ndx], "flag", flg) + return + + def myset_mdopt(setting, obj, ndx, atrb, bx): + """ Callback to set Scan Edge mode """ + stx = str(setting.value) + if stx == "-": + val = 0xf + elif stx == "FM": + val = 0 + else: + val = 1 + setattr(obj[ndx], atrb, val) + return + + def myset_bitmask(setting, obj, ndx, atrb, knt): + """ Callback to gnerate byte-array bitmask from string""" + # knt is BIT count to process + lsx = str(setting.value).strip().split(",") + for kx in range(0, len(lsx)): + try: + lsx[kx] = int(lsx[kx]) + except Exception: + lsx[kx] = -99 # will nop + ary = [0, 0, 0, 0xfe] + for kx in range(0, knt): + if kx < 8: + if kx in lsx: + ary[0] += 1 << kx + elif kx >= 8 and kx < 16: + if kx in lsx: + ary[1] += 1 << (kx - 8) + elif kx >= 16 and kx < 24: + if kx in lsx: + ary[2] += 1 << (kx - 16) + else: + if kx in lsx: # only bit 25 + ary[3] += 1 + setattr(obj[ndx], atrb, ary) + return + + # --- Basic + options = ["Off", "S-Meter Squelch", "ATT"] + rx = RadioSettingValueList(options, options[_sets.sqlatt]) + rset = RadioSetting("settings.sqlatt", "Squelch/ATT", rx) + basic.append(rset) + + options = ["Short", "Long"] + rx = RadioSettingValueList(options, options[_sets.sqldly]) + rset = RadioSetting("settings.sqldly", "Squelch Delay", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.pttlock)) + rset = RadioSetting("settings.pttlock", "PTT Lockout", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bcl)) + rset = RadioSetting("settings.bcl", "Busy Channel Lockout", rx) + basic.append(rset) + + options = ["Off", "1m", "3m", "5m", "10m", "15m", "30m"] + rx = RadioSettingValueList(options, options[_sets.tot]) + rset = RadioSetting("settings.tot", "Tx Timeout", rx) + basic.append(rset) + + val = int(_sets.vfohome) + if val == 0xffff: + val = 0 + val = val / 1000000.0 + rx = RadioSettingValueFloat(0.0, 550.0, val, 0.005, 4) + rx.set_mutable(False) + rset = RadioSetting("settings.vfohome", "Home VFO (Read-Only)", rx) + basic.append(rset) + + val = _sets.homech + if val == 0xffff: + val = -1 + rx = RadioSettingValueInteger(-1, 999, val) + rx.set_mutable(False) + rset = RadioSetting("settings.homech", + "Home Channel (Read-Only)", rx) + basic.append(rset) + + options = ["1", "2", "3", "4"] + rx = RadioSettingValueList(options, options[_sets.micgain]) + rset = RadioSetting("settings.micgain", "Microphone Gain", rx) + basic.append(rset) + + _bmem = self._memobj.initmem + rx = RadioSettingValueInteger(0, 999, _bmem.left_memory) + rset = RadioSetting("initmem.left_memory", + "Left Bank Initial Mem Chan", rx) + basic.append(rset) + + rx = RadioSettingValueInteger(0, 999, _bmem.right_memory) + rset = RadioSetting("initmem.right_memory", + "Right Bank Initial Mem Chan", rx) + basic.append(rset) + + stx = "" + for i in range(0, 16): + stx += chr(_cmnt.com[i]) + stx = stx.rstrip() + rx = RadioSettingValueString(0, 16, stx) + rset = RadioSetting("comment.com", "Comment (16 chars)", rx) + rset.set_apply_callback(myset_comment, _cmnt, "com", 16) + basic.append(rset) + + # --- Other + rset = RadioSetting("drv_clone_speed", "Use Hi-Speed Clone", + RadioSettingValueBoolean(self._can_hispeed)) + other.append(rset) + + options = ["Single", "All", "Ham"] + rx = RadioSettingValueList(options, options[_sets.actband]) + rset = RadioSetting("settings.actband", "Active Band", rx) + other.append(rset) + + options = ["Slow", "Mid", "Fast", "Auto"] + rx = RadioSettingValueList(options, options[_sets.fanspeed]) + rset = RadioSetting("settings.fanspeed", "Fan Speed", rx) + other.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.dialspdup)) + rset = RadioSetting("settings.dialspdup", "Dial Speed-Up", rx) + other.append(rset) + + options = ["Off", "On(Dup)", "On(Dup+Tone)"] + rx = RadioSettingValueList(options, options[_sets.autorptr]) + rset = RadioSetting("settings.autorptr", "Auto Repeater", rx) + other.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.rmtmic)) + rset = RadioSetting("settings.rmtmic", + "One-Touch PTT (Remote Mic)", rx) + other.append(rset) + + options = ["Low", "Mid", "High"] + rx = RadioSettingValueList(options, options[_sets.vhfpower]) + rset = RadioSetting("settings.vhfpower", "VHF Power Default", rx) + other.append(rset) + + rx = RadioSettingValueList(options, options[_sets.uhfpower]) + rset = RadioSetting("settings.uhfpower", "UHF Power Default", rx) + other.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.toneburst)) + rset = RadioSetting("settings.toneburst", "1750 Htz Tone Burst", rx) + other.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.ifxchg)) + rset = RadioSetting("settings.ifxchg", "IF Exchange", rx) + other.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.rpthangup)) + rset = RadioSetting("settings.rpthangup", + "Repeater Hang up Timeout", rx) + other.append(rset) + + stx = str(_sets.civaddr)[2:] # Hex value + rx = RadioSettingValueString(1, 2, stx) + rset = RadioSetting("settings.civaddr", "CI-V Address (90)", rx) + rset.set_apply_callback(hex_val, _sets, "civaddr") + other.append(rset) + + options = ["1200", "2400", "4800", "9600", "19200", "Auto"] + rx = RadioSettingValueList(options, options[_sets.civbaud]) + rset = RadioSetting("settings.civbaud", "CI-V Baud Rate (bps)", rx) + other.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.civtcvr)) + rset = RadioSetting("settings.civtcvr", "CI-V Transceive", rx) + other.append(rset) + + # A/B Band Settings + options = ["Off", "On", "Bell"] + rx = RadioSettingValueList(options, options[_sets.aprichn]) + rset = RadioSetting("settings.aprichn", + "A Band: VFO Priority Watch Mode", rx) + abset.append(rset) + + options = ["2", "4", "6", "8", "10", "12", "14", + "16", "18", "20", "Hold"] + rx = RadioSettingValueList(options, options[_sets.ascanpause]) + rset = RadioSetting("settings.ascanpause", + "-- A Band: Scan Pause Time (Secs)", rx) + abset.append(rset) + + options = ["0", "1", "2", "3", "4", "5", "Hold"] + rx = RadioSettingValueList(options, options[_sets.ascanresume]) + rset = RadioSetting("settings.ascanresume", + "-- A Band: Scan Resume Time (Secs)", rx) + abset.append(rset) + + options = ["5", "10", "15"] + rx = RadioSettingValueList(options, options[_sets.atmpskiptym]) + rset = RadioSetting("settings.atmpskiptym", + "-- A Band: Temp Skip Time (Secs)", rx) + abset.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.aprgskpscn)) + rset = RadioSetting("settings.aprgskpscn", + "-- A Band: Program Skip Scan", rx) + abset.append(rset) + + rx = RadioSettingValueString(10, 10, banklink(_bklk.alnk)) + rset = RadioSetting("banklink.alnk", + "-- A Band Banklink (use _ to skip)", rx) + rset.set_apply_callback(myset_banklink, _bklk, "alnk") + abset.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.awxalert)) + rset = RadioSetting("settings.awxalert", + "-- A Band: Weather Alert", rx) + abset.append(rset) + + # Use list for Wx chans since chan 1 = index 0 + options = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] + rx = RadioSettingValueList(options, options[_wxch.awxchan]) + rset = RadioSetting("abwx.awxchan", "-- A Band: Weather Channel", rx) + abset.append(rset) + + options = ["Off", "On", "Bell"] + rx = RadioSettingValueList(options, options[_sets.bprichn]) + rset = RadioSetting("settings.bprichn", + "B Band: VFO Priority Watch Mode", rx) + abset.append(rset) + + options = ["2", "4", "6", "8", "10", "12", "14", + "16", "18", "20", "Hold"] + rx = RadioSettingValueList(options, options[_sets.bscanpause]) + rset = RadioSetting("settings.bscanpause", + "-- B Band: Scan Pause Time (Secs)", rx) + abset.append(rset) + + options = ["0", "1", "2", "3", "4", "5", "Hold"] + rx = RadioSettingValueList(options, options[_sets.bscanresume]) + rset = RadioSetting("settings.bscanresume", + "-- B Band: Scan Resume Time (Secs)", rx) + abset.append(rset) + + options = ["5", "10", "15"] + rx = RadioSettingValueList(options, options[_sets.btmpskiptym]) + rset = RadioSetting("settings.btmpskiptym", + "-- B Band: Temp Skip Time (Secs)", rx) + abset.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bprgskpscn)) + rset = RadioSetting("settings.bprgskpscn", + "-- B Band: Program Skip Scan", rx) + abset.append(rset) + + rx = RadioSettingValueString(10, 10, banklink(_bklk.blnk)) + rset = RadioSetting("banklink.blnk", + "-- B Band Banklink (use _ to skip)", rx) + rset.set_apply_callback(myset_banklink, _bklk, "blnk") + abset.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bwxalert)) + rset = RadioSetting("settings.bwxalert", + "-- B Band: Weather Alert", rx) + abset.append(rset) + + options = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] + rx = RadioSettingValueList(options, options[_wxch.bwxchan]) + rset = RadioSetting("abwx.bwxchan", "-- B Band: Weather Channel", rx) + abset.append(rset) + + # --- Microphone Keys + # The Mic keys get wierd: stored values are indecis to the full + # options list, but only a subset is valid... + shortopts = ["Off", "Monitor", "MR (Ch 0)", "MR (Ch 1)", "Band/Bank", + "Scan", "Temp Skip", "Mode", "Low", "Dup", "Priority", + "Tone", "MW", "Mute", "DTMF Direct", "T-Call"] + ptr = shortopts.index(MICKEYOPTS[_sets.mickyrxf1]) + rx = RadioSettingValueList(shortopts, shortopts[ptr]) + rset = RadioSetting("settings.mickyrxf1", + "During Rx/Standby [F-1]", rx) + rset.set_apply_callback(mic_keys, _sets, "mickyrxf1") + mickey.append(rset) + + ptr = shortopts.index(MICKEYOPTS[_sets.mickyrxf2]) + rx = RadioSettingValueList(shortopts, shortopts[ptr]) + rset = RadioSetting("settings.mickyrxf2", + "During Rx/Standby [F-2]", rx) + rset.set_apply_callback(mic_keys, _sets, "mickyrxf2") + mickey.append(rset) + + options = ["Off", "Low", "T-Call"] # NOT a subset of MICKEYOPTS + rx = RadioSettingValueList(options, options[_sets.mickytxf1]) + rset = RadioSetting("settings.mickytxf1", "During Tx [F-1]", rx) + mickey.append(rset) + + rx = RadioSettingValueList(options, options[_sets.mickytxf2]) + rset = RadioSetting("settings.mickytxf2", "During Tx [F-2]", rx) + mickey.append(rset) + + # These next two get the full options list + rx = RadioSettingValueList(MICKEYOPTS, MICKEYOPTS[_sets.mickyrxup]) + rset = RadioSetting("settings.mickyrxup", + "During Rx/Standby [Up]", rx) + mickey.append(rset) + + rx = RadioSettingValueList(MICKEYOPTS, MICKEYOPTS[_sets.mickyrxdn]) + rset = RadioSetting("settings.mickyrxdn", + "During Rx/Standby [Down]", rx) + mickey.append(rset) + + options = ["Off", "Low", "T-Call"] + rx = RadioSettingValueList(options, options[_sets.mickytxup]) + rset = RadioSetting("settings.mickytxup", "During Tx [Up]", rx) + mickey.append(rset) + + rx = RadioSettingValueList(options, options[_sets.mickytxdn]) + rset = RadioSetting("settings.mickytxdn", "During Tx [Down]", rx) + mickey.append(rset) + + # --- Bluetooth + rx = RadioSettingValueBoolean(bool(_sets.btoothon)) + rset = RadioSetting("settings.btoothon", "Bluetooth", rx) + bluet.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.btoothauto)) + rset = RadioSetting("settings.btoothauto", "Auto Connect", rx) + bluet.append(rset) + + options = ["Headset Only", "Headset & Speaker"] + rx = RadioSettingValueList(options, options[_sets.bthdset]) + rset = RadioSetting("settings.bthdset", "Headset Audio", rx) + bluet.append(rset) + + options = ["Normal", "Microphone", "PTT (Audio:Main)", + "PTT(Audio:Controller)"] + rx = RadioSettingValueList(options, options[_sets.bthfctn]) + rset = RadioSetting("settings.bthfctn", "Headset Function", rx) + bluet.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bthvox)) + rset = RadioSetting("settings.bthvox", "Vox", rx) + bluet.append(rset) + + options = ["Off", "1.0", "2", "3", "4", "5", "6", "7", "8", "9", "10"] + rx = RadioSettingValueList(options, options[_sets.bthvoxlvl]) + rset = RadioSetting("settings.bthvoxlvl", "Vox Level", rx) + bluet.append(rset) + + options = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"] + rx = RadioSettingValueList(options, options[_sets.bthvoxdly]) + rset = RadioSetting("settings.bthvoxdly", "Vox Delay (Secs)", rx) + bluet.append(rset) + + options = ["Off", "1", "2", "3", "4", "5", "10", "15"] + rx = RadioSettingValueList(options, options[_sets.bthvoxtot]) + rset = RadioSetting("settings.bthvoxtot", "Vox Time-Out (Mins)", rx) + bluet.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bthhdpsav)) + rset = RadioSetting("settings.bthhdpsav", + "ICOM Headset Power-Save", rx) + bluet.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bth1ptt)) + rset = RadioSetting("settings.bth1ptt", + "ICOM Headset One-Touch PTT", rx) + bluet.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bthpttbeep)) + rset = RadioSetting("settings.bthpttbeep", + "ICOM Headset PTT Beep", rx) + bluet.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bthcustbeep)) + rset = RadioSetting("settings.bthcustbeep", + "ICOM Headset Custom Key Beep", rx) + bluet.append(rset) + + rx = RadioSettingValueList(MICKEYOPTS, MICKEYOPTS[_sets.bthplaykey]) + rset = RadioSetting("settings.bthplaykey", + "ICOM Headset Custom Key [Play]", rx) + bluet.append(rset) + + rx = RadioSettingValueList(MICKEYOPTS, MICKEYOPTS[_sets.bthfwdkey]) + rset = RadioSetting("settings.bthfwdkey", + "ICOM Headset Custom Key [Fwd]", rx) + bluet.append(rset) + + rx = RadioSettingValueList(MICKEYOPTS, MICKEYOPTS[_sets.bthrwdkey]) + rset = RadioSetting("settings.bthrwdkey", + "ICOM Headset Custom Key [Rwd]", rx) + bluet.append(rset) + + # ---- Display + options = ["1: Dark", "2", "3", "4: Bright"] + rx = RadioSettingValueList(options, options[_sets.backlight]) + rset = RadioSetting("settings.backlight", "Backlight Level", rx) + disp.append(rset) + + options = ["Off", "Auto-Off", "Auto-1", "Auto-2", "Auto-3"] + rx = RadioSettingValueList(options, options[_sets.autodim]) + rset = RadioSetting("settings.autodim", "Auto Dimmer", rx) + disp.append(rset) + + options = ["5", "10"] + rx = RadioSettingValueList(options, options[_sets.autodimtot]) + rset = RadioSetting("settings.autodimtot", + "Auto-Dimmer Timeout (Secs)", rx) + disp.append(rset) + + options = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] + rx = RadioSettingValueList(options, options[_sets.contrast]) + rset = RadioSetting("settings.contrast", "LCD Contrast", rx) + disp.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.openmsg)) + rset = RadioSetting("settings.openmsg", "Opening Message", rx) + disp.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.memname)) + rset = RadioSetting("settings.memname", "Memory Names", rx) + disp.append(rset) + + options = ["CH ID", "Frequency"] + rx = RadioSettingValueList(options, options[_sets.airbandch]) + rset = RadioSetting("settings.airbandch", "Air Band Display", rx) + disp.append(rset) + + # -- Sounds + rx = RadioSettingValueInteger(0, 9, _sets.beeplvl) + rset = RadioSetting("settings.beeplvl", "Beep Level", rx) + sound.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.homebeep)) + rset = RadioSetting("settings.homebeep", "Home Chan Beep", rx) + sound.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.keybeep)) + rset = RadioSetting("settings.keybeep", "Key Touch Beep", rx) + sound.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.bandedgbeep)) + rset = RadioSetting("settings.bandedgbeep", "Band Edge Beep", rx) + sound.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.scanstpbeep)) + rset = RadioSetting("settings.scanstpbeep", "Scan Stop Beep", rx) + sound.append(rset) + + options = ["Off", "Mute", "Beep", "Mute & Beep"] + rx = RadioSettingValueList(options, options[_sets.subandmute]) + rset = RadioSetting("settings.subandmute", "Sub Band Mute", rx) + sound.append(rset) + + # --- DTMF Codes + options = ["100", "200", "300", "500"] + rx = RadioSettingValueList(options, options[_sets.dtmfspd]) + rset = RadioSetting("settings.dtmfspd", "DTMF Speed (mSecs)", rx) + dtmf.append(rset) + + for kx in range(0, 16): + stx = unpack_str(_dtm[kx].codes) + rx = RadioSettingValueString(0, 24, stx) + # NOTE the / to indicate indexed array + rset = RadioSetting("dtmfcode/%d.codes" % kx, + "DTMF Code %X" % kx, rx) + rset.set_apply_callback(pack_chars, _dtm, "codes", kx) + dtmf.append(rset) + + # --- Program Scan Edges + for kx in range(0, 25): + stx = "" + for i in range(0, 6): + stx += chr(_pses[kx].name[i]) + stx = stx.rstrip() + rx = RadioSettingValueString(0, 6, stx) + rset = RadioSetting("pgmscanedge/%d.name" % kx, + "Program Scan %d Name" % kx, rx) + rset.set_apply_callback(myset_psnam, _pses, kx, "name", 6) + edges.append(rset) + + # Freq's use the multiplier flags + fmult = 5000.0 + if _pses[kx].loflags == 0x10: + fmult = 8333 + if _pses[kx].loflags == 0x08: + fmult = 6250.0 + flow = (int(_pses[kx].lofreq) * fmult) / 1000000.0 + flow = round(flow, 4) + + fmult = 5000.0 + if _pses[kx].hiflags == 0x10: + fmult = 8333 + if _pses[kx].hiflags == 0x08: + fmult = 6250.0 + fhigh = (int(_pses[kx].hifreq) * fmult) / 1000000.0 + fhigh = round(fhigh, 4) + if (flow > 0) and (flow >= fhigh): # reverse em + val = flow + flow = fhigh + fhigh = val + rx = RadioSettingValueFloat(0, 550.0, flow, 0.010, 3) + rset = RadioSetting("pgmscanedge/%d.lofreq" % kx, + "-- Scan %d Low Limit" % kx, rx) + rset.set_apply_callback(myset_frqflgs, _pses, kx, "loflags", + "lofreq") + edges.append(rset) + + rx = RadioSettingValueFloat(0, 550.0, fhigh, 0.010, 3) + rset = RadioSetting("pgmscanedge/%d.hifreq" % kx, + "-- Scan %d High Limit" % kx, rx) + rset.set_apply_callback(myset_frqflgs, _pses, kx, "hiflags", + "hifreq") + edges.append(rset) + + # Tstep and Mode depend on the bands + ndxt = 0 + ndxm = 0 + bxnd = 0 + tsopt = ["-", "5k", "6.25k", "10k", "12.5k", "15k", + "20k", "25k", "30k", "50k"] + mdopt = ["-", "FM", "FM-N"] + if fhigh > 0: + if fhigh < 135.0: # Air band + bxnd = 1 + tsopt = ["-", "8.33k", "25k", "Auto"] + ndxt = _pses[kx].tstp + if ndxt == 0xe: # Auto + ndxt = 3 + elif ndxt == 8: # 25k + ndxt = 2 + elif ndxt == 2: # 8.33k + ndxt = 1 + else: + ndxt = 0 + mdopt = ["-"] + elif (flow >= 137.0) and (fhigh <= 174.0): # VHF + ndxt = _pses[kx].tstp - 1 + ndxm = _pses[kx].mode + 1 + bxnd = 2 + elif (flow >= 375.0) and (fhigh <= 550.0): # UHF + ndxt = _pses[kx].tstp - 1 + ndxm = _pses[kx].mode + 1 + bxnd = 3 + else: # Mixed, ndx's = 0 default + tsopt = ["-"] + mdopt = ["-"] + bxnd = 4 + if (ndxt > 9) or (ndxt < 0): + ndxt = 0 # trap ff + if ndxm > 2: + ndxm = 0 + # end if fhigh > 0 + rx = RadioSettingValueList(tsopt, tsopt[ndxt]) + rset = RadioSetting("pgmscanedge/%d.tstp" % kx, + "-- Scan %d Freq Step" % kx, rx) + rset.set_apply_callback(myset_tsopt, _pses, kx, "tstp", bxnd) + edges.append(rset) + + rx = RadioSettingValueList(mdopt, mdopt[ndxm]) + rset = RadioSetting("pgmscanedge/%d.mode" % kx, + "-- Scan %d Mode" % kx, rx) + rset.set_apply_callback(myset_mdopt, _pses, kx, "mode", bxnd) + edges.append(rset) + # End for kx + + # --- Program Scan Links + _psln = self._memobj.pslnam + _pslg = self._memobj.pslgrps + for kx in range(0, 10): + stx = "" + for i in range(0, 6): + stx += chr(_psln[kx].nam[i]) + stx = stx.rstrip() + rx = RadioSettingValueString(0, 6, stx) + rset = RadioSetting("pslnam/%d.nam" % kx, + "Program Scan Link %d Name" % kx, rx) + rset.set_apply_callback(myset_psnam, _psln, kx, "nam", 6) + pslnk.append(rset) + + for px in range(0, 25): + # Generate string numeric representation of 4-byte bitmask + stx = "" + for nx in range(0, 25): + if nx < 8: + if (_pslg[kx].msk[0] & (1 << nx)): + stx += "%0d, " % nx + elif (nx >= 8) and (nx < 16): + if (_pslg[kx].msk[1] & (1 << (nx - 8))): + sstx += "%0d, " % nx + elif (nx >= 16) and (nx < 24): + if (_pslg[kx].msk[2] & (1 << (nx - 16))): + stx += "%0d, " % nx + elif (nx >= 24): + if (_pslg[kx].msk[3] & (1 << (nx - 24))): + stx += "%0d, " % nx + rx = RadioSettingValueString(0, 80, stx) + rset = RadioSetting("pslgrps/%d.msk" % kx, + "--- Scan Link %d Scans" % kx, rx) + rset.set_apply_callback(myset_bitmask, _pslg, + kx, "msk", 25) + pslnk.append(rset) + # end for px + # End for kx + return group # END get_settings() + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.get_name() == "drv_clone_speed": + val = element.value.get_value() + self.__class__._can_hispeed = val + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception as e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/ic2820.py b/chirp/drivers/ic2820.py new file mode 100644 index 0000000..315436b --- /dev/null +++ b/chirp/drivers/ic2820.py @@ -0,0 +1,356 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, util, directory, bitwise + +MEM_FORMAT = """ +struct { + u32 freq; + u32 offset; + char urcall[8]; + char r1call[8]; + char r2call[8]; + u8 unknown1; + u8 unknown2:1, + duplex:2, + tmode:3, + unknown3:2; + u16 ctone:6, + rtone:6, + tune_step:4; + u16 dtcs:7, + mode:3, + unknown4:6; + u8 unknown5:1, + digital_code:7; + u8 unknown6:2, + dtcs_polarity:2, + unknown7:4; + char name[8]; +} memory[522]; + +#seekto 0x61E0; +u8 used_flags[66]; + +#seekto 0x6222; +u8 skip_flags[65]; +u8 pskip_flags[65]; + +#seekto 0x62A4; +struct { + u8 bank; + u8 index; +} bank_info[500]; + +#seekto 0x66C0; +struct { + char name[8]; +} bank_names[26]; + +#seekto 0x6970; +struct { + char call[8]; + u8 unknown[4]; +} mycall[6]; + +#seekto 0x69B8; +struct { + char call[8]; +} urcall[60]; + +struct { + char call[8]; +} rptcall[60]; + +""" + +TMODES = ["", "Tone", "??0", "TSQL", "??1", "??2", "DTCS"] +DUPLEX = ["", "-", "+", "+"] # Not sure about index 3 +MODES = ["FM", "NFM", "AM", "??", "DV"] +DTCSP = ["NN", "NR", "RN", "RR"] + +MEM_LOC_SIZE = 48 + + +class IC2820Bank(icf.IcomNamedBank): + """An IC2820 bank""" + def get_name(self): + _banks = self._model._radio._memobj.bank_names + return str(_banks[self.index].name).rstrip() + + def set_name(self, name): + _banks = self._model._radio._memobj.bank_names + _banks[self.index].name = str(name).ljust(8)[:8] + + +def _get_special(): + special = {"C0": 500 + 20, + "C1": 500 + 21} + + for i in range(0, 10): + ida = "%iA" % i + idb = "%iB" % i + special[ida] = 500 + i * 2 + special[idb] = 500 + i * 2 + 1 + + return special + + +def _resolve_memory_number(number): + if isinstance(number, str): + return _get_special()[number] + else: + return number + + +def _wipe_memory(mem, char): + mem.set_raw(char * (mem.size() // 8)) + + +@directory.register +class IC2820Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport): + """Icom IC-2820""" + VENDOR = "Icom" + MODEL = "IC-2820H" + + _model = "\x29\x70\x00\x01" + _memsize = 44224 + _endframe = "Icom Inc\x2e68" + + _ranges = [(0x0000, 0x6960, 32), + (0x6960, 0x6980, 16), + (0x6980, 0x7160, 32), + (0x7160, 0x7180, 16), + (0x7180, 0xACC0, 32), + ] + + _num_banks = 26 + _bank_class = IC2820Bank + _can_hispeed = True + + MYCALL_LIMIT = (1, 7) + URCALL_LIMIT = (1, 61) + RPTCALL_LIMIT = (1, 61) + + _memories = {} + + def _get_bank(self, loc): + _bank = self._memobj.bank_info[loc] + if _bank.bank == 0xFF: + return None + else: + return _bank.bank + + def _set_bank(self, loc, bank): + _bank = self._memobj.bank_info[loc] + if bank is None: + _bank.bank = 0xFF + else: + _bank.bank = bank + + def _get_bank_index(self, loc): + _bank = self._memobj.bank_info[loc] + return _bank.index + + def _set_bank_index(self, loc, index): + _bank = self._memobj.bank_info[loc] + _bank.index = index + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank_index = True + rf.has_bank_names = True + rf.requires_call_lists = False + rf.memory_bounds = (0, 499) + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(set(DUPLEX)) + rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS) + rf.valid_bands = [(118000000, 999990000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 8 + rf.valid_special_chans = sorted(_get_special().keys()) + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_memory(self, number): + number = _resolve_memory_number(number) + + bitpos = (1 << (number % 8)) + bytepos = number / 8 + + _mem = self._memobj.memory[number] + _used = self._memobj.used_flags[bytepos] + + is_used = ((_used & bitpos) == 0) + + if is_used and MODES[_mem.mode] == "DV": + mem = chirp_common.DVMemory() + mem.dv_urcall = str(_mem.urcall).rstrip() + mem.dv_rpt1call = str(_mem.r1call).rstrip() + mem.dv_rpt2call = str(_mem.r2call).rstrip() + else: + mem = chirp_common.Memory() + + mem.number = number + if number < 500: + _skip = self._memobj.skip_flags[bytepos] + _pskip = self._memobj.pskip_flags[bytepos] + if _skip & bitpos: + mem.skip = "S" + elif _pskip & bitpos: + mem.skip = "P" + else: + mem.extd_number = util.get_dict_rev(_get_special(), number) + mem.immutable = ["number", "skip", "bank", "bank_index", + "extd_number"] + + if not is_used: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) + mem.offset = int(_mem.offset) + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity] + if _mem.tune_step > 8: + mem.tuning_step = 5.0 # Sometimes TS is garbage? + else: + mem.tuning_step = chirp_common.TUNING_STEPS[_mem.tune_step] + mem.name = str(_mem.name).rstrip() + + return mem + + def set_memory(self, mem): + bitpos = (1 << (mem.number % 8)) + bytepos = mem.number / 8 + + _mem = self._memobj.memory[mem.number] + _used = self._memobj.used_flags[bytepos] + + was_empty = _used & bitpos + + if mem.number < 500: + skip = self._memobj.skip_flags[bytepos] + pskip = self._memobj.pskip_flags[bytepos] + if mem.skip == "S": + skip |= bitpos + else: + skip &= ~bitpos + if mem.skip == "P": + pskip |= bitpos + else: + pskip &= ~bitpos + + if mem.empty: + _used |= bitpos + _wipe_memory(_mem, "\xFF") + self._set_bank(mem.number, None) + return + + _used &= ~bitpos + if was_empty: + _wipe_memory(_mem, "\x00") + + _mem.freq = mem.freq + _mem.offset = mem.offset + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.mode = MODES.index(mem.mode) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity) + _mem.tune_step = chirp_common.TUNING_STEPS.index(mem.tuning_step) + _mem.name = mem.name.ljust(8) + + if isinstance(mem, chirp_common.DVMemory): + _mem.urcall = mem.dv_urcall.ljust(8) + _mem.r1call = mem.dv_rpt1call.ljust(8) + _mem.r2call = mem.dv_rpt2call.ljust(8) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_urcall_list(self): + _calls = self._memobj.urcall + calls = [] + + for i in range(*self.URCALL_LIMIT): + calls.append(str(_calls[i-1].call)) + + return calls + + def get_repeater_call_list(self): + _calls = self._memobj.rptcall + calls = [] + + for i in range(*self.RPTCALL_LIMIT): + calls.append(str(_calls[i-1].call)) + + return calls + + def get_mycall_list(self): + _calls = self._memobj.mycall + calls = [] + + for i in range(*self.MYCALL_LIMIT): + calls.append(str(_calls[i-1].call)) + + return calls + + def set_urcall_list(self, calls): + _calls = self._memobj.urcall + + for i in range(*self.URCALL_LIMIT): + try: + call = calls[i-1] + except IndexError: + call = " " * 8 + + _calls[i-1].call = call.ljust(8)[:8] + + def set_repeater_call_list(self, calls): + _calls = self._memobj.rptcall + + for i in range(*self.RPTCALL_LIMIT): + try: + call = calls[i-1] + except IndexError: + call = " " * 8 + + _calls[i-1].call = call.ljust(8)[:8] + + def set_mycall_list(self, calls): + _calls = self._memobj.mycall + + for i in range(*self.MYCALL_LIMIT): + try: + call = calls[i-1] + except IndexError: + call = " " * 8 + + _calls[i-1].call = call.ljust(8)[:8] diff --git a/chirp/drivers/ic9x.py b/chirp/drivers/ic9x.py new file mode 100644 index 0000000..fc52560 --- /dev/null +++ b/chirp/drivers/ic9x.py @@ -0,0 +1,426 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import threading +import logging + +from chirp.drivers import ic9x_ll, icf +from chirp import chirp_common, errors, util, directory +from chirp import bitwise + +LOG = logging.getLogger(__name__) + +IC9XA_SPECIAL = {} +IC9XB_SPECIAL = {} + +for i in range(0, 25): + idA = "%iA" % i + idB = "%iB" % i + Anum = 800 + i * 2 + Bnum = 400 + i * 2 + + IC9XA_SPECIAL[idA] = Anum + IC9XA_SPECIAL[idB] = Bnum + + IC9XB_SPECIAL[idA] = Bnum + IC9XB_SPECIAL[idB] = Bnum + 1 + +IC9XA_SPECIAL["C0"] = IC9XB_SPECIAL["C0"] = -1 +IC9XA_SPECIAL["C1"] = IC9XB_SPECIAL["C1"] = -2 + +IC9X_SPECIAL = { + 0: {}, + 1: IC9XA_SPECIAL, + 2: IC9XB_SPECIAL, +} + +CHARSET = chirp_common.CHARSET_ALPHANUMERIC + \ + "!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~" + +LOCK = threading.Lock() + + +class IC9xBank(icf.IcomNamedBank): + """Icom 9x Bank""" + def get_name(self): + banks = self._model._radio._ic9x_get_banks() + return banks[self.index] + + def set_name(self, name): + banks = self._model._radio._ic9x_get_banks() + banks[self.index] = name + self._model._radio._ic9x_set_banks(banks) + + +@directory.register +class IC9xRadio(icf.IcomLiveRadio): + """Base class for Icom IC-9x radios""" + MODEL = "IC-91/92AD" + + _model = "ic9x" # Fake model info for detect.py + vfo = 0 + __last = 0 + _upper = 300 + + _num_banks = 26 + _bank_class = IC9xBank + + def _get_bank(self, loc): + mem = self.get_memory(loc) + return mem._bank + + def _set_bank(self, loc, bank): + mem = self.get_memory(loc) + mem._bank = bank + self.set_memory(mem) + + def _get_bank_index(self, loc): + mem = self.get_memory(loc) + return mem._bank_index + + def _set_bank_index(self, loc, index): + mem = self.get_memory(loc) + mem._bank_index = index + self.set_memory(mem) + + def __init__(self, *args, **kwargs): + icf.IcomLiveRadio.__init__(self, *args, **kwargs) + + if self.pipe: + self.pipe.timeout = 0.1 + + self.__memcache = {} + self.__bankcache = {} + + global LOCK + self._lock = LOCK + + def _maybe_send_magic(self): + if (time.time() - self.__last) > 1: + LOG.debug("Sending magic") + ic9x_ll.send_magic(self.pipe) + self.__last = time.time() + + def get_memory(self, number): + if isinstance(number, str): + try: + number = IC9X_SPECIAL[self.vfo][number] + except KeyError: + raise errors.InvalidMemoryLocation( + "Unknown channel %s" % number) + + if number < -2 or number > 999: + raise errors.InvalidValueError("Number must be between 0 and 999") + + if number in self.__memcache: + return self.__memcache[number] + + self._lock.acquire() + try: + self._maybe_send_magic() + mem = ic9x_ll.get_memory(self.pipe, self.vfo, number) + except errors.InvalidMemoryLocation: + mem = chirp_common.Memory() + mem.number = number + if number < self._upper: + mem.empty = True + except: + self._lock.release() + raise + + self._lock.release() + + if number > self._upper or number < 0: + mem.extd_number = util.get_dict_rev(IC9X_SPECIAL, + [self.vfo][number]) + mem.immutable = ["number", "skip", "bank", "bank_index", + "extd_number"] + + self.__memcache[mem.number] = mem + + return mem + + def get_raw_memory(self, number): + self._lock.acquire() + try: + ic9x_ll.send_magic(self.pipe) + mframe = ic9x_ll.get_memory_frame(self.pipe, self.vfo, number) + except: + self._lock.release() + raise + + self._lock.release() + + return repr(bitwise.parse(ic9x_ll.MEMORY_FRAME_FORMAT, mframe)) + + def get_memories(self, lo=0, hi=None): + if hi is None: + hi = self._upper + + memories = [] + + for i in range(lo, hi + 1): + try: + LOG.debug("Getting %i" % i) + mem = self.get_memory(i) + if mem: + memories.append(mem) + LOG.debug("Done: %s" % mem) + except errors.InvalidMemoryLocation: + pass + except errors.InvalidDataError as e: + LOG.error("Error talking to radio: %s" % e) + break + + return memories + + def set_memory(self, _memory): + # Make sure we mirror the DV-ness of the new memory we're + # setting, and that we capture the Bank value of any currently + # stored memory (unless the special type is provided) and + # communicate that to the low-level routines with the special + # subclass + if isinstance(_memory, ic9x_ll.IC9xMemory) or \ + isinstance(_memory, ic9x_ll.IC9xDVMemory): + memory = _memory + else: + if isinstance(_memory, chirp_common.DVMemory): + memory = ic9x_ll.IC9xDVMemory() + memory.clone(self.get_memory(_memory.number)) + else: + memory = ic9x_ll.IC9xMemory() + memory.clone(self.get_memory(_memory.number)) + + memory.clone(_memory) + + self._lock.acquire() + self._maybe_send_magic() + try: + if memory.empty: + ic9x_ll.erase_memory(self.pipe, self.vfo, memory.number) + else: + ic9x_ll.set_memory(self.pipe, self.vfo, memory) + memory = ic9x_ll.get_memory(self.pipe, self.vfo, memory.number) + except: + self._lock.release() + raise + + self._lock.release() + + self.__memcache[memory.number] = memory + + def _ic9x_get_banks(self): + if len(list(self.__bankcache.keys())) == 26: + return [self.__bankcache[k] for k in + sorted(self.__bankcache.keys())] + + self._lock.acquire() + try: + self._maybe_send_magic() + banks = ic9x_ll.get_banks(self.pipe, self.vfo) + except: + self._lock.release() + raise + + self._lock.release() + + i = 0 + for bank in banks: + self.__bankcache[i] = bank + i += 1 + + return banks + + def _ic9x_set_banks(self, banks): + + if len(banks) != len(list(self.__bankcache.keys())): + raise errors.InvalidDataError("Invalid bank list length (%i:%i)" % + (len(banks), + len(list(self.__bankcache.keys())))) + + cached_names = [str(self.__bankcache[x]) + for x in sorted(self.__bankcache.keys())] + + need_update = False + for i in range(0, 26): + if banks[i] != cached_names[i]: + need_update = True + self.__bankcache[i] = banks[i] + LOG.dbeug("Updating %s: %s -> %s" % + (chr(i + ord("A")), cached_names[i], banks[i])) + + if need_update: + self._lock.acquire() + try: + self._maybe_send_magic() + ic9x_ll.set_banks(self.pipe, self.vfo, banks) + except: + self._lock.release() + raise + + self._lock.release() + + def get_sub_devices(self): + return [IC9xRadioA(self.pipe), IC9xRadioB(self.pipe)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_sub_devices = True + rf.valid_special_chans = list(IC9X_SPECIAL[self.vfo].keys()) + + return rf + + +class IC9xRadioA(IC9xRadio): + """IC9x Band A subdevice""" + VARIANT = "Band A" + vfo = 1 + _upper = 849 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = True + rf.has_bank_index = True + rf.has_bank_names = True + rf.memory_bounds = (0, self._upper) + rf.valid_modes = ["FM", "WFM", "AM"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_duplexes = ["", "-", "+"] + rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS) + rf.valid_bands = [(500000, 9990000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_characters = CHARSET + rf.valid_name_length = 8 + return rf + + +class IC9xRadioB(IC9xRadio, chirp_common.IcomDstarSupport): + """IC9x Band B subdevice""" + VARIANT = "Band B" + vfo = 2 + _upper = 399 + + MYCALL_LIMIT = (1, 7) + URCALL_LIMIT = (1, 61) + RPTCALL_LIMIT = (1, 61) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = True + rf.has_bank_index = True + rf.has_bank_names = True + rf.requires_call_lists = False + rf.memory_bounds = (0, self._upper) + rf.valid_modes = ["FM", "NFM", "AM", "DV"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_duplexes = ["", "-", "+"] + rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS) + rf.valid_bands = [(118000000, 174000000), (350000000, 470000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_characters = CHARSET + rf.valid_name_length = 8 + return rf + + def __init__(self, *args, **kwargs): + IC9xRadio.__init__(self, *args, **kwargs) + + self.__rcalls = [] + self.__mcalls = [] + self.__ucalls = [] + + def __get_call_list(self, cache, cstype, ulimit): + if cache: + return cache + + calls = [] + + self._maybe_send_magic() + for i in range(ulimit - 1): + call = ic9x_ll.get_call(self.pipe, cstype, i+1) + calls.append(call) + + return calls + + def __set_call_list(self, cache, cstype, ulimit, calls): + for i in range(ulimit - 1): + blank = " " * 8 + + try: + acall = cache[i] + except IndexError: + acall = blank + + try: + bcall = calls[i] + except IndexError: + bcall = blank + + if acall == bcall: + continue # No change to this one + + self._maybe_send_magic() + ic9x_ll.set_call(self.pipe, cstype, i + 1, calls[i]) + + return calls + + def get_mycall_list(self): + self.__mcalls = self.__get_call_list(self.__mcalls, + ic9x_ll.IC92MyCallsignFrame, + self.MYCALL_LIMIT[1]) + return self.__mcalls + + def get_urcall_list(self): + self.__ucalls = self.__get_call_list(self.__ucalls, + ic9x_ll.IC92YourCallsignFrame, + self.URCALL_LIMIT[1]) + return self.__ucalls + + def get_repeater_call_list(self): + self.__rcalls = self.__get_call_list(self.__rcalls, + ic9x_ll.IC92RepeaterCallsignFrame, + self.RPTCALL_LIMIT[1]) + return self.__rcalls + + def set_mycall_list(self, calls): + self.__mcalls = self.__set_call_list(self.__mcalls, + ic9x_ll.IC92MyCallsignFrame, + self.MYCALL_LIMIT[1], + calls) + + def set_urcall_list(self, calls): + self.__ucalls = self.__set_call_list(self.__ucalls, + ic9x_ll.IC92YourCallsignFrame, + self.URCALL_LIMIT[1], + calls) + + def set_repeater_call_list(self, calls): + self.__rcalls = self.__set_call_list(self.__rcalls, + ic9x_ll.IC92RepeaterCallsignFrame, + self.RPTCALL_LIMIT[1], + calls) + + +def _test(): + import serial + ser = IC9xRadioB(serial.Serial(port="/dev/ttyUSB1", + baudrate=38400, timeout=0.1)) + print(ser.get_urcall_list()) + print("-- FOO --") + ser.set_urcall_list(["K7TAY", "FOOBAR", "BAZ"]) + + +if __name__ == "__main__": + _test() diff --git a/chirp/drivers/ic9x_icf.py b/chirp/drivers/ic9x_icf.py new file mode 100644 index 0000000..3e3e3d5 --- /dev/null +++ b/chirp/drivers/ic9x_icf.py @@ -0,0 +1,81 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf, ic9x_icf_ll +from chirp import chirp_common, util, directory, errors + + +# Don't register as this module is used to load icf file from File-Open menu +# see do_open in mainapp.py +class IC9xICFRadio(chirp_common.CloneModeRadio): + VENDOR = "Icom" + MODEL = "IC-91/92AD" + VARIANT = "ICF File" + _model = None + + _upper = 1200 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.memory_bounds = (0, self._upper) + rf.has_sub_devices = True + rf.valid_modes = ["FM", "AM"] + if "A" in self.VARIANT: + rf.valid_modes.append("WFM") + else: + rf.valid_modes.append("DV") + rf.valid_modes.append("NFM") + return rf + + def get_raw_memory(self, number): + raw = ic9x_icf_ll.get_raw_memory(self._mmap, number).get_packed() + return util.hexprint(raw) + + def get_memory(self, number): + return ic9x_icf_ll.get_memory(self._mmap, number) + + def load_mmap(self, filename): + _mdata, self._mmap = icf.read_file(filename) + + def get_sub_devices(self): + return [IC9xICFRadioA(self._mmap), + IC9xICFRadioB(self._mmap)] + + +class IC9xICFRadioA(IC9xICFRadio): + VARIANT = "ICF File Band A" + + _upper = 800 + + def get_memory(self, number): + if number > self._upper: + raise errors.InvalidMemoryLocation("Number must be <800") + + return ic9x_icf_ll.get_memory(self._mmap, number) + + +class IC9xICFRadioB(IC9xICFRadio): + VARIANT = "ICF File Band B" + + _upper = 400 + + def get_memory(self, number): + if number > self._upper: + raise errors.InvalidMemoryLocation("Number must be <400") + + mem = ic9x_icf_ll.get_memory(self._mmap, 850 + number) + mem.number = number + return mem diff --git a/chirp/drivers/ic9x_icf_ll.py b/chirp/drivers/ic9x_icf_ll.py new file mode 100644 index 0000000..e1619d5 --- /dev/null +++ b/chirp/drivers/ic9x_icf_ll.py @@ -0,0 +1,152 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +from chirp import chirp_common +from chirp.memmap import MemoryMap + +MEM_LOC_SIZE_A = 20 +MEM_LOC_SIZE_B = MEM_LOC_SIZE_A + 1 + (3 * 8) + +POS_FREQ = 0 +POS_OFFSET = 3 +POS_TONE = 5 +POS_MODE = 6 +POS_DTCS = 7 +POS_TS = 8 +POS_DTCSPOL = 11 +POS_DUPLEX = 11 +POS_NAME = 12 + + +def get_mem_offset(number): + """Get the offset into the memory map for memory @number""" + if number < 850: + return MEM_LOC_SIZE_A * number + else: + return (MEM_LOC_SIZE_A * 850) + (MEM_LOC_SIZE_B * (number - 850)) + + +def get_raw_memory(mmap, number): + """Return a raw representation of memory @number""" + offset = get_mem_offset(number) + if number >= 850: + size = MEM_LOC_SIZE_B + else: + size = MEM_LOC_SIZE_A + return MemoryMap(mmap[offset:offset+size]) + + +def get_freq(mmap): + """Return the memory frequency""" + if ord(mmap[10]) & 0x10: + mult = 6250 + else: + mult = 5000 + val, = struct.unpack(">I", "\x00" + mmap[POS_FREQ:POS_FREQ+3]) + return val * mult + + +def get_offset(mmap): + """Return the memory offset""" + val, = struct.unpack(">H", mmap[POS_OFFSET:POS_OFFSET+2]) + return val * 5000 + + +def get_rtone(mmap): + """Return the memory rtone""" + val = (ord(mmap[POS_TONE]) & 0xFC) >> 2 + return chirp_common.TONES[val] + + +def get_ctone(mmap): + """Return the memory ctone""" + val = (ord(mmap[POS_TONE]) & 0x03) | ((ord(mmap[POS_TONE+1]) & 0xF0) >> 4) + return chirp_common.TONES[val] + + +def get_dtcs(mmap): + """Return the memory dtcs value""" + val = ord(mmap[POS_DTCS]) >> 1 + return chirp_common.DTCS_CODES[val] + + +def get_mode(mmap): + """Return the memory mode""" + val = ord(mmap[POS_MODE]) & 0x07 + + modemap = ["FM", "NFM", "WFM", "AM", "DV", "FM"] + + return modemap[val] + + +def get_ts(mmap): + """Return the memory tuning step""" + val = (ord(mmap[POS_TS]) & 0xF0) >> 4 + if val == 14: + return 5.0 # Coerce "Auto" to 5.0 + + icf_ts = list(chirp_common.TUNING_STEPS) + icf_ts.insert(2, 8.33) + icf_ts.insert(3, 9.00) + icf_ts.append(100.0) + icf_ts.append(125.0) + icf_ts.append(200.0) + + return icf_ts[val] + + +def get_dtcs_polarity(mmap): + """Return the memory dtcs polarity""" + val = (ord(mmap[POS_DTCSPOL]) & 0x03) + + pols = ["NN", "NR", "RN", "RR"] + + return pols[val] + + +def get_duplex(mmap): + """Return the memory duplex""" + val = (ord(mmap[POS_DUPLEX]) & 0x0C) >> 2 + + dup = ["", "-", "+", ""] + + return dup[val] + + +def get_name(mmap): + """Return the memory name""" + return mmap[POS_NAME:POS_NAME+8] + + +def get_memory(_mmap, number): + """Get memory @number from global memory map @_mmap""" + mmap = get_raw_memory(_mmap, number) + mem = chirp_common.Memory() + mem.number = number + mem.freq = get_freq(mmap) + mem.offset = get_offset(mmap) + mem.rtone = get_rtone(mmap) + mem.ctone = get_ctone(mmap) + mem.dtcs = get_dtcs(mmap) + mem.mode = get_mode(mmap) + mem.tuning_step = get_ts(mmap) + mem.dtcs_polarity = get_dtcs_polarity(mmap) + mem.duplex = get_duplex(mmap) + mem.name = get_name(mmap) + + mem.empty = mem.freq == 0 + + return mem diff --git a/chirp/drivers/ic9x_ll.py b/chirp/drivers/ic9x_ll.py new file mode 100644 index 0000000..d645ecf --- /dev/null +++ b/chirp/drivers/ic9x_ll.py @@ -0,0 +1,580 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import logging + +from chirp import chirp_common, util, errors, bitwise +from chirp.memmap import MemoryMap + +LOG = logging.getLogger(__name__) + +TUNING_STEPS = [ + 5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15, 20, 25, 30, 50, 100, 125, 200 + ] + +MODES = ["FM", "NFM", "WFM", "AM", "DV"] +DUPLEX = ["", "-", "+"] +TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS"] +DTCS_POL = ["NN", "NR", "RN", "RR"] + +MEM_LEN = 34 +DV_MEM_LEN = 60 + + +# Dirty hack until I clean up this IC9x mess +class IC9xMemory(chirp_common.Memory): + """A dirty hack to stash bank information in a memory""" + _bank = None + _bank_index = 0 + + def __init__(self): + chirp_common.Memory.__init__(self) + + +class IC9xDVMemory(chirp_common.DVMemory): + """See above dirty hack""" + _bank = None + _bank_index = 0 + + def __init__(self): + chirp_common.DVMemory.__init__(self) + + +def _ic9x_parse_frames(buf): + frames = [] + + while "\xfe\xfe" in buf: + try: + start = buf.index("\xfe\xfe") + end = buf[start:].index("\xfd") + start + 1 + except Exception as e: + LOG.error("No trailing bit") + break + + framedata = buf[start:end] + buf = buf[end:] + + try: + frame = IC92Frame() + frame.from_raw(framedata[2:-1]) + frames.append(frame) + except errors.InvalidDataError as e: + LOG.error("Broken frame: %s" % e) + + # LOG.debug("Parsed %i frames" % len(frames)) + + return frames + + +def ic9x_send(pipe, buf): + """Send @buf to @pipe, wrapped in a header and trailer. Attempt to read + any response frames, which are returned as a list""" + + # Add header and trailer + realbuf = "\xfe\xfe" + buf + "\xfd" + + # LOG.debug("Sending:\n%s" % util.hexprint(realbuf)) + + pipe.write(realbuf) + pipe.flush() + + data = "" + while True: + buf = pipe.read(4096) + if not buf: + break + + data += buf + + return _ic9x_parse_frames(data) + + +class IC92Frame: + """IC9x frame base class""" + def get_vfo(self): + """Return the vfo number""" + return ord(self._map[0]) + + def set_vfo(self, vfo): + """Set the vfo number""" + self._map[0] = chr(vfo) + + def from_raw(self, data): + """Construct the frame from raw data""" + self._map = MemoryMap(data) + + def from_frame(self, frame): + """Construct the frame by copying another frame""" + self._map = MemoryMap(frame.get_raw()) + + def __init__(self, subcmd=0, flen=0, cmd=0x1A): + self._map = MemoryMap("\x00" * (4 + flen)) + self._map[0] = "\x01\x80" + chr(cmd) + chr(subcmd) + + def get_payload(self): + """Return the entire payload (sans header)""" + return self._map[4:] + + def get_raw(self): + """Return the raw version of the frame""" + return self._map.get_packed() + + def __str__(self): + string = "Frame VFO=%i (len = %i)\n" % (self.get_vfo(), + len(self.get_payload())) + string += util.hexprint(self.get_payload()) + string += "\n" + + return string + + def send(self, pipe, verbose=False): + """Send the frame to the radio via @pipe""" + if verbose: + LOG.debug("Sending:\n%s" % util.hexprint(self.get_raw())) + + response = ic9x_send(pipe, self.get_raw()) + + if len(response) == 0: + raise errors.InvalidDataError("No response from radio") + + return response[0] + + def __setitem__(self, start, value): + self._map[start+4] = value + + def __getitem__(self, index): + return self._map[index+4] + + def __getslice__(self, start, end): + return self._map[start+4:end+4] + + +class IC92GetBankFrame(IC92Frame): + """A frame for requesting bank information""" + def __init__(self): + IC92Frame.__init__(self, 0x09) + + def send(self, pipe, verbose=False): + rframes = ic9x_send(pipe, self.get_raw()) + + if len(rframes) == 0: + raise errors.InvalidDataError("No response from radio") + + return rframes + + +class IC92BankFrame(IC92Frame): + """A frame for bank information""" + def __init__(self): + # 1 byte for identifier + # 8 bytes for name + IC92Frame.__init__(self, 0x0B, 9) + + def get_name(self): + """Return the bank name""" + return self[1:] + + def get_identifier(self): + """Return the letter for the bank (A-Z)""" + return self[0] + + def set_name(self, name): + """Set the bank name""" + self[1] = name[:8].ljust(8) + + def set_identifier(self, ident): + """Set the letter for the bank (A-Z)""" + self[0] = ident[0] + + +class IC92MemClearFrame(IC92Frame): + """A frame for clearing (erasing) a memory""" + def __init__(self, loc): + # 2 bytes for location + # 1 byte for 0xFF + IC92Frame.__init__(self, 0x00, 4) + + self[0] = struct.pack(">BHB", 1, int("%i" % loc, 16), 0xFF) + + +class IC92MemGetFrame(IC92Frame): + """A frame for requesting a memory""" + def __init__(self, loc, iscall=False): + # 2 bytes for location + IC92Frame.__init__(self, 0x00, 3) + + if iscall: + call = 2 + else: + call = 1 + + self[0] = struct.pack(">BH", call, int("%i" % loc, 16)) + + +class IC92GetCallsignFrame(IC92Frame): + """A frame for getting callsign information""" + def __init__(self, calltype, number): + IC92Frame.__init__(self, calltype, 1, 0x1D) + + self[0] = chr(number) + + +class IC92CallsignFrame(IC92Frame): + """A frame to communicate callsign information""" + command = 0 # Invalid + width = 8 + + def __init__(self, number=0, callsign=""): + # 1 byte for index + # $width bytes for callsign + IC92Frame.__init__(self, self.command, self.width+1, 0x1D) + + self[0] = chr(number) + callsign[:self.width].ljust(self.width) + + def get_callsign(self): + """Return the actual callsign""" + return self[1:self.width+1].rstrip() + + +class IC92YourCallsignFrame(IC92CallsignFrame): + """URCALL frame""" + command = 6 # Your + + +class IC92RepeaterCallsignFrame(IC92CallsignFrame): + """RPTCALL frame""" + command = 7 # Repeater + + +class IC92MyCallsignFrame(IC92CallsignFrame): + """MYCALL frame""" + command = 8 # My + width = 12 # 4 bytes for /STID + +MEMORY_FRAME_FORMAT = """ +struct { + u8 vfo; + bbcd number[2]; + lbcd freq[5]; + lbcd offset[4]; + u8 unknown8; + bbcd rtone[2]; + bbcd ctone[2]; + bbcd dtcs[2]; + u8 unknown9[2]; + u8 unknown2:1, + mode:3, + tuning_step:4; + u8 unknown1:3, + tmode: 3, + duplex: 2; + u8 unknown5:4, + dtcs_polarity:2, + pskip:1, + skip:1; + char bank; + bbcd bank_index[1]; + char name[8]; + u8 unknown10; + u8 digital_code; + char rpt2call[8]; + char rpt1call[8]; + char urcall[8]; +} mem[1]; +""" + + +class IC92MemoryFrame(IC92Frame): + """A frame for communicating memory information""" + def __init__(self): + IC92Frame.__init__(self, 0, DV_MEM_LEN) + + # For good measure, here is a whole, valid memory block + # at 146.010 FM. Since the 9x will complain if any bits + # are invalid, it's easiest to start with a known-good one + # since we don't set everything. + self[0] = \ + "\x01\x00\x03\x00\x00\x01\x46\x01" + \ + "\x00\x00\x60\x00\x00\x08\x85\x08" + \ + "\x85\x00\x23\x22\x80\x06\x00\x00" + \ + "\x00\x00\x20\x20\x20\x20\x20\x20" + \ + "\x20\x20\x00\x00\x20\x20\x20\x20" + \ + "\x20\x20\x20\x20\x4b\x44\x37\x52" + \ + "\x45\x58\x20\x43\x43\x51\x43\x51" + \ + "\x43\x51\x20\x20" + + def set_vfo(self, vfo): + IC92Frame.set_vfo(self, vfo) + if vfo == 1: + self._map.truncate(MEM_LEN + 4) + + def set_iscall(self, iscall): + """This frame refers to a call channel if @iscall is True""" + if iscall: + self[0] = 2 + else: + self[0] = 1 + + def get_iscall(self): + """Return True if this frame refers to a call channel""" + return ord(self[0]) == 2 + + def set_memory(self, mem): + """Take Memory object @mem and configure the frame accordingly""" + if mem.number < 0: + self.set_iscall(True) + mem.number = abs(mem.number) - 1 + LOG.debug("Memory is %i (call %s)" % + (mem.number, self.get_iscall())) + + _mem = bitwise.parse(MEMORY_FRAME_FORMAT, self).mem + + _mem.number = mem.number + + _mem.freq = mem.freq + _mem.offset = mem.offset + _mem.rtone = int(mem.rtone * 10) + _mem.ctone = int(mem.ctone * 10) + _mem.dtcs = int(mem.dtcs) + _mem.mode = MODES.index(mem.mode) + _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.tmode = TMODES.index(mem.tmode) + _mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity) + + if mem._bank is not None: + _mem.bank = chr(ord("A") + mem._bank) + _mem.bank_index = mem._bank_index + + _mem.skip = mem.skip == "S" + _mem.pskip = mem.skip == "P" + + _mem.name = mem.name.ljust(8)[:8] + + if mem.mode == "DV": + _mem.urcall = mem.dv_urcall.upper().ljust(8)[:8] + _mem.rpt1call = mem.dv_rpt1call.upper().ljust(8)[:8] + _mem.rpt2call = mem.dv_rpt2call.upper().ljust(8)[:8] + _mem.digital_code = mem.dv_code + + def get_memory(self): + """Return a Memory object based on the contents of the frame""" + _mem = bitwise.parse(MEMORY_FRAME_FORMAT, self).mem + + if MODES[_mem.mode] == "DV": + mem = IC9xDVMemory() + else: + mem = IC9xMemory() + + mem.number = int(_mem.number) + if self.get_iscall(): + mem.number = -1 - mem.number + + mem.freq = int(_mem.freq) + mem.offset = int(_mem.offset) + mem.rtone = int(_mem.rtone) / 10.0 + mem.ctone = int(_mem.ctone) / 10.0 + mem.dtcs = int(_mem.dtcs) + mem.mode = MODES[int(_mem.mode)] + mem.tuning_step = TUNING_STEPS[int(_mem.tuning_step)] + mem.duplex = DUPLEX[int(_mem.duplex)] + mem.tmode = TMODES[int(_mem.tmode)] + mem.dtcs_polarity = DTCS_POL[int(_mem.dtcs_polarity)] + + if int(_mem.bank) != 0: + mem._bank = ord(str(_mem.bank)) - ord("A") + mem._bank_index = int(_mem.bank_index) + + if _mem.skip: + mem.skip = "S" + elif _mem.pskip: + mem.skip = "P" + else: + mem.skip = "" + + mem.name = str(_mem.name).rstrip() + + if mem.mode == "DV": + mem.dv_urcall = str(_mem.urcall).rstrip() + mem.dv_rpt1call = str(_mem.rpt1call).rstrip() + mem.dv_rpt2call = str(_mem.rpt2call).rstrip() + mem.dv_code = int(_mem.digital_code) + + return mem + + +def _send_magic_4800(pipe): + cmd = "\x01\x80\x19" + magic = ("\xFE" * 25) + cmd + for _i in [0, 1]: + resp = ic9x_send(pipe, magic) + if resp: + return resp[0].get_raw()[0] == "\x80" + return True + + +def _send_magic_38400(pipe): + cmd = "\x01\x80\x19" + # rsp = "\x80\x01\x19" + magic = ("\xFE" * 400) + cmd + for _i in [0, 1]: + resp = ic9x_send(pipe, magic) + if resp: + return resp[0].get_raw()[0] == "\x80" + return False + + +def send_magic(pipe): + """Send the magic incantation to wake up an ic9x radio""" + if pipe.baudrate == 38400: + resp = _send_magic_38400(pipe) + if resp: + return + LOG.info("Switching from 38400 to 4800") + pipe.baudrate = 4800 + resp = _send_magic_4800(pipe) + pipe.baudrate = 38400 + if resp: + return + raise errors.RadioError("Radio not responding") + elif pipe.baudrate == 4800: + resp = _send_magic_4800(pipe) + if resp: + return + LOG.info("Switching from 4800 to 38400") + pipe.baudrate = 38400 + resp = _send_magic_38400(pipe) + if resp: + return + pipe.baudrate = 4800 + raise errors.RadioError("Radio not responding") + else: + raise errors.InvalidDataError("Radio in unknown state (%i)" % + pipe.baudrate) + + +def get_memory_frame(pipe, vfo, number): + """Get the memory frame for @vfo and @number via @pipe""" + if number < 0: + number = abs(number + 1) + call = True + else: + call = False + + frame = IC92MemGetFrame(number, call) + frame.set_vfo(vfo) + + return frame.send(pipe) + + +def get_memory(pipe, vfo, number): + """Get a memory object for @vfo and @number via @pipe""" + rframe = get_memory_frame(pipe, vfo, number) + + if len(rframe.get_payload()) < 1: + raise errors.InvalidMemoryLocation("No response from radio") + + if rframe.get_payload()[3] == '\xff': + raise errors.InvalidMemoryLocation("Radio says location is empty") + + mf = IC92MemoryFrame() + mf.from_frame(rframe) + + return mf.get_memory() + + +def set_memory(pipe, vfo, memory): + """Set memory @memory on @vfo via @pipe""" + frame = IC92MemoryFrame() + frame.set_memory(memory) + frame.set_vfo(vfo) + + # LOG.debug("Sending (%i):" % (len(frame.get_raw()))) + # LOG.debug(util.hexprint(frame.get_raw())) + + rframe = frame.send(pipe) + + if rframe.get_raw()[2] != "\xfb": + raise errors.InvalidDataError("Radio reported error:\n%s" % + util.hexprint(rframe.get_payload())) + + +def erase_memory(pipe, vfo, number): + """Erase memory @number on @vfo via @pipe""" + frame = IC92MemClearFrame(number) + frame.set_vfo(vfo) + + rframe = frame.send(pipe) + if rframe.get_raw()[2] != "\xfb": + raise errors.InvalidDataError("Radio reported error") + + +def get_banks(pipe, vfo): + """Get banks for @vfo via @pipe""" + frame = IC92GetBankFrame() + frame.set_vfo(vfo) + + rframes = frame.send(pipe) + + if vfo == 1: + base = 180 + else: + base = 237 + + banks = [] + + for i in range(base, base+26): + bframe = IC92BankFrame() + bframe.from_frame(rframes[i]) + + banks.append(bframe.get_name().rstrip()) + + return banks + + +def set_banks(pipe, vfo, banks): + """Set banks for @vfo via @pipe""" + for i in range(0, 26): + bframe = IC92BankFrame() + bframe.set_vfo(vfo) + bframe.set_identifier(chr(i + ord("A"))) + bframe.set_name(banks[i]) + + rframe = bframe.send(pipe) + if rframe.get_payload() != "\xfb": + raise errors.InvalidDataError("Radio reported error") + + +def get_call(pipe, cstype, number): + """Get @cstype callsign @number via @pipe""" + cframe = IC92GetCallsignFrame(cstype.command, number) + cframe.set_vfo(2) + rframe = cframe.send(pipe) + + cframe = IC92CallsignFrame() + cframe.from_frame(rframe) + + return cframe.get_callsign() + + +def set_call(pipe, cstype, number, call): + """Set @cstype @call at position @number via @pipe""" + cframe = cstype(number, call) + cframe.set_vfo(2) + rframe = cframe.send(pipe) + + if rframe.get_payload() != "\xfb": + raise errors.RadioError("Radio reported error") diff --git a/chirp/drivers/icf.py b/chirp/drivers/icf.py new file mode 100644 index 0000000..6d702db --- /dev/null +++ b/chirp/drivers/icf.py @@ -0,0 +1,815 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import re +import time +import logging +import struct + +from chirp import bitwise +from chirp import chirp_common, errors, util, memmap +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettings + +LOG = logging.getLogger(__name__) + +CMD_CLONE_OUT = 0xE2 +CMD_CLONE_IN = 0xE3 +CMD_CLONE_DAT = 0xE4 +CMD_CLONE_END = 0xE5 + +SAVE_PIPE = None + + +class IcfFrame: + """A single ICF communication frame""" + src = 0 + dst = 0 + cmd = 0 + + payload = "" + + def __str__(self): + addrs = {0xEE: "PC", + 0xEF: "Radio"} + cmds = {0xE0: "ID", + 0xE1: "Model", + 0xE2: "Clone out", + 0xE3: "Clone in", + 0xE4: "Clone data", + 0xE5: "Clone end", + 0xE6: "Clone result"} + + return "%s -> %s [%s]:\n%s" % (addrs[self.src], addrs[self.dst], + cmds[self.cmd], + util.hexprint(self.payload)) + + def __init__(self): + pass + + +def parse_frame_generic(data): + """Parse an ICF frame of unknown type from the beginning of @data""" + frame = IcfFrame() + + assert isinstance(data, bytes), ('parse_frame_generic() expected bytes, ' + 'but got %s' % data.__class__) + + frame.src = data[2] + frame.dst = data[3] + frame.cmd = data[4] + + try: + end = data.index(0xFD) + except ValueError as e: + return None, data + + frame.payload = data[5:end] + + return frame, data[end+1:] + + +class RadioStream: + """A class to make reading a stream of IcfFrames easier""" + def __init__(self, pipe): + self.pipe = pipe + self.data = bytes() + + def _process_frames(self): + if not self.data.startswith(b"\xFE\xFE"): + LOG.error("Out of sync with radio:\n%s" % util.hexprint(self.data)) + raise errors.InvalidDataError("Out of sync with radio") + elif len(self.data) < 5: + return [] # Not enough data for a full frame + + frames = [] + + while self.data: + try: + cmd = self.data[4] + except IndexError: + break # Out of data + + try: + frame, rest = parse_frame_generic(self.data) + if not frame: + break + elif frame.src == 0xEE and frame.dst == 0xEF: + # PC echo, ignore + pass + else: + frames.append(frame) + + self.data = bytes(rest) + except errors.InvalidDataError as e: + LOG.error("Failed to parse frame (cmd=%i): %s" % (cmd, e)) + return [] + + return frames + + def get_frames(self, nolimit=False): + """Read any pending frames from the stream""" + while True: + _data = self.pipe.read(64) + if not _data: + break + else: + self.data += _data + + if not nolimit and len(self.data) > 128 and 0xFD in self.data: + break # Give us a chance to do some status + if len(self.data) > 1024: + break # Avoid an endless loop of chewing garbage + + if not self.data: + return [] + + return self._process_frames() + + +def get_model_data(radio, mdata=bytes(b"\x00\x00\x00\x00")): + """Query the @radio for its model data""" + send_clone_frame(radio, 0xe0, mdata, raw=True) + + stream = RadioStream(radio.pipe) + frames = stream.get_frames() + + if len(frames) != 1: + raise errors.RadioError("Unexpected response from radio") + + return frames[0].payload + + +def get_clone_resp(pipe, length=None, max_count=None): + """Read the response to a clone frame""" + def exit_criteria(buf, length, cnt, max_count): + """Stop reading a clone response if we have enough data or encounter + the end of a frame""" + if max_count is not None: + if cnt >= max_count: + return True + if length is None: + return buf.endswith("\xfd") + else: + return len(buf) == length + + resp = "" + cnt = 0 + while not exit_criteria(resp, length, cnt, max_count): + resp += pipe.read(1) + cnt += 1 + return resp + + +def send_clone_frame(radio, cmd, data, raw=False, checksum=False): + """Send a clone frame with @cmd and @data to the @radio""" + payload = radio.get_payload(bytes(data), raw, checksum) + + frame = (bytes(b"\xfe\xfe\xee\xef") + + bytes([cmd]) + + payload + + bytes(b"\xfd")) + + if SAVE_PIPE: + LOG.debug("Saving data...") + SAVE_PIPE.write(frame) + + # LOG.debug("Sending:\n%s" % util.hexprint(frame)) + # LOG.debug("Sending:\n%s" % util.hexprint(hed[6:])) + if cmd == 0xe4: + # Uncomment to avoid cloning to the radio + # return frame + pass + + radio.pipe.write(frame) + if radio.MUNCH_CLONE_RESP: + # Do max 2*len(frame) read(1) calls + get_clone_resp(radio.pipe, max_count=2*len(frame)) + + return frame + + +def process_data_frame(radio, frame, _mmap): + """Process a data frame, adding the payload to @_mmap""" + _data = radio.process_frame_payload(frame.payload) + + # NOTE: On the _data[N:N+1] below. Because: + # - on py2 bytes[N] is a bytes + # - on py3 bytes[N] is an int + # - on both bytes[N:M] is a bytes + # So we do a slice so we get consistent behavior + # Checksum logic added by Rick DeWitt, 9/2019, issue # 7075 + if len(_mmap) >= 0x10000: # This map size not tested for checksum + saddr, = struct.unpack(">I", _data[0:4]) + length, = struct.unpack("B", _data[4:5]) + data = _data[5:5+length] + sumc, = struct.unpack("B", _data[5+length:]) + addr1, = struct.unpack("B", _data[0:1]) + addr2, = struct.unpack("B", _data[1:2]) + addr3, = struct.unpack("B", _data[2:3]) + addr4, = struct.unpack("B", _data[3:4]) + else: # But this one has been tested for raw mode radio (IC-2730) + saddr, = struct.unpack(">H", _data[0:2]) + length, = struct.unpack("B", _data[2:3]) + data = _data[3:3+length] + sumc, = struct.unpack("B", _data[3+length:]) + addr1, = struct.unpack("B", _data[0:1]) + addr2, = struct.unpack("B", _data[1:2]) + addr3 = 0 + addr4 = 0 + + cs = addr1 + addr2 + addr3 + addr4 + length + for byte in data: + cs += byte + vx = ((cs ^ 0xFFFF) + 1) & 0xFF + if sumc != vx: + LOG.error("Bad checksum in address %04X frame: %02x " + "calculated, %02x sent!" % (saddr, vx, sumc)) + raise errors.InvalidDataError( + "Checksum error in download! " + "Try disabling High Speed Clone option in Settings.") + try: + _mmap[saddr] = data + except IndexError: + LOG.error("Error trying to set %i bytes at %05x (max %05x)" % + (length, saddr, len(_mmap))) + return saddr, saddr + length + + +def start_hispeed_clone(radio, cmd): + """Send the magic incantation to the radio to go fast""" + buf = ((bytes(b"\xFE") * 20) + + bytes(b"\xEE\xEF\xE8") + + radio.get_model() + + bytes(b"\x00\x00\x02\x01\xFD")) + LOG.debug("Starting HiSpeed:\n%s" % util.hexprint(buf)) + radio.pipe.write(buf) + radio.pipe.flush() + resp = radio.pipe.read(128) + LOG.debug("Response:\n%s" % util.hexprint(resp)) + + LOG.info("Switching to 38400 baud") + radio.pipe.baudrate = 38400 + + buf = ((bytes(b"\xFE") * 14) + + bytes(b"\xEE\xEF") + + bytes([cmd]) + + radio.get_model()[:3] + + bytes(b"\x00\xFD")) + LOG.debug("Starting HiSpeed Clone:\n%s" % util.hexprint(buf)) + radio.pipe.write(buf) + radio.pipe.flush() + + +def _clone_from_radio(radio): + md = get_model_data(radio) + + if md[0:4] != radio.get_model(): + LOG.info("This model: %s" % util.hexprint(md[0:4])) + LOG.info("Supp model: %s" % util.hexprint(radio.get_model())) + raise errors.RadioError("I can't talk to this model") + + if radio.is_hispeed(): + start_hispeed_clone(radio, CMD_CLONE_OUT) + else: + send_clone_frame(radio, CMD_CLONE_OUT, + radio.get_model(), + raw=True) + + LOG.debug("Sent clone frame") + + stream = RadioStream(radio.pipe) + + addr = 0 + _mmap = memmap.MemoryMapBytes(bytes(b'\x00') * radio.get_memsize()) + last_size = 0 + while True: + frames = stream.get_frames() + if not frames: + break + + for frame in frames: + if frame.cmd == CMD_CLONE_DAT: + src, dst = process_data_frame(radio, frame, _mmap) + if last_size != (dst - src): + LOG.debug("ICF Size change from %i to %i at %04x" % + (last_size, dst - src, src)) + last_size = dst - src + if addr != src: + LOG.debug("ICF GAP %04x - %04x" % (addr, src)) + addr = dst + elif frame.cmd == CMD_CLONE_END: + LOG.debug("End frame (%i):\n%s" % + (len(frame.payload), util.hexprint(frame.payload))) + LOG.debug("Last addr: %04x" % addr) + + if radio.status_fn: + status = chirp_common.Status() + status.msg = "Cloning from radio" + status.max = radio.get_memsize() + status.cur = addr + radio.status_fn(status) + + return _mmap + + +def clone_from_radio(radio): + """Do a full clone out of the radio's memory""" + try: + return _clone_from_radio(radio) + except Exception as e: + raise errors.RadioError("Failed to communicate with the radio: %s" % e) + + +def send_mem_chunk(radio, start, stop, bs=32): + """Send a single chunk of the radio's memory from @start-@stop""" + _mmap = radio.get_mmap().get_byte_compatible() + + status = chirp_common.Status() + status.msg = "Cloning to radio" + status.max = radio.get_memsize() + + for i in range(start, stop, bs): + if i + bs < stop: + size = bs + else: + size = stop - i + + if radio.get_memsize() >= 0x10000: + chunk = struct.pack(">IB", i, size) + else: + chunk = struct.pack(">HB", i, size) + chunk += _mmap[i:i+size] + + send_clone_frame(radio, + CMD_CLONE_DAT, + chunk, + raw=False, + checksum=True) + + if radio.status_fn: + status.cur = i+bs + radio.status_fn(status) + + return True + + +def _clone_to_radio(radio): + global SAVE_PIPE + + # Uncomment to save out a capture of what we actually write to the radio + # SAVE_PIPE = file("pipe_capture.log", "w", 0) + + md = get_model_data(radio) + + if md[0:4] != radio.get_model(): + raise errors.RadioError("I can't talk to this model") + + # This mimics what the Icom software does, but isn't required and just + # takes longer + # md = get_model_data(radio, mdata=md[0:2]+"\x00\x00") + # md = get_model_data(radio, mdata=md[0:2]+"\x00\x00") + + stream = RadioStream(radio.pipe) + + if radio.is_hispeed(): + start_hispeed_clone(radio, CMD_CLONE_IN) + else: + send_clone_frame(radio, CMD_CLONE_IN, + radio.get_model(), + raw=True) + + frames = [] + + for start, stop, bs in radio.get_ranges(): + if not send_mem_chunk(radio, start, stop, bs): + break + frames += stream.get_frames() + + send_clone_frame(radio, CMD_CLONE_END, + radio.get_endframe(), + raw=True) + + if SAVE_PIPE: + SAVE_PIPE.close() + SAVE_PIPE = None + + for i in range(0, 10): + try: + frames += stream.get_frames(True) + result = frames[-1] + except IndexError: + LOG.debug("Waiting for clone result...") + time.sleep(0.5) + + if len(frames) == 0: + raise errors.RadioError("Did not get clone result from radio") + + return result.payload[0] == bytes(b'\x00') + + +def clone_to_radio(radio): + """Initiate a full memory clone out to @radio""" + try: + return _clone_to_radio(radio) + except Exception as e: + logging.exception("Failed to communicate with the radio") + raise errors.RadioError("Failed to communicate with the radio: %s" % e) + + +def convert_model(mod_str): + """Convert an ICF-style model string into what we get from the radio""" + data = "" + for i in range(0, len(mod_str), 2): + hexval = mod_str[i:i+2] + intval = int(hexval, 16) + data += chr(intval) + + return data + + +def convert_data_line(line): + """Convert an ICF data line to raw memory format""" + if line.startswith("#"): + return "" + + line = line.strip() + + if len(line) == 38: + # Small memory (< 0x10000) + size = int(line[4:6], 16) + data = line[6:] + else: + # Large memory (>= 0x10000) + size = int(line[8:10], 16) + data = line[10:] + + _mmap = "" + i = 0 + while i < (size * 2): + try: + val = int("%s%s" % (data[i], data[i+1]), 16) + i += 2 + _mmap += struct.pack("B", val) + except ValueError as e: + LOG.debug("Failed to parse byte: %s" % e) + break + + return _mmap + + +def read_file(filename): + """Read an ICF file and return the model string and memory data""" + f = file(filename) + + mod_str = f.readline() + dat = f.readlines() + + model = convert_model(mod_str.strip()) + + _mmap = "" + for line in dat: + if not line.startswith("#"): + _mmap += convert_data_line(line) + + return model, memmap.MemoryMap(_mmap) + + +def is_9x_icf(filename): + """Returns True if @filename is an IC9x ICF file""" + try: + with open(filename) as f: + mdata = f.read(8) + except UnicodeDecodeError: + # ICF files are ASCII, so any unicode failure means no. + return False + + return mdata in ["30660000", "28880000"] + + +def is_icf_file(filename): + """Returns True if @filename is an ICF file""" + try: + with open(filename) as f: + data = f.readline() + data += f.readline() + except UnicodeDecodeError: + # ICF files are ASCII, so any unicode failure means no. + return False + + data = data.replace("\n", "").replace("\r", "") + + return bool(re.match("^[0-9]{8}#", data)) + + +class IcomBank(chirp_common.Bank): + """A bank that works for all Icom radios""" + # Integral index of the bank (not to be confused with per-memory + # bank indexes + index = 0 + + +class IcomNamedBank(IcomBank): + """A bank with an adjustable name""" + def set_name(self, name): + """Set the name of the bank""" + pass + + +class IcomBankModel(chirp_common.BankModel): + """Icom radios all have pretty much the same simple bank model. This + central implementation can, with a few icom-specific radio interfaces + serve most/all of them""" + + def get_num_mappings(self): + return self._radio._num_banks + + def get_mappings(self): + banks = [] + + for i in range(0, self._radio._num_banks): + index = chr(ord("A") + i) + bank = self._radio._bank_class(self, index, "BANK-%s" % index) + bank.index = i + banks.append(bank) + return banks + + def add_memory_to_mapping(self, memory, bank): + self._radio._set_bank(memory.number, bank.index) + + def remove_memory_from_mapping(self, memory, bank): + if self._radio._get_bank(memory.number) != bank.index: + raise Exception("Memory %i not in bank %s. Cannot remove." % + (memory.number, bank)) + + self._radio._set_bank(memory.number, None) + + def get_mapping_memories(self, bank): + memories = [] + for i in range(*self._radio.get_features().memory_bounds): + if self._radio._get_bank(i) == bank.index: + memories.append(self._radio.get_memory(i)) + return memories + + def get_memory_mappings(self, memory): + index = self._radio._get_bank(memory.number) + if index is None: + return [] + else: + return [self.get_mappings()[index]] + + +class IcomIndexedBankModel(IcomBankModel, + chirp_common.MappingModelIndexInterface): + """Generic bank model for Icom radios with indexed banks""" + def get_index_bounds(self): + return self._radio._bank_index_bounds + + def get_memory_index(self, memory, bank): + return self._radio._get_bank_index(memory.number) + + def set_memory_index(self, memory, bank, index): + if bank not in self.get_memory_mappings(memory): + raise Exception("Memory %i is not in bank %s" % (memory.number, + bank)) + + if index not in list(range(*self._radio._bank_index_bounds)): + raise Exception("Invalid index") + self._radio._set_bank_index(memory.number, index) + + def get_next_mapping_index(self, bank): + indexes = [] + for i in range(*self._radio.get_features().memory_bounds): + if self._radio._get_bank(i) == bank.index: + indexes.append(self._radio._get_bank_index(i)) + + for i in range(0, 256): + if i not in indexes: + return i + + raise errors.RadioError("Out of slots in this bank") + + +def compute_checksum(data): + cs = 0 + for byte in data: + cs += byte + return ((cs ^ 0xFFFF) + 1) & 0xFF + + +class IcomCloneModeRadio(chirp_common.CloneModeRadio): + """Base class for Icom clone-mode radios""" + VENDOR = "Icom" + BAUDRATE = 9600 + NEEDS_COMPAT_SERIAL = False + # Ideally, the driver should read clone response after each clone frame + # is sent, but for some reason it hasn't behaved this way for years. + # So not to break the existing tested drivers the MUNCH_CLONE_RESP flag + # was added. It's False by default which brings the old behavior, + # i.e. clone response is not read. The expectation is that new Icom + # drivers will use MUNCH_CLONE_RESP = True and old drivers will be + # gradually migrated to this. Once all Icom drivers will use + # MUNCH_CLONE_RESP = True, this flag will be removed. + MUNCH_CLONE_RESP = False + + _model = "\x00\x00\x00\x00" # 4-byte model string + _endframe = "" # Model-unique ending frame + _ranges = [] # Ranges of the mmap to send to the radio + _num_banks = 10 # Most simple Icoms have 10 banks, A-J + _bank_index_bounds = (0, 99) + _bank_class = IcomBank + _can_hispeed = False + + @classmethod + def is_hispeed(cls): + """Returns True if the radio supports hispeed cloning""" + return cls._can_hispeed + + @classmethod + def get_model(cls): + """Returns the Icom model data for this radio""" + return bytes([ord(x) for x in cls._model]) + + @classmethod + def get_endframe(cls): + """Returns the magic clone end frame for this radio""" + return bytes([ord(x) for x in cls._endframe]) + + @classmethod + def get_ranges(cls): + """Returns the ranges this radio likes to have in a clone""" + return cls._ranges + + def process_frame_payload(self, payload): + """Convert BCD-encoded data to raw""" + bcddata = payload + data = bytes() + i = 0 + while i+1 < len(bcddata): + try: + val = int("%s%s" % (chr(bcddata[i]), chr(bcddata[i+1])), 16) + i += 2 + data += struct.pack("B", val) + except (ValueError, TypeError) as e: + LOG.error("Failed to parse byte %i (%r): %s" % (i, + bcddata[i:i+2], + e)) + break + + return data + + def get_payload(self, data, raw, checksum): + """Returns the data with optional checksum BCD-encoded for the radio""" + if raw: + return data + payload = bytes() + for byte in data: + payload += bytes(b"%02X" % byte) + if checksum: + payload += bytes(b"%02X" % compute_checksum(data)) + return payload + + def sync_in(self): + self._mmap = clone_from_radio(self) + self.process_mmap() + + def sync_out(self): + clone_to_radio(self) + + def get_bank_model(self): + rf = self.get_features() + if rf.has_bank: + if rf.has_bank_index: + return IcomIndexedBankModel(self) + else: + return IcomBankModel(self) + else: + return None + + # Icom-specific bank routines + def _get_bank(self, loc): + """Get the integral bank index of memory @loc, or None""" + raise Exception("Not implemented") + + def _set_bank(self, loc, index): + """Set the integral bank index of memory @loc to @index, or + no bank if None""" + raise Exception("Not implemented") + + def get_settings(self): + return make_speed_switch_setting(self) + + def set_settings(self, settings): + return honor_speed_switch_setting(self, settings) + + +def flip_high_order_bit(data): + return [chr(ord(d) ^ 0x80) for d in list(data)] + + +def escape_raw_byte(byte): + """Escapes a raw byte for sending to the radio""" + # Certain bytes are used as control characters to the radio, so if one of + # these bytes is present in the stream to the radio, it gets escaped as + # 0xff followed by (byte & 0x0f) + if byte > 0xf9: + return bytes([0xff, byte & 0xf]) + return bytes([byte]) + + +def unescape_raw_bytes(escaped_data): + """Unescapes raw bytes from the radio.""" + data = b"" + i = 0 + while i < len(escaped_data): + byte = escaped_data[i] + if byte == 0xff: + if i + 1 >= len(escaped_data): + raise errors.InvalidDataError( + "Unexpected escape character at end of data") + i += 1 + byte = 0xf0 | escaped_data[i] + data += bytes([byte]) + i += 1 + return data + + +class IcomRawCloneModeRadio(IcomCloneModeRadio): + """Subclass for Icom clone-mode radios using the raw data protocol.""" + def process_frame_payload(self, payload): + """Payloads from a raw-clone-mode radio are already in raw format.""" + return unescape_raw_bytes(payload) + + def get_payload(self, data, raw, checksum): + """Returns the data with optional checksum in raw format.""" + payload = data + if checksum: + payload += bytes([compute_checksum(data)]) + # Escape control characters. + escaped_payload = b''.join([escape_raw_byte(b) for b in payload]) + return escaped_payload + + def sync_in(self): + # The radio returns all the bytes with the high-order bit flipped. + _mmap = clone_from_radio(self) + _mmap = flip_high_order_bit(_mmap.get_packed()) + self._mmap = memmap.MemoryMap(_mmap) + self.process_mmap() + + def get_mmap(self): + _data = flip_high_order_bit(self._mmap.get_packed()) + return memmap.MemoryMap(_data) + + +class IcomLiveRadio(chirp_common.LiveRadio): + """Base class for an Icom Live-mode radio""" + VENDOR = "Icom" + BAUD_RATE = 38400 + + _num_banks = 26 # Most live Icoms have 26 banks, A-Z + _bank_index_bounds = (0, 99) + _bank_class = IcomBank + + def get_bank_model(self): + rf = self.get_features() + if rf.has_bank: + if rf.has_bank_index: + return IcomIndexedBankModel(self) + else: + return IcomBankModel(self) + else: + return None + + +def make_speed_switch_setting(radio): + if not radio.__class__._can_hispeed: + return {} + drvopts = RadioSettingGroup("drvopts", "Driver Options") + top = RadioSettings(drvopts) + rs = RadioSetting("drv_clone_speed", "Use Hi-Speed Clone", + RadioSettingValueBoolean(radio._can_hispeed)) + drvopts.append(rs) + return top + + +def honor_speed_switch_setting(radio, settings): + for element in settings: + if element.get_name() == "drvopts": + return honor_speed_switch_setting(radio, element) + if element.get_name() == "drv_clone_speed": + radio.__class__._can_hispeed = element.value.get_value() + return diff --git a/chirp/drivers/icomciv.py b/chirp/drivers/icomciv.py new file mode 100644 index 0000000..24fb297 --- /dev/null +++ b/chirp/drivers/icomciv.py @@ -0,0 +1,692 @@ + +import struct +import logging +from chirp.drivers import icf +from chirp import chirp_common, util, errors, bitwise, directory +from chirp.memmap import MemoryMap +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList, RadioSettingValueBoolean + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +bbcd number[2]; +u8 unknown:3 + split:1, + unknown_0:4; +lbcd freq[5]; +u8 unknown2:5, + mode:3; +u8 filter; +u8 unknown_1:3, + dig:1, + unknown_2:4; +""" + + +# http://www.vk4adc.com/ +# web/index.php/reference-information/49-general-ref-info/182-civ7400 +MEM_IC7000_FORMAT = """ +u8 bank; +bbcd number[2]; +u8 spl:4, + skip:4; +lbcd freq[5]; +u8 mode; +u8 filter; +u8 duplex:4, + tmode:4; +bbcd rtone[3]; +bbcd ctone[3]; +u8 dtcs_polarity; +bbcd dtcs[2]; +lbcd freq_tx[5]; +u8 mode_tx; +u8 filter_tx; +u8 duplex_tx:4, + tmode_tx:4; +bbcd rtone_tx[3]; +bbcd ctone_tx[3]; +u8 dtcs_polarity_tx; +bbcd dtcs_tx[2]; +char name[9]; +""" + +MEM_IC7100_FORMAT = """ +u8 bank; // 1 bank number +bbcd number[2]; // 2,3 +u8 splitSelect; // 4 split and select memory settings +lbcd freq[5]; // 5-9 operating freq +u8 mode; // 10 operating mode +u8 filter; // 11 filter +u8 dataMode; // 12 data mode setting (on or off) +u8 duplex:4, // 13 duplex on/-/+ + tmode:4; // 13 tone +u8 dsql:4, // 14 digital squelch + unknown1:4; // 14 zero +bbcd rtone[3]; // 15-17 repeater tone freq +bbcd ctone[3]; // 18-20 tone squelch setting +u8 dtcsPolarity; // 21 DTCS polarity +u8 unknown2:4, // 22 zero + firstDtcs:4; // 22 first digit of DTCS code +u8 secondDtcs:4, // 23 second digit DTCS + thirdDtcs:4; // 23 third digit DTCS +u8 digitalSquelch; // 24 Digital code squelch setting +u8 duplexOffset[3]; // 25-27 duplex offset freq +char destCall[8]; // 28-35 destination call sign +char accessRepeaterCall[8];// 36-43 access repeater call sign +char linkRepeaterCall[8]; // 44-51 gateway/link repeater call sign +bbcd duplexSettings[47]; // repeat of 5-51 for duplex +char name[16]; // 52-60 Name of station +""" + +mem_duptone_format = """ +bbcd number[2]; +u8 unknown1; +lbcd freq[5]; +u8 unknown2:5, + mode:3; +u8 unknown1; +u8 unknown2:2, + duplex:2, + unknown3:1, + tmode:3; +u8 unknown4; +bbcd rtone[2]; +u8 unknown5; +bbcd ctone[2]; +u8 dtcs_polarity; +bbcd dtcs[2]; +u8 unknown[11]; +char name[9]; +""" + +SPLIT = ["", "spl"] + + +class Frame: + """Base class for an ICF frame""" + _cmd = 0x00 + _sub = 0x00 + + def __init__(self): + self._data = "" + + def set_command(self, cmd, sub): + """Set the command number (and optional subcommand)""" + self._cmd = cmd + self._sub = sub + + def get_data(self): + """Return the data payload""" + return self._data + + def set_data(self, data): + """Set the data payload""" + self._data = data + + def send(self, src, dst, serial, willecho=True): + """Send the frame over @serial, using @src and @dst addresses""" + raw = struct.pack("BBBBBB", 0xFE, 0xFE, src, dst, self._cmd, self._sub) + raw += str(self._data) + chr(0xFD) + + LOG.debug("%02x -> %02x (%i):\n%s" % + (src, dst, len(raw), util.hexprint(raw))) + + serial.write(raw) + if willecho: + echo = serial.read(len(raw)) + if echo != raw and echo: + LOG.debug("Echo differed (%i/%i)" % (len(raw), len(echo))) + LOG.debug(util.hexprint(raw)) + LOG.debug(util.hexprint(echo)) + + def read(self, serial): + """Read the frame from @serial""" + data = "" + while not data.endswith(chr(0xFD)): + char = serial.read(1) + if not char: + LOG.debug("Read %i bytes total" % len(data)) + raise errors.RadioError("Timeout") + data += char + + if data == chr(0xFD): + raise errors.RadioError("Radio reported error") + + src, dst = struct.unpack("BB", data[2:4]) + LOG.debug("%02x <- %02x:\n%s" % (dst, src, util.hexprint(data))) + + self._cmd = ord(data[4]) + self._sub = ord(data[5]) + self._data = data[6:-1] + + return src, dst + + def get_obj(self): + raise errors.RadioError("Generic frame has no structure") + + +class MemFrame(Frame): + """A memory frame""" + _cmd = 0x1A + _sub = 0x00 + _loc = 0 + + def set_location(self, loc): + """Set the memory location number""" + self._loc = loc + self._data = struct.pack(">H", int("%04i" % loc, 16)) + + def make_empty(self): + """Mark as empty so the radio will erase the memory""" + self._data = struct.pack(">HB", int("%04i" % self._loc, 16), 0xFF) + + def is_empty(self): + """Return True if memory is marked as empty""" + return len(self._data) < 5 + + def get_obj(self): + """Return a bitwise parsed object""" + self._data = MemoryMap(str(self._data)) # Make sure we're assignable + return bitwise.parse(MEM_FORMAT, self._data) + + def initialize(self): + """Initialize to sane values""" + self._data = MemoryMap("".join(["\x00"] * (self.get_obj().size() / 8))) + + +class BankMemFrame(MemFrame): + """A memory frame for radios with multiple banks""" + FORMAT = MEM_IC7000_FORMAT + _bnk = 0 + + def set_location(self, loc, bank=1): + self._loc = loc + self._bnk = bank + self._data = struct.pack( + ">BH", int("%02i" % bank, 16), int("%04i" % loc, 16)) + + def make_empty(self): + """Mark as empty so the radio will erase the memory""" + self._data = struct.pack( + ">BHB", int("%02i" % self._bnk, 16), + int("%04i" % self._loc, 16), 0xFF) + + def get_obj(self): + self._data = MemoryMap(str(self._data)) # Make sure we're assignable + return bitwise.parse(self.FORMAT, self._data) + + +class IC7100MemFrame(BankMemFrame): + FORMAT = MEM_IC7100_FORMAT + + +class DupToneMemFrame(MemFrame): + def get_obj(self): + self._data = MemoryMap(str(self._data)) + return bitwise.parse(mem_duptone_format, self._data) + + +class IcomCIVRadio(icf.IcomLiveRadio): + """Base class for ICOM CIV-based radios""" + BAUD_RATE = 19200 + MODEL = "CIV Radio" + _model = "\x00" + _template = 0 + + # complete list of modes from CI-V documentation + # each radio supports a subset + # WARNING: "S-AM" and "PSK" are not valid (yet) for chirp + _MODES = [ + "LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM", "CWR" + "RTTYR", "S-AM", "PSK", None, None, None, None, None, + None, None, None, None, None, None, None, None, + "DV", + ] + + def mem_to_ch_bnk(self, mem): + l, h = self._bank_index_bounds + bank_no = (mem // (h - l + 1)) + l + channel = mem % (h - l + 1) + l + return (channel, bank_no) + + def _send_frame(self, frame): + return frame.send(ord(self._model), 0xE0, self.pipe, + willecho=self._willecho) + + def _recv_frame(self, frame=None): + if not frame: + frame = Frame() + frame.read(self.pipe) + return frame + + def _initialize(self): + pass + + def _detect_echo(self): + echo_test = "\xfe\xfe\xe0\xe0\xfa\xfd" + self.pipe.write(echo_test) + resp = self.pipe.read(6) + LOG.debug("Echo:\n%s" % util.hexprint(resp)) + return resp == echo_test + + def __init__(self, *args, **kwargs): + icf.IcomLiveRadio.__init__(self, *args, **kwargs) + + self._classes = { + "mem": MemFrame, + } + + if self.pipe: + self._willecho = self._detect_echo() + LOG.debug("Interface echo: %s" % self._willecho) + self.pipe.timeout = 1 + + # f = Frame() + # f.set_command(0x19, 0x00) + # self._send_frame(f) + # + # res = f.read(self.pipe) + # if res: + # LOG.debug("Result: %x->%x (%i)" % + # (res[0], res[1], len(f.get_data()))) + # LOG.debug(util.hexprint(f.get_data())) + # + # self._id = f.get_data()[0] + self._rf = chirp_common.RadioFeatures() + + self._initialize() + + def get_features(self): + return self._rf + + def _get_template_memory(self): + f = self._classes["mem"]() + f.set_location(self._template) + self._send_frame(f) + f.read(self.pipe) + return f + + def get_raw_memory(self, number): + f = self._classes["mem"]() + if self._rf.has_bank: + ch, bnk = self.mem_to_ch_bnk(number) + f.set_location(ch, bnk) + loc = "bank %i, channel %02i" % (bnk, ch) + else: + f.set_location(number) + loc = "number %i" % number + self._send_frame(f) + f.read(self.pipe) + if f.get_data() and f.get_data()[-1] == "\xFF": + return "Memory " + loc + " empty." + else: + return repr(f.get_obj()) + +# We have a simple mapping between the memory location in the frequency +# editor and (bank, channel) of the radio. The mapping doesn't +# change so we use a little math to calculate what bank a location +# is in. We can't change the bank a location is in so we just pass. + def _get_bank(self, loc): + l, h = self._bank_index_bounds + return loc // (h - l + 1) + + def _set_bank(self, loc, bank): + pass + + def get_memory(self, number): + LOG.debug("Getting %i" % number) + f = self._classes["mem"]() + if self._rf.has_bank: + ch, bnk = self.mem_to_ch_bnk(number) + f.set_location(ch, bnk) + LOG.debug("Bank %i, Channel %02i" % (bnk, ch)) + else: + f.set_location(number) + self._send_frame(f) + + mem = chirp_common.Memory() + mem.number = number + mem.immutable = [] + + f = self._recv_frame(f) + if len(f.get_data()) == 0: + raise errors.RadioError("Radio reported error") + if f.get_data() and f.get_data()[-1] == "\xFF": + mem.empty = True + LOG.debug("Found %i empty" % mem.number) + return mem + + memobj = f.get_obj() + LOG.debug(repr(memobj)) + + try: + if memobj.skip == 1: + mem.skip = "" + else: + mem.skip = "S" + except AttributeError: + pass + + mem.freq = int(memobj.freq) + try: + mem.mode = self._MODES[memobj.mode] + + # We do not know what a variety of the positions between + # PSK and DV mean, so let's behave as if those values + # are not set to maintain consistency between known-unknown + # values and unknown-unknown ones. + if mem.mode is None: + raise IndexError(memobj.mode) + except IndexError: + LOG.error( + "Bank %s location %s is set for mode %s, but no known " + "mode matches that value.", + int(memobj.bank), + int(memobj.number), + repr(memobj.mode), + ) + raise + + if self._rf.has_name: + mem.name = str(memobj.name).rstrip() + + if self._rf.valid_tmodes: + mem.tmode = self._rf.valid_tmodes[memobj.tmode] + + if self._rf.has_dtcs_polarity: + if memobj.dtcs_polarity == 0x11: + mem.dtcs_polarity = "RR" + elif memobj.dtcs_polarity == 0x10: + mem.dtcs_polarity = "RN" + elif memobj.dtcs_polarity == 0x01: + mem.dtcs_polarity = "NR" + else: + mem.dtcs_polarity = "NN" + + if self._rf.has_dtcs: + mem.dtcs = bitwise.bcd_to_int(memobj.dtcs) + + if "Tone" in self._rf.valid_tmodes: + mem.rtone = int(memobj.rtone) / 10.0 + + if "TSQL" in self._rf.valid_tmodes and self._rf.has_ctone: + mem.ctone = int(memobj.ctone) / 10.0 + + if self._rf.valid_duplexes: + mem.duplex = self._rf.valid_duplexes[memobj.duplex] + + if self._rf.can_odd_split and memobj.spl: + mem.duplex = "split" + mem.offset = int(memobj.freq_tx) + mem.immutable = [] + else: + mem.immutable = ["offset"] + + mem.extra = RadioSettingGroup("extra", "Extra") + try: + dig = RadioSetting("dig", "Digital", + RadioSettingValueBoolean(bool(memobj.dig))) + except AttributeError: + pass + else: + dig.set_doc("Enable digital mode") + mem.extra.append(dig) + + options = ["Wide", "Mid", "Narrow"] + try: + fil = RadioSetting( + "filter", "Filter", + RadioSettingValueList(options, + options[memobj.filter - 1])) + except AttributeError: + pass + else: + fil.set_doc("Filter settings") + mem.extra.append(fil) + + return mem + + def set_memory(self, mem): + LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number)) + if self._rf.has_bank: + ch, bnk = self.mem_to_ch_bnk(mem.number) + LOG.debug("Bank %i, Channel %02i" % (bnk, ch)) + f = self._get_template_memory() + if mem.empty: + if self._rf.has_bank: + f.set_location(ch, bnk) + else: + f.set_location(mem.number) + LOG.debug("Making %i empty" % mem.number) + f.make_empty() + self._send_frame(f) + +# The next two lines accept the radio's status after setting the memory +# and reports the results to the debug log. This is needed for the +# IC-7000. No testing was done to see if it breaks memory delete on the +# IC-746 or IC-7200. + f = self._recv_frame() + LOG.debug("Result:\n%s" % util.hexprint(f.get_data())) + return + + # f.set_data(MemoryMap(self.get_raw_memory(mem.number))) + # f.initialize() + + memobj = f.get_obj() + if self._rf.has_bank: + memobj.bank = bnk + memobj.number = ch + else: + memobj.number = mem.number + if mem.skip == "S": + memobj.skip = 0 + else: + try: + memobj.skip = 1 + except KeyError: + pass + memobj.freq = int(mem.freq) + memobj.mode = self._MODES.index(mem.mode) + if self._rf.has_name: + name_length = len(memobj.name.get_value()) + memobj.name = mem.name.ljust(name_length)[:name_length] + + if self._rf.valid_tmodes: + memobj.tmode = self._rf.valid_tmodes.index(mem.tmode) + + if self._rf.has_ctone: + memobj.ctone = int(mem.ctone * 10) + memobj.rtone = int(mem.rtone * 10) + + if self._rf.has_dtcs_polarity: + if mem.dtcs_polarity == "RR": + memobj.dtcs_polarity = 0x11 + elif mem.dtcs_polarity == "RN": + memobj.dtcs_polarity = 0x10 + elif mem.dtcs_polarity == "NR": + memobj.dtcs_polarity = 0x01 + else: + memobj.dtcs_polarity = 0x00 + + if self._rf.has_dtcs: + bitwise.int_to_bcd(memobj.dtcs, mem.dtcs) + + if self._rf.can_odd_split and mem.duplex == "split": + memobj.spl = 1 + memobj.duplex = 0 + memobj.freq_tx = int(mem.offset) + memobj.tmode_tx = memobj.tmode + memobj.ctone_tx = memobj.ctone + memobj.rtone_tx = memobj.rtone + memobj.dtcs_polarity_tx = memobj.dtcs_polarity + memobj.dtcs_tx = memobj.dtcs + elif self._rf.valid_duplexes: + memobj.duplex = self._rf.valid_duplexes.index(mem.duplex) + + for setting in mem.extra: + if setting.get_name() == "filter": + setattr(memobj, setting.get_name(), int(setting.value) + 1) + else: + setattr(memobj, setting.get_name(), setting.value) + + LOG.debug(repr(memobj)) + self._send_frame(f) + + f = self._recv_frame() + LOG.debug("Result:\n%s" % util.hexprint(f.get_data())) + + +@directory.register +class Icom7200Radio(IcomCIVRadio): + """Icom IC-7200""" + MODEL = "7200" + _model = "\x76" + _template = 201 + + _num_banks = 1 # Banks not supported + + def _initialize(self): + self._rf.has_bank = False + self._rf.has_dtcs_polarity = False + self._rf.has_dtcs = False + self._rf.has_ctone = False + self._rf.has_offset = False + self._rf.has_name = False + self._rf.has_tuning_step = False + self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", + "CWR", "RTTYR"] + self._rf.valid_tmodes = [] + self._rf.valid_duplexes = [] + self._rf.valid_bands = [(30000, 60000000)] + self._rf.valid_skips = [] + self._rf.memory_bounds = (1, 201) + + +@directory.register +class Icom7000Radio(IcomCIVRadio): + """Icom IC-7000""" + MODEL = "IC-7000" + _model = "\x70" + _template = 102 + + _num_banks = 5 # Banks A-E + _bank_index_bounds = (1, 99) + _bank_class = icf.IcomBank + + def _initialize(self): + self._classes["mem"] = BankMemFrame + self._rf.has_bank = True + self._rf.has_dtcs_polarity = True + self._rf.has_dtcs = True + self._rf.has_ctone = True + self._rf.has_offset = True + self._rf.has_name = True + self._rf.has_tuning_step = False + self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM"] + self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + self._rf.valid_duplexes = ["", "-", "+", "split"] + self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)] + self._rf.valid_tuning_steps = [] + self._rf.valid_skips = ["S", ""] + self._rf.valid_name_length = 9 + self._rf.valid_characters = chirp_common.CHARSET_ASCII + self._rf.memory_bounds = (0, 99 * self._num_banks - 1) + self._rf.can_odd_split = True + + +@directory.register +class Icom7100Radio(IcomCIVRadio): + """Icom IC-7100""" + MODEL = "IC-7100" + _model = "\x88" + _template = 102 + + _num_banks = 5 + _bank_index_bounds = (1, 99) + _bank_class = icf.IcomBank + + def _initialize(self): + self._classes["mem"] = IC7100MemFrame + self._rf.has_bank = True + self._rf.has_bank_index = False + self._rf.has_bank_names = False + self._rf.has_dtcs_polarity = False + self._rf.has_dtcs = False + self._rf.has_ctone = True + self._rf.has_offset = False + self._rf.has_name = True + self._rf.has_tuning_step = False + self._rf.valid_modes = [ + "LSB", "USB", "AM", "CW", "RTTY", "FM", "WFM", "CWR", "RTTYR", "DV" + ] + self._rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + self._rf.valid_duplexes = ["", "-", "+"] + self._rf.valid_bands = [(30000, 199999999), (400000000, 470000000)] + self._rf.valid_tuning_steps = [] + self._rf.valid_skips = [] + self._rf.valid_name_length = 16 + self._rf.valid_characters = chirp_common.CHARSET_ASCII + self._rf.memory_bounds = (0, 99 * self._num_banks - 1) + + +@directory.register +class Icom746Radio(IcomCIVRadio): + """Icom IC-746""" + MODEL = "746" + BAUD_RATE = 9600 + _model = "\x56" + _template = 102 + + _num_banks = 1 # Banks not supported + + def _initialize(self): + self._classes["mem"] = DupToneMemFrame + self._rf.has_bank = False + self._rf.has_dtcs_polarity = False + self._rf.has_dtcs = False + self._rf.has_ctone = True + self._rf.has_offset = False + self._rf.has_name = True + self._rf.has_tuning_step = False + self._rf.valid_modes = ["LSB", "USB", "AM", "CW", "RTTY", "FM"] + self._rf.valid_tmodes = ["", "Tone", "TSQL"] + self._rf.valid_duplexes = ["", "-", "+"] + self._rf.valid_bands = [(30000, 199999999)] + self._rf.valid_tuning_steps = [] + self._rf.valid_skips = [] + self._rf.valid_name_length = 9 + self._rf.valid_characters = chirp_common.CHARSET_ASCII + self._rf.memory_bounds = (1, 99) + +CIV_MODELS = { + (0x76, 0xE0): Icom7200Radio, + (0x88, 0xE0): Icom7100Radio, + (0x70, 0xE0): Icom7000Radio, + (0x46, 0xE0): Icom746Radio, +} + + +def probe_model(ser): + """Probe the radio attatched to @ser for its model""" + f = Frame() + f.set_command(0x19, 0x00) + + for model, controller in CIV_MODELS.keys(): + f.send(model, controller, ser) + try: + f.read(ser) + except errors.RadioError: + continue + + if len(f.get_data()) == 1: + model = ord(f.get_data()[0]) + return CIV_MODELS[(model, controller)] + + if f.get_data(): + LOG.debug("Got data, but not 1 byte:") + LOG.debug(util.hexprint(f.get_data())) + raise errors.RadioError("Unknown response") + + raise errors.RadioError("Unsupported model") diff --git a/chirp/drivers/icp7.py b/chirp/drivers/icp7.py new file mode 100644 index 0000000..c9a2459 --- /dev/null +++ b/chirp/drivers/icp7.py @@ -0,0 +1,243 @@ +# Copyright 2017 SASANO Takayoshi (JG1UAA) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, directory, bitwise + +# memory nuber: +# 000 - 999 regular memory channels (supported, others not) +# 1000 - 1049 scan edges +# 1050 - 1249 auto write channels +# 1250 call channel (C0) +# 1251 call channel (C1) + + +MEM_FORMAT = """ +struct { + ul32 freq; + ul32 offset; + ul16 train_sql:2, + tmode:3, + duplex:2, + train_tone:9; + ul16 tuning_step:4, + rtone:6, + ctone:6; + ul16 unknown0:6, + mode:3, + dtcs:7; + u8 unknown1:6, + dtcs_polarity:2; + char name[6]; +} memory[1251]; + +#seekto 0x6b1e; +struct { + u8 bank; + u8 index; +} banks[1050]; + +#seekto 0x689e; +u8 used[132]; + +#seekto 0x6922; +u8 skips[132]; + +#seekto 0x69a6; +u8 pskips[132]; + +#seekto 0x7352; +struct { + char name[6]; +} bank_names[18]; + +""" + +MODES = ["FM", "WFM", "AM", "Auto"] +TMODES = ["", "Tone", "TSQL", "", "DTCS"] +DUPLEX = ["", "-", "+"] +DTCS_POLARITY = ["NN", "NR", "RN", "RR"] +TUNING_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, + 25.0, 30.0, 50.0, 100.0, 200.0, 0.0] # 0.0 as "Auto" + + +class ICP7Bank(icf.IcomBank): + """ICP7 bank""" + def get_name(self): + _bank = self._model._radio._memobj.bank_names[self.index] + return str(_bank.name).rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_names[self.index] + _bank.name = name.ljust(6)[:6] + + +@directory.register +class ICP7Radio(icf.IcomCloneModeRadio): + """Icom IC-P7""" + VENDOR = "Icom" + MODEL = "IC-P7" + + _model = "\x28\x69\x00\x01" + _memsize = 0x7500 + _endframe = "Icom Inc\x2e\x41\x38" + + _ranges = [(0x0000, 0x7500, 32)] + + _num_banks = 18 + _bank_class = ICP7Bank + _can_hispeed = True + + def _get_bank(self, loc): + _bank = self._memobj.banks[loc] + if _bank.bank != 0xff: + return _bank.bank + else: + return None + + def _set_bank(self, loc, bank): + _bank = self._memobj.banks[loc] + if bank is None: + _bank.bank = 0xff + else: + _bank.bank = bank + + def _get_bank_index(self, loc): + _bank = self._memobj.banks[loc] + return _bank.index + + def _set_bank_index(self, loc, index): + _bank = self._memobj.banks[loc] + _bank.index = index + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 999) + rf.valid_tmodes = TMODES + rf.valid_duplexes = DUPLEX + rf.valid_modes = MODES + rf.valid_bands = [(495000, 999990000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_tuning_steps = TUNING_STEPS + rf.valid_name_length = 6 + rf.has_settings = True + rf.has_ctone = True + rf.has_bank = True + rf.has_bank_index = True + rf.has_bank_names = True + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + bit = 1 << (number % 8) + byte = int(number / 8) + + _mem = self._memobj.memory[number] + _usd = self._memobj.used[byte] + _skp = self._memobj.skips[byte] + _psk = self._memobj.pskips[byte] + + mem = chirp_common.Memory() + mem.number = number + + if _usd & bit: + mem.empty = True + return mem + + mem.freq = _mem.freq // 3 + mem.offset = _mem.offset // 3 + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.tuning_step = TUNING_STEPS[_mem.tuning_step] + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity] + mem.name = str(_mem.name).rstrip() + + if _skp & bit: + mem.skip = "P" if _psk & bit else "S" + else: + mem.skip = "" + + return mem + + def set_memory(self, mem): + bit = 1 << (mem.number % 8) + byte = int(mem.number / 8) + + _mem = self._memobj.memory[mem.number] + _usd = self._memobj.used[byte] + _skp = self._memobj.skips[byte] + _psk = self._memobj.pskips[byte] + + if mem.empty: + _usd |= bit + + # We use default value instead of zero-fill + # to avoid unexpected behavior. + _mem.freq = 15000 + _mem.offset = 479985000 + _mem.train_sql = ~0 + _mem.tmode = ~0 + _mem.duplex = ~0 + _mem.train_tone = ~0 + _mem.tuning_step = ~0 + _mem.rtone = ~0 + _mem.ctone = ~0 + _mem.unknown0 = 0 + _mem.mode = ~0 + _mem.dtcs = ~0 + _mem.unknown1 = ~0 + _mem.dtcs_polarity = ~0 + _mem.name = " " + + _skp |= bit + _psk |= bit + + else: + _usd &= ~bit + + _mem.freq = mem.freq * 3 + _mem.offset = mem.offset * 3 + _mem.train_sql = 0 # Train SQL mode (0:off 1:Tone 2:MSK) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.train_tone = 228 # Train SQL Tone (x10Hz) + _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step) + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.unknown0 = 0 # unknown (always zero) + _mem.mode = MODES.index(mem.mode) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.unknown1 = ~0 # unknown (always one) + _mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity) + _mem.name = mem.name.ljust(6)[:6] + + if mem.skip == "S": + _skp |= bit + _psk &= ~bit + elif mem.skip == "P": + _skp |= bit + _psk |= bit + else: + _skp &= ~bit + _psk &= ~bit diff --git a/chirp/drivers/icq7.py b/chirp/drivers/icq7.py new file mode 100644 index 0000000..7a081b9 --- /dev/null +++ b/chirp/drivers/icq7.py @@ -0,0 +1,349 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import logging + +from chirp.drivers import icf +from chirp import chirp_common, directory, bitwise +from chirp.chirp_common import to_GHz, from_GHz +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings + + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct { + bbcd freq[3]; + u8 fractional:1, + unknown:7; + bbcd offset[2]; + u16 ctone:6 + rtone:6, + tune_step:4; +} memory[200]; + +#seekto 0x0690; +struct { + u8 tmode:2, + duplex:2, + skip:1, + pskip:1, + mode:2; +} flags[200]; + +#seekto 0x0690; +u8 flags_whole[200]; + +#seekto 0x0767; +struct { +i8 rit; +u8 squelch; +u8 lock:1, + ritfunct:1, + unknown:6; +u8 unknown1[6]; +u8 d_sel; +u8 autorp; +u8 priority; +u8 resume; +u8 pause; +u8 p_scan; +u8 bnk_scan; +u8 expand; +u8 ch; +u8 beep; +u8 light; +u8 ap_off; +u8 p_save; +u8 monitor; +u8 speed; +u8 edge; +u8 lockgroup; +} settings; + +""" + +TMODES = ["", "", "Tone", "TSQL", "TSQL"] # last one is pocket beep +DUPLEX = ["", "", "-", "+"] +MODES = ["FM", "WFM", "AM", "Auto"] +STEPS = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] +AUTORP_LIST = ["Off", "Duplex Only", "Duplex and Tone"] +LOCKGROUP_LIST = ["Normal", "No Squelch", "No Volume", "All"] +SQUELCH_LIST = ["Open", "Auto"] + ["L%s" % x for x in range(1, 10)] +MONITOR_LIST = ["Push", "Hold"] +LIGHT_LIST = ["Off", "On", "Auto"] +PRIORITY_LIST = ["Off", "On", "Bell"] +BANKSCAN_LIST = ["Off", "Bank 0", "Bank 1"] +EDGE_LIST = ["%sP" % x for x in range(0, 20)] + ["Band", "All"] +PAUSE_LIST = ["%s sec" % x for x in range(2, 22, 2)] + ["Hold"] +RESUME_LIST = ["%s sec" % x for x in range(0, 6)] +APOFF_LIST = ["Off"] + ["%s min" % x for x in range(30, 150, 30)] +D_SEL_LIST = ["100 KHz", "1 MHz", "10 MHz"] + + +@directory.register +class ICQ7Radio(icf.IcomCloneModeRadio): + """Icom IC-Q7A""" + VENDOR = "Icom" + MODEL = "IC-Q7A" + + _model = "\x19\x95\x00\x01" + _memsize = 0x7C0 + _endframe = "Icom Inc\x2e" + + _ranges = [(0x0000, 0x07C0, 16)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.memory_bounds = (0, 199) + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(1000000, 823995000), + (849000000, 868995000), + (894000000, 1309995000)] + rf.valid_skips = ["", "S", "P"] + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_name = False + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return (repr(self._memobj.memory[number]) + + repr(self._memobj.flags[number])) + + def validate_memory(self, mem): + if mem.freq < 30000000 and mem.mode != 'AM': + return [chirp_common.ValidationError( + 'Only AM is allowed below 30MHz')] + return icf.IcomCloneModeRadio.validate_memory(self, mem) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _flag = self._memobj.flags[number] + + mem = chirp_common.Memory() + mem.number = number + if self._memobj.flags_whole[number] == 0xFF: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 1000 + if _mem.fractional: + mem.freq = chirp_common.fix_rounded_step(mem.freq) + mem.offset = int(_mem.offset) * 1000 + try: + mem.rtone = chirp_common.TONES[_mem.rtone] + except IndexError: + mem.rtone = 88.5 + try: + mem.ctone = chirp_common.TONES[_mem.ctone] + except IndexError: + mem.ctone = 88.5 + try: + mem.tuning_step = STEPS[_mem.tune_step] + except IndexError: + LOG.error("Invalid tune step index %i" % _mem.tune_step) + mem.tmode = TMODES[_flag.tmode] + mem.duplex = DUPLEX[_flag.duplex] + if mem.freq < 30000000: + mem.mode = "AM" + else: + mem.mode = MODES[_flag.mode] + if _flag.pskip: + mem.skip = "P" + elif _flag.skip: + mem.skip = "S" + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _flag = self._memobj.flags[mem.number] + + if mem.empty: + self._memobj.flags_whole[mem.number] = 0xFF + return + + _mem.set_raw("\x00" * 8) + + if mem.freq > to_GHz(1): + _mem.freq = (mem.freq // 1000) - to_GHz(1) + upper = from_GHz(mem.freq) << 4 + _mem.freq[0].clr_bits(0xF0) + _mem.freq[0].set_bits(upper) + else: + _mem.freq = mem.freq / 1000 + _mem.fractional = chirp_common.is_fractional_step(mem.freq) + _mem.offset = mem.offset / 1000 + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.tune_step = STEPS.index(mem.tuning_step) + _flag.tmode = TMODES.index(mem.tmode) + _flag.duplex = DUPLEX.index(mem.duplex) + _flag.mode = MODES.index(mem.mode) + _flag.skip = mem.skip == "S" and 1 or 0 + _flag.pskip = mem.skip == "P" and 1 or 0 + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + group = RadioSettings(basic) + + rs = RadioSetting("ch", "Channel Indication Mode", + RadioSettingValueBoolean(_settings.ch)) + basic.append(rs) + + rs = RadioSetting("expand", "Expanded Settings Mode", + RadioSettingValueBoolean(_settings.expand)) + basic.append(rs) + + rs = RadioSetting("beep", "Beep Tones", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + rs = RadioSetting("autorp", "Auto Repeater Function", + RadioSettingValueList( + AUTORP_LIST, AUTORP_LIST[_settings.autorp])) + basic.append(rs) + + rs = RadioSetting("ritfunct", "RIT Runction", + RadioSettingValueBoolean(_settings.ritfunct)) + basic.append(rs) + + rs = RadioSetting("rit", "RIT Shift (KHz)", + RadioSettingValueInteger(-7, 7, _settings.rit)) + basic.append(rs) + + rs = RadioSetting("lock", "Lock", + RadioSettingValueBoolean(_settings.lock)) + basic.append(rs) + + rs = RadioSetting("lockgroup", "Lock Group", + RadioSettingValueList( + LOCKGROUP_LIST, + LOCKGROUP_LIST[_settings.lockgroup])) + basic.append(rs) + + rs = RadioSetting("squelch", "Squelch", + RadioSettingValueList( + SQUELCH_LIST, SQUELCH_LIST[_settings.squelch])) + basic.append(rs) + + rs = RadioSetting("monitor", "Monitor Switch Function", + RadioSettingValueList( + MONITOR_LIST, + MONITOR_LIST[_settings.monitor])) + basic.append(rs) + + rs = RadioSetting("light", "Display Backlighting", + RadioSettingValueList( + LIGHT_LIST, LIGHT_LIST[_settings.light])) + basic.append(rs) + + rs = RadioSetting("priority", "Priority Watch Operation", + RadioSettingValueList( + PRIORITY_LIST, + PRIORITY_LIST[_settings.priority])) + basic.append(rs) + + rs = RadioSetting("p_scan", "Frequency Skip Function", + RadioSettingValueBoolean(_settings.p_scan)) + basic.append(rs) + + rs = RadioSetting("bnk_scan", "Memory Bank Scan Selection", + RadioSettingValueList( + BANKSCAN_LIST, + BANKSCAN_LIST[_settings.bnk_scan])) + basic.append(rs) + + rs = RadioSetting("edge", "Band Edge Scan Selection", + RadioSettingValueList( + EDGE_LIST, EDGE_LIST[_settings.edge])) + basic.append(rs) + + rs = RadioSetting("pause", "Scan Pause Time", + RadioSettingValueList( + PAUSE_LIST, PAUSE_LIST[_settings.pause])) + basic.append(rs) + + rs = RadioSetting("resume", "Scan Resume Time", + RadioSettingValueList( + RESUME_LIST, RESUME_LIST[_settings.resume])) + basic.append(rs) + + rs = RadioSetting("p_save", "Power Saver", + RadioSettingValueBoolean(_settings.p_save)) + basic.append(rs) + + rs = RadioSetting("ap_off", "Auto Power-off Function", + RadioSettingValueList( + APOFF_LIST, APOFF_LIST[_settings.ap_off])) + basic.append(rs) + + rs = RadioSetting("speed", "Dial Speed Acceleration", + RadioSettingValueBoolean(_settings.speed)) + basic.append(rs) + + rs = RadioSetting("d_sel", "Dial Select Step", + RadioSettingValueList( + D_SEL_LIST, D_SEL_LIST[_settings.d_sel])) + basic.append(rs) + + return group + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception as e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/ict70.py b/chirp/drivers/ict70.py new file mode 100644 index 0000000..f05eb21 --- /dev/null +++ b/chirp/drivers/ict70.py @@ -0,0 +1,223 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, directory, bitwise + +MEM_FORMAT = """ +struct { + u24 freq; + ul16 offset; + char name[6]; + u8 unknown2:2, + rtone:6; + u8 unknown3:2, + ctone:6; + u8 unknown4:1, + dtcs:7; + u8 tuning_step:4, + narrow:1, + unknown5:1, + duplex:2; + u8 unknown6:1, + power:2, + dtcs_polarity:2, + tmode:3; +} memory[300]; + +#seekto 0x12E0; +u8 used[38]; + +#seekto 0x1306; +u8 skips[38]; + +#seekto 0x132C; +u8 pskips[38]; + +#seekto 0x1360; +struct { + u8 bank; + u8 index; +} banks[300]; + +#seekto 0x16D0; +struct { + char name[6]; +} bank_names[26]; + +""" + +TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS"] +DUPLEX = ["", "-", "+"] +DTCS_POLARITY = ["NN", "NR", "RN", "RR"] +TUNING_STEPS = [5.0, 5.0, 5.0, 5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, + 50.0, 100.0, 125.0, 200.0] +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5), + chirp_common.PowerLevel("Low", watts=0.5), + chirp_common.PowerLevel("Mid", watts=1.0), + ] + + +class ICT70Bank(icf.IcomBank): + """ICT70 bank""" + def get_name(self): + _bank = self._model._radio._memobj.bank_names[self.index] + return str(_bank.name).rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_names[self.index] + _bank.name = name.ljust(6)[:6] + + +@directory.register +class ICT70Radio(icf.IcomCloneModeRadio): + """Icom IC-T70""" + VENDOR = "Icom" + MODEL = "IC-T70" + + _model = "\x32\x53\x00\x01" + _memsize = 0x19E0 + _endframe = "Icom Inc\x2eCF" + + _ranges = [(0x0000, 0x19E0, 32)] + + _num_banks = 26 + _bank_class = ICT70Bank + + def _get_bank(self, loc): + _bank = self._memobj.banks[loc] + if _bank.bank != 0xFF: + return _bank.bank + else: + return None + + def _set_bank(self, loc, bank): + _bank = self._memobj.banks[loc] + if bank is None: + _bank.bank = 0xFF + else: + _bank.bank = bank + + def _get_bank_index(self, loc): + _bank = self._memobj.banks[loc] + return _bank.index + + def _set_bank_index(self, loc, index): + _bank = self._memobj.banks[loc] + _bank.index = index + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 299) + rf.valid_tmodes = TMODES + rf.valid_duplexes = DUPLEX + rf.valid_power_levels = POWER_LEVELS + rf.valid_modes = ["FM", "NFM"] + rf.valid_bands = [(136000000, 174000000), (400000000, 479000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_tuning_steps = TUNING_STEPS + rf.valid_name_length = 6 + rf.has_ctone = True + rf.has_bank = True + rf.has_bank_index = True + rf.has_bank_names = True + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + bit = 1 << (number % 8) + byte = int(number / 8) + + _mem = self._memobj.memory[number] + _usd = self._memobj.used[byte] + _skp = self._memobj.skips[byte] + _psk = self._memobj.pskips[byte] + + mem = chirp_common.Memory() + mem.number = number + + if _usd & bit: + mem.empty = True + return mem + + if _mem.freq & 0x800000: + mem.freq = (_mem.freq & ~0x800000) * 6250 + else: + mem.freq = _mem.freq * 5000 + mem.offset = _mem.offset * 5000 + mem.name = str(_mem.name).rstrip() + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.tuning_step = TUNING_STEPS[_mem.tuning_step] + mem.mode = _mem.narrow and "NFM" or "FM" + mem.duplex = DUPLEX[_mem.duplex] + mem.power = POWER_LEVELS[_mem.power] + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity] + mem.tmode = TMODES[_mem.tmode] + mem.skip = (_psk & bit and "P") or (_skp & bit and "S") or "" + + return mem + + def set_memory(self, mem): + bit = 1 << (mem.number % 8) + byte = int(mem.number / 8) + + _mem = self._memobj.memory[mem.number] + _usd = self._memobj.used[byte] + _skp = self._memobj.skips[byte] + _psk = self._memobj.pskips[byte] + + _mem.set_raw("\x00" * (_mem.size() // 8)) + + if mem.empty: + _usd |= bit + return + + _usd &= ~bit + + if chirp_common.is_12_5(mem.freq): + _mem.freq = (mem.freq // 6250) | 0x800000 + else: + _mem.freq = mem.freq // 5000 + _mem.offset = mem.offset // 5000 + _mem.name = mem.name.ljust(6)[:6] + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tuning_step = TUNING_STEPS.index(mem.tuning_step) + _mem.narrow = mem.mode == "NFM" + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.dtcs_polarity = DTCS_POLARITY.index(mem.dtcs_polarity) + _mem.tmode = TMODES.index(mem.tmode) + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + if mem.skip == "S": + _skp |= bit + _psk &= ~bit + elif mem.skip == "P": + _skp &= ~bit + _psk |= bit + else: + _skp &= ~bit + _psk &= ~bit diff --git a/chirp/drivers/ict7h.py b/chirp/drivers/ict7h.py new file mode 100644 index 0000000..9a28646 --- /dev/null +++ b/chirp/drivers/ict7h.py @@ -0,0 +1,122 @@ +# Copyright 2012 Eric Allen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, directory, bitwise + +mem_format = """ +struct { + bbcd freq[2]; + u8 lastfreq:4, + fraction:4; + bbcd offset[2]; + u8 unknown; + u8 rtone; + u8 ctone; +} memory[60]; + +#seekto 0x0270; +struct { + u8 empty:1, + tmode:2, + duplex:2, + unknown3:1, + skip:1, + unknown4:1; +} flags[60]; +""" + +TMODES = ["", "", "Tone", "TSQL", "TSQL"] # last one is pocket beep +DUPLEX = ["", "", "-", "+"] +MODES = ["FM"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0] + + +@directory.register +class ICT7HRadio(icf.IcomCloneModeRadio): + VENDOR = "Icom" + MODEL = "IC-T7H" + + _model = "\x18\x10\x00\x01" + _memsize = 0x03B0 + _endframe = "Icom Inc\x2e" + + _ranges = [(0x0000, _memsize, 16)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 60) + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(118000000, 174000000), + (400000000, 470000000)] + rf.valid_skips = ["", "S"] + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_name = False + rf.has_tuning_step = False + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + _flag = self._memobj.flags[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + mem.empty = _flag.empty == 1 and True or False + + mem.freq = int(_mem.freq) * 100000 + mem.freq += _mem.lastfreq * 10000 + mem.freq += int(_mem.fraction / 2.0 * 1000) + + mem.offset = int(_mem.offset) * 10000 + mem.rtone = chirp_common.TONES[_mem.rtone - 1] + mem.ctone = chirp_common.TONES[_mem.ctone - 1] + mem.tmode = TMODES[_flag.tmode] + mem.duplex = DUPLEX[_flag.duplex] + mem.mode = "FM" + if _flag.skip: + mem.skip = "S" + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + _flag = self._memobj.flags[mem.number - 1] + + _mem.freq = int(mem.freq / 100000) + topfreq = int(mem.freq / 100000) * 100000 + lastfreq = int((mem.freq - topfreq) / 10000) + _mem.lastfreq = lastfreq + midfreq = (mem.freq - topfreq - lastfreq * 10000) + _mem.fraction = midfreq // 500 + + _mem.offset = mem.offset / 10000 + _mem.rtone = chirp_common.TONES.index(mem.rtone) + 1 + _mem.ctone = chirp_common.TONES.index(mem.ctone) + 1 + _flag.tmode = TMODES.index(mem.tmode) + _flag.duplex = DUPLEX.index(mem.duplex) + _flag.skip = mem.skip == "S" and 1 or 0 + _flag.empty = mem.empty and 1 or 0 diff --git a/chirp/drivers/ict8.py b/chirp/drivers/ict8.py new file mode 100644 index 0000000..299333a --- /dev/null +++ b/chirp/drivers/ict8.py @@ -0,0 +1,147 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, util, directory +from chirp import bitwise + +mem_format = """ +struct memory { + bbcd freq[3]; + bbcd offset[3]; + u8 rtone; + u8 ctone; +}; + +struct flags { + u8 empty:1, + skip:1, + tmode:2, + duplex:2, + unknown2:2; +}; + +struct memory memory[100]; + +#seekto 0x0400; +struct { + char name[4]; +} names[100]; + +#seekto 0x0600; +struct flags flags[100]; +""" + +DUPLEX = ["", "", "-", "+"] +TMODES = ["", "", "Tone", "TSQL"] + + +def _get_freq(bcd_array): + lastnibble = bcd_array[2].get_bits(0x0F) + return (int(bcd_array) - lastnibble) * 1000 + lastnibble * 500 + + +def _set_freq(bcd_array, freq): + bitwise.int_to_bcd(bcd_array, freq / 1000) + bcd_array[2].set_raw(bcd_array[2].get_bits(0xF0) + freq % 10000 // 500) + + +@directory.register +class ICT8ARadio(icf.IcomCloneModeRadio): + """Icom IC-T8A""" + VENDOR = "Icom" + MODEL = "IC-T8A" + + _model = "\x19\x03\x00\x01" + _memsize = 0x07B0 + _endframe = "Icom Inc\x2e" + + _ranges = [(0x0000, 0x07B0, 16)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_tmodes = TMODES + rf.valid_duplexes = DUPLEX + rf.valid_bands = [(50000000, 54000000), + (118000000, 174000000), + (400000000, 470000000)] + rf.valid_skips = ["", "S"] + rf.valid_modes = ["FM"] + rf.memory_bounds = (0, 99) + rf.valid_name_length = 4 + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_tuning_step = False + rf.has_mode = False + rf.has_bank = False + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_raw_memory(self, number): + return (str(self._memobj.memory[number]) + + str(self._memobj.names[number]) + + str(self._memobj.flags[number])) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _flg = self._memobj.flags[number] + _name = self._memobj.names[number] + + mem = chirp_common.Memory() + mem.number = number + + if _flg.empty: + mem.empty = True + return mem + + mem.freq = _get_freq(_mem.freq) + mem.offset = _get_freq(_mem.offset) + mem.rtone = chirp_common.TONES[_mem.rtone - 1] + mem.ctone = chirp_common.TONES[_mem.ctone - 1] + mem.duplex = DUPLEX[_flg.duplex] + mem.tmode = TMODES[_flg.tmode] + mem.skip = _flg.skip and "S" or "" + if _name.name.get_raw() != "\xFF\xFF\xFF\xFF": + mem.name = str(_name.name).rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _flg = self._memobj.flags[mem.number] + _name = self._memobj.names[mem.number] + + if mem.empty: + _flg.empty = True + return + + _mem.set_raw("\x00" * 8) + _flg.set_raw("\x00") + + _set_freq(_mem.freq, mem.freq) + _set_freq(_mem.offset, mem.offset) + _mem.rtone = chirp_common.TONES.index(mem.rtone) + 1 + _mem.ctone = chirp_common.TONES.index(mem.ctone) + 1 + _flg.duplex = DUPLEX.index(mem.duplex) + _flg.tmode = TMODES.index(mem.tmode) + _flg.skip = mem.skip == "S" + + if mem.name: + _name.name = mem.name.ljust(4) + else: + _name.name = "\xFF\xFF\xFF\xFF" diff --git a/chirp/drivers/icw32.py b/chirp/drivers/icw32.py new file mode 100644 index 0000000..0276a0c --- /dev/null +++ b/chirp/drivers/icw32.py @@ -0,0 +1,251 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +from chirp.drivers import icf +from chirp import chirp_common, util, directory, bitwise + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x%x; +struct { + bbcd freq[3]; + bbcd offset[3]; + u8 ctone; + u8 rtone; + char name[8]; +} memory[111]; + +#seekto 0x%x; +struct { + u8 empty:1, + skip:1, + tmode:2, + duplex:2, + unk3:1, + am:1; +} flag[111]; + +#seekto 0x0E9C; +struct { + u8 unknown1:7, + right_scan_direction:1; + u8 right_scanning:1, + unknown2:7; + u8 unknown3:7, + left_scan_direction:1; + u8 left_scanning:1, + unknown4:7; +} state[1]; + +#seekto 0x0F20; +struct { + bbcd freq[3]; + bbcd offset[3]; + u8 ctone; + u8 rtone; +} callchans[2]; + +""" + +DUPLEX = ["", "", "-", "+"] +TONE = ["", "", "Tone", "TSQL"] + + +def _get_special(): + special = {} + for i in range(0, 5): + special["M%iA" % (i+1)] = 100 + i*2 + special["M%iB" % (i+1)] = 100 + i*2 + 1 + return special + + +@directory.register +class ICW32ARadio(icf.IcomCloneModeRadio): + """Icom IC-W32A""" + VENDOR = "Icom" + MODEL = "IC-W32A" + + _model = "\x18\x82\x00\x01" + _memsize = 4064 + _endframe = "Icom Inc\x2e" + + _ranges = [(0x0000, 0x0FE0, 16)] + + _limits = (0, 0) + _mem_positions = (0, 1) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 99) + rf.valid_bands = [self._limits] + if int(self._limits[0] / 100) == 1: + rf.valid_modes = ["FM", "AM"] + else: + rf.valid_modes = ["FM"] + rf.valid_tmodes = ["", "Tone", "TSQL"] + rf.valid_name_length = 8 + rf.valid_special_chans = sorted(_get_special().keys()) + + rf.has_sub_devices = self.VARIANT == "" + rf.has_ctone = True + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_mode = "AM" in rf.valid_modes + rf.has_tuning_step = False + rf.has_bank = False + + return rf + + def process_mmap(self): + fmt = MEM_FORMAT % self._mem_positions + self._memobj = bitwise.parse(fmt, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + if isinstance(number, str): + number = _get_special()[number] + + _mem = self._memobj.memory[number] + _flg = self._memobj.flag[number] + + mem = chirp_common.Memory() + mem.number = number + + if number < 100: + # Normal memories + mem.skip = _flg.skip and "S" or "" + else: + # Special memories + mem.extd_number = util.get_dict_rev(_get_special(), number) + + if _flg.empty: + mem.empty = True + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 100 + if str(_mem.name)[0] != chr(0xFF): + mem.name = str(_mem.name).rstrip() + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + + mem.mode = _flg.am and "AM" or "FM" + mem.duplex = DUPLEX[_flg.duplex] + mem.tmode = TONE[_flg.tmode] + + if number > 100: + mem.immutable = ["number", "skip", "extd_number", "name"] + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number] + _flg = self._memobj.flag[mem.number] + + _flg.empty = mem.empty + if mem.empty: + return + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 100 + if mem.name: + _mem.name = mem.name.ljust(8)[:8] + else: + _mem.name = "".join(["\xFF" * 8]) + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + + _flg.duplex = DUPLEX.index(mem.duplex) + _flg.tmode = TONE.index(mem.tmode) + _flg.skip = mem.skip == "S" + _flg.am = mem.mode == "AM" + + if self._memobj.state.left_scanning: + LOG.debug("Canceling scan on left VFO") + self._memobj.state.left_scanning = 0 + if self._memobj.state.right_scanning: + LOG.debug("Canceling scan on right VFO") + self._memobj.state.right_scanning = 0 + + def get_sub_devices(self): + return [ICW32ARadioVHF(self._mmap), ICW32ARadioUHF(self._mmap)] + + @classmethod + def match_model(cls, filedata, filename): + if not len(filedata) == cls._memsize: + return False + return filedata[-16:] == b"IcomCloneFormat3" + + +class ICW32ARadioVHF(ICW32ARadio): + """ICW32 VHF subdevice""" + VARIANT = "VHF" + _limits = (118000000, 174000000) + _mem_positions = (0x0000, 0x0DC0) + + +class ICW32ARadioUHF(ICW32ARadio): + """ICW32 UHF subdevice""" + VARIANT = "UHF" + _limits = (400000000, 470000000) + _mem_positions = (0x06E0, 0x0E2E) + + +# IC-W32E are the very same as IC-W32A but have a different _model +@directory.register +class ICW32ERadio(ICW32ARadio): + """Icom IC-W32E""" + MODEL = "IC-W32E" + + _model = "\x18\x82\x00\x02" + + # an extra byte is added to distinguish file images from IC-W32A + # it will be allocated and initialized to 0x00 in _clone_from_radio + # (icf.py) but radio will not send it + # That byte is not sent to radio because the _clone_to_radio use _ranges + # for the send cycle + _memsize = ICW32ARadio._memsize + 1 + + def get_sub_devices(self): + # this is needed because sub devices must be of a child class + return [ICW32ERadioVHF(self._mmap), ICW32ERadioUHF(self._mmap)] + + @classmethod + def match_model(cls, filedata, filename): + if not len(filedata) == cls._memsize: + return False + return filedata[-16 - 1: -1] == b"IcomCloneFormat3" and \ + filedata[-1] in [0, '\x00'] + + +# this is the very same as ICW32ARadioVHF but have ICW32ERadio as parent class +class ICW32ERadioVHF(ICW32ERadio): + """ICW32 VHF subdevice""" + VARIANT = "VHF" + _limits = (118000000, 174000000) + _mem_positions = (0x0000, 0x0DC0) + + +# this is the very same as ICW32ARadioUHF but have ICW32ERadio as parent class +class ICW32ERadioUHF(ICW32ERadio): + """ICW32 UHF subdevice""" + VARIANT = "UHF" + _limits = (400000000, 470000000) + _mem_positions = (0x06E0, 0x0E2E) diff --git a/chirp/drivers/icx8x.py b/chirp/drivers/icx8x.py new file mode 100644 index 0000000..b8f1533 --- /dev/null +++ b/chirp/drivers/icx8x.py @@ -0,0 +1,209 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +from chirp.drivers import icf, icx8x_ll +from chirp import chirp_common, errors, directory + +LOG = logging.getLogger(__name__) + + +def _isuhf(radio): + try: + md = icf.get_model_data(radio) + val = md[20] + uhf = val & 0x10 + except: + raise errors.RadioError("Unable to probe radio band") + + LOG.debug("Radio is a %s82" % (uhf and "U" or "V")) + + return uhf + + +@directory.register +class ICx8xRadio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport): + """Icom IC-V/U82""" + VENDOR = "Icom" + MODEL = "IC-V82/U82" + NEEDS_COMPAT_SERIAL = True + + _model = "\x28\x26\x00\x01" + _memsize = 6464 + _endframe = "Icom Inc\x2eCD" + + _memories = [] + + _ranges = [(0x0000, 0x1340, 32), + (0x1340, 0x1360, 16), + (0x1360, 0x136B, 8), + + (0x1370, 0x1440, 32), + + (0x1460, 0x15D0, 32), + + (0x15E0, 0x1930, 32), + + (0x1938, 0x1940, 8), + ] + + MYCALL_LIMIT = (0, 6) + URCALL_LIMIT = (0, 6) + RPTCALL_LIMIT = (0, 6) + + def _get_bank(self, loc): + return icx8x_ll.get_bank(self._mmap, loc) + + def _set_bank(self, loc, bank): + return icx8x_ll.set_bank(self._mmap, loc, bank) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 199) + rf.valid_modes = ["FM", "NFM", "DV"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_duplexes = ["", "-", "+"] + rf.valid_tuning_steps = [5., 10., 12.5, 15., 20., 25., 30., 50.] + if self._isuhf: + rf.valid_bands = [(420000000, 470000000)] + else: + rf.valid_bands = [(118000000, 176000000)] + rf.valid_skips = ["", "S"] + rf.valid_name_length = 5 + rf.valid_special_chans = sorted(icx8x_ll.ICx8x_SPECIAL.keys()) + + return rf + + def _get_type(self): + flag = (_isuhf(self) != 0) + + if self._isuhf is not None and (self._isuhf != flag): + raise errors.RadioError("VHF/UHF model mismatch") + + self._isuhf = flag + + return flag + + def __init__(self, pipe): + icf.IcomCloneModeRadio.__init__(self, pipe) + + # Until I find a better way, I'll stash a boolean to indicate + # UHF-ness in an unused region of memory. If we're opening a + # file, look for the flag. If we're syncing from serial, set + # that flag. + if isinstance(pipe, str): + self._isuhf = (ord(self._mmap[0x1930]) != 0) + # LOG.debug("Found %s image" % (self.isUHF and "UHF" or "VHF")) + else: + self._isuhf = None + + def sync_in(self): + self._get_type() + icf.IcomCloneModeRadio.sync_in(self) + self._mmap[0x1930] = self._isuhf and 1 or 0 + + def sync_out(self): + self._get_type() + icf.IcomCloneModeRadio.sync_out(self) + + def get_memory(self, number): + if not self._mmap: + self.sync_in() + + if self._isuhf: + base = 400 + else: + base = 0 + + if isinstance(number, str): + try: + number = icx8x_ll.ICx8x_SPECIAL[number] + except KeyError: + raise errors.InvalidMemoryLocation("Unknown channel %s" % + number) + + return icx8x_ll.get_memory(self._mmap, number, base) + + def set_memory(self, memory): + if not self._mmap: + self.sync_in() + + if self._isuhf: + base = 400 + else: + base = 0 + + if memory.empty: + self._mmap = icx8x_ll.erase_memory(self._mmap, memory.number) + else: + self._mmap = icx8x_ll.set_memory(self._mmap, memory, base) + + def get_raw_memory(self, number): + return icx8x_ll.get_raw_memory(self._mmap, number) + + def get_urcall_list(self): + calls = [] + + for i in range(*self.URCALL_LIMIT): + call = icx8x_ll.get_urcall(self._mmap, i) + calls.append(call) + + return calls + + def get_repeater_call_list(self): + calls = [] + + for i in range(*self.RPTCALL_LIMIT): + call = icx8x_ll.get_rptcall(self._mmap, i) + calls.append(call) + + return calls + + def get_mycall_list(self): + calls = [] + + for i in range(*self.MYCALL_LIMIT): + call = icx8x_ll.get_mycall(self._mmap, i) + calls.append(call) + + return calls + + def set_urcall_list(self, calls): + for i in range(*self.URCALL_LIMIT): + try: + call = calls[i] + except IndexError: + call = " " * 8 + + icx8x_ll.set_urcall(self._mmap, i, call) + + def set_repeater_call_list(self, calls): + for i in range(*self.RPTCALL_LIMIT): + try: + call = calls[i] + except IndexError: + call = " " * 8 + + icx8x_ll.set_rptcall(self._mmap, i, call) + + def set_mycall_list(self, calls): + for i in range(*self.MYCALL_LIMIT): + try: + call = calls[i] + except IndexError: + call = " " * 8 + + icx8x_ll.set_mycall(self._mmap, i, call) diff --git a/chirp/drivers/icx8x_ll.py b/chirp/drivers/icx8x_ll.py new file mode 100644 index 0000000..ea380a2 --- /dev/null +++ b/chirp/drivers/icx8x_ll.py @@ -0,0 +1,539 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct + +from chirp import chirp_common, errors +from chirp.memmap import MemoryMap +from chirp.chirp_common import to_MHz +from chirp.util import StringStruct as struct + +POS_FREQ_START = 0 +POS_FREQ_END = 2 +POS_OFFSET = 2 +POS_NAME_START = 4 +POS_NAME_END = 9 +POS_RTONE = 9 +POS_CTONE = 10 +POS_DTCS = 11 +POS_TUNE_STEP = 17 +POS_TMODE = 21 +POS_MODE = 21 +POS_MULT_FLAG = 21 +POS_DTCS_POL = 22 +POS_DUPLEX = 22 +POS_DIG = 23 +POS_TXI = 23 + +POS_FLAGS_START = 0x1370 +POS_MYCALL = 0x15E0 +POS_URCALL = 0x1640 +POS_RPCALL = 0x16A0 +POS_RP2CALL = 0x1700 + +MEM_LOC_SIZE = 24 + +ICx8x_SPECIAL = {"C": 206} +ICx8x_SPECIAL_REV = {206: "C"} + +for i in range(0, 3): + idA = "%iA" % i + idB = "%iB" % i + num = 200 + i * 2 + ICx8x_SPECIAL[idA] = num + ICx8x_SPECIAL[idB] = num + 1 + ICx8x_SPECIAL_REV[num] = idA + ICx8x_SPECIAL_REV[num+1] = idB + + +def bank_name(index): + char = chr(ord("A") + index) + return "BANK-%s" % char + + +def get_freq(mmap, base): + if (ord(mmap[POS_MULT_FLAG]) & 0x80) == 0x80: + mult = 6250 + else: + mult = 5000 + + val = struct.unpack(">= 4 + icx8x_ts = list(chirp_common.TUNING_STEPS) + del icx8x_ts[1] + + try: + return icx8x_ts[tsidx] + except IndexError: + raise errors.InvalidDataError("TS index %i out of range (%i)" % + (tsidx, len(icx8x_ts))) + + +def set_tune_step(mmap, tstep): + val = struct.unpack("B", mmap[POS_TUNE_STEP])[0] & 0x0F + icx8x_ts = list(chirp_common.TUNING_STEPS) + del icx8x_ts[1] + + tsidx = icx8x_ts.index(tstep) + val |= (tsidx << 4) + + mmap[POS_TUNE_STEP] = val + + +def get_mode(mmap): + val = struct.unpack("B", mmap[POS_DIG])[0] & 0x08 + + if val == 0x08: + return "DV" + + val = struct.unpack("B", mmap[POS_MODE])[0] & 0x20 + + if val == 0x20: + return "NFM" + else: + return "FM" + + +def set_mode(mmap, mode): + dig = struct.unpack("B", mmap[POS_DIG])[0] & 0xF7 + + val = struct.unpack("B", mmap[POS_MODE])[0] & 0xDF + + if mode == "FM": + pass + elif mode == "NFM": + val |= 0x20 + elif mode == "DV": + dig |= 0x08 + else: + raise errors.InvalidDataError("%s mode not supported" % mode) + + mmap[POS_DIG] = dig + mmap[POS_MODE] = val + + +def is_used(mmap, number): + if number == ICx8x_SPECIAL["C"]: + return True + + return (ord(mmap[POS_FLAGS_START + number]) & 0x20) == 0 + + +def set_used(mmap, number, used=True): + if number == ICx8x_SPECIAL["C"]: + return + + val = struct.unpack("B", mmap[POS_FLAGS_START + number])[0] & 0xDF + + if not used: + val |= 0x20 + + mmap[POS_FLAGS_START + number] = val + + +def get_skip(mmap, number): + val = struct.unpack("B", mmap[POS_FLAGS_START + number])[0] & 0x10 + + if val != 0: + return "S" + else: + return "" + + +def set_skip(mmap, number, skip): + if skip == "P": + raise errors.InvalidDataError("PSKIP not supported by this model") + + val = struct.unpack("B", mmap[POS_FLAGS_START + number])[0] & 0xEF + + if skip == "S": + val |= 0x10 + + mmap[POS_FLAGS_START + number] = val + + +def get_call_indices(mmap): + return ord(mmap[18]) & 0x0F, \ + (ord(mmap[19]) & 0xF0) >> 4, \ + ord(mmap[19]) & 0x0F + + +def set_call_indices(_map, mmap, urcall, r1call, r2call): + ulist = [] + for i in range(0, 6): + ulist.append(get_urcall(_map, i)) + + rlist = [] + for i in range(0, 6): + rlist.append(get_rptcall(_map, i)) + + try: + if not urcall: + uindex = 0 + else: + uindex = ulist.index(urcall) + except ValueError: + raise errors.InvalidDataError("Call `%s' not in URCALL list" % urcall) + + try: + if not r1call: + r1index = 0 + else: + r1index = rlist.index(r1call) + except ValueError: + raise errors.InvalidDataError("Call `%s' not in RCALL list" % r1call) + + try: + if not r2call: + r2index = 0 + else: + r2index = rlist.index(r2call) + except ValueError: + raise errors.InvalidDataError("Call `%s' not in RCALL list" % r2call) + + mmap[18] = (ord(mmap[18]) & 0xF0) | uindex + mmap[19] = (r1index << 4) | r2index + +# -- + + +def get_mem_offset(number): + return number * MEM_LOC_SIZE + + +def get_raw_memory(mmap, number): + offset = get_mem_offset(number) + return MemoryMap(mmap[offset:offset + MEM_LOC_SIZE]) + + +def get_bank(mmap, number): + val = ord(mmap[POS_FLAGS_START + number]) & 0x0F + + if val >= 10: + return None + else: + return val + + +def set_bank(mmap, number, bank): + if bank is not None and bank > 9: + raise errors.InvalidDataError("Invalid bank number %i" % bank) + + if bank is None: + index = 0x0A + else: + index = bank + + val = ord(mmap[POS_FLAGS_START + number]) & 0xF0 + val |= index + mmap[POS_FLAGS_START + number] = val + + +def _get_memory(_map, mmap, base): + if get_mode(mmap) == "DV": + mem = chirp_common.DVMemory() + i_ucall, i_r1call, i_r2call = get_call_indices(mmap) + mem.dv_urcall = get_urcall(_map, i_ucall) + mem.dv_rpt1call = get_rptcall(_map, i_r1call) + mem.dv_rpt2call = get_rptcall(_map, i_r2call) + else: + mem = chirp_common.Memory() + + mem.freq = get_freq(mmap, base) + mem.name = get_name(mmap) + mem.rtone = get_rtone(mmap) + mem.ctone = get_ctone(mmap) + mem.dtcs = get_dtcs(mmap) + mem.dtcs_polarity = get_dtcs_polarity(mmap) + mem.offset = get_dup_offset(mmap) + mem.duplex = get_duplex(mmap) + mem.tmode = get_tone_enabled(mmap) + mem.tuning_step = get_tune_step(mmap) + mem.mode = get_mode(mmap) + + return mem + + +def get_memory(_map, number, base): + if not is_used(_map, number): + mem = chirp_common.Memory() + if number < 200: + mem.number = number + mem.empty = True + return mem + else: + mmap = get_raw_memory(_map, number) + mem = _get_memory(_map, mmap, base) + + mem.number = number + + if number < 200: + mem.skip = get_skip(_map, number) + else: + mem.extd_number = ICx8x_SPECIAL_REV[number] + mem.immutable = ["number", "skip", "bank", "bank_index", "extd_number"] + + return mem + + +def clear_tx_inhibit(mmap): + txi = struct.unpack("B", mmap[POS_TXI])[0] + txi |= 0x40 + mmap[POS_TXI] = txi + + +def set_memory(_map, memory, base): + mmap = get_raw_memory(_map, memory.number) + + set_freq(mmap, memory.freq, base) + set_name(mmap, memory.name) + set_rtone(mmap, memory.rtone) + set_ctone(mmap, memory.ctone) + set_dtcs(mmap, memory.dtcs) + set_dtcs_polarity(mmap, memory.dtcs_polarity) + set_dup_offset(mmap, memory.offset) + set_duplex(mmap, memory.duplex) + set_tone_enabled(mmap, memory.tmode) + set_tune_step(mmap, memory.tuning_step) + set_mode(mmap, memory.mode) + if memory.number < 200: + set_skip(_map, memory.number, memory.skip) + + if isinstance(memory, chirp_common.DVMemory): + set_call_indices(_map, + mmap, + memory.dv_urcall, + memory.dv_rpt1call, + memory.dv_rpt2call) + + if not is_used(_map, memory.number): + clear_tx_inhibit(mmap) + + _map[get_mem_offset(memory.number)] = mmap.get_packed() + set_used(_map, memory.number) + + return _map + + +def erase_memory(_map, number): + set_used(_map, number, False) + + return _map + + +def call_location(base, index): + return base + (16 * index) + + +def get_urcall(mmap, index): + if index > 5: + raise errors.InvalidDataError("URCALL index %i must be <= 5" % index) + + start = call_location(POS_URCALL, index) + + return mmap[start:start+8].rstrip() + + +def get_rptcall(mmap, index): + if index > 5: + raise errors.InvalidDataError("RPTCALL index %i must be <= 5" % index) + + start = call_location(POS_RPCALL, index) + + return mmap[start:start+8].rstrip() + + +def get_mycall(mmap, index): + if index > 5: + raise errors.InvalidDataError("MYCALL index %i must be <= 5" % index) + + start = call_location(POS_MYCALL, index) + + return mmap[start:start+8].rstrip() + + +def set_urcall(mmap, index, call): + if index > 5: + raise errors.InvalidDataError("URCALL index %i must be <= 5" % index) + + start = call_location(POS_URCALL, index) + + mmap[start] = call.ljust(12) + + return mmap + + +def set_rptcall(mmap, index, call): + if index > 5: + raise errors.InvalidDataError("RPTCALL index %i must be <= 5" % index) + + start = call_location(POS_RPCALL, index) + mmap[start] = call.ljust(12) + + start = call_location(POS_RP2CALL, index) + mmap[start] = call.ljust(12) + + return mmap + + +def set_mycall(mmap, index, call): + if index > 5: + raise errors.InvalidDataError("MYCALL index %i must be <= 5" % index) + + start = call_location(POS_MYCALL, index) + + mmap[start] = call.ljust(12) + + return mmap diff --git a/chirp/drivers/id31.py b/chirp/drivers/id31.py new file mode 100644 index 0000000..97d1c4b --- /dev/null +++ b/chirp/drivers/id31.py @@ -0,0 +1,337 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import directory, bitwise, chirp_common + +MEM_FORMAT = """ +struct { + u24 freq; + u16 offset; + u16 rtone:6, + ctone:6, + unknown2:1, + mode:3; + u8 dtcs; + u8 tune_step:4, + unknown5:4; + u8 unknown4; + u8 tmode:4, + duplex:2, + dtcs_polarity:2; + char name[16]; + u8 unknow13; + u8 urcall[7]; + u8 rpt1call[7]; + u8 rpt2call[7]; +} memory[500]; + +#seekto 0x69C0; +u8 used_flags[70]; + +#seekto 0x6A06; +u8 skip_flags[69]; + +#seekto 0x6A4B; +u8 pskp_flags[69]; + +#seekto 0x6AC0; +struct { + u8 bank; + u8 index; +} banks[500]; + +#seekto 0x6F50; +struct { + char name[16]; +} bank_names[26]; + +#seekto 0x74BF; +struct { + u8 unknown0; + u24 freq; + u16 offset; + u8 unknown1[3]; + u8 call[7]; + char name[16]; + char subname[8]; + u8 unknown3[9]; +} repeaters[700]; + +#seekto 0xFABC; +struct { + u8 call[7]; +} rptcall[700]; + +#seekto 0x10F20; +struct { + char call[8]; + char tag[4]; +} mycall[6]; + +#seekto 0x10F68; +struct { + char call[8]; +} urcall[200]; + +""" + +TMODES = ["", "Tone", "TSQL", "TSQL", "DTCS", "DTCS", "TSQL-R", "DTCS-R"] +DUPLEX = ["", "-", "+"] +DTCS_POLARITY = ["NN", "NR", "RN", "RR"] +TUNING_STEPS = [5.0, 6.25, 0, 0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, + 100.0, 125.0, 200.0] + + +def _decode_call(_call): + # Why Icom, why? + call = "" + shift = 1 + acc = 0 + for val in _call: + mask = (1 << (shift)) - 1 + call += chr((val >> shift) | acc) + acc = (val & mask) << (7 - shift) + shift += 1 + call += chr(acc) + return call + + +def _encode_call(call): + _call = [0x00] * 7 + for i in range(0, 7): + val = ord(call[i]) << (i + 1) + if i > 0: + _call[i-1] |= (val & 0xFF00) >> 8 + _call[i] = val + _call[6] |= (ord(call[7]) & 0x7F) + + return _call + + +def _get_freq(_mem): + freq = int(_mem.freq) + offs = int(_mem.offset) + + if freq & 0x00200000: + mult = 6250 + else: + mult = 5000 + + freq &= 0x0003FFFF + + return (freq * mult), (offs * mult) + + +def _set_freq(_mem, freq, offset): + if chirp_common.is_fractional_step(freq): + mult = 6250 + flag = 0x00200000 + else: + mult = 5000 + flag = 0x00000000 + + _mem.freq = (freq // mult) | flag + _mem.offset = (offset // mult) + + +class ID31Bank(icf.IcomBank): + """A ID-31 Bank""" + def get_name(self): + _banks = self._model._radio._memobj.bank_names + return str(_banks[self.index].name).rstrip() + + def set_name(self, name): + _banks = self._model._radio._memobj.bank_names + _banks[self.index].name = str(name).ljust(16)[:16] + + +@directory.register +class ID31Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport): + """Icom ID-31""" + MODEL = "ID-31A" + + _memsize = 0x15500 + _model = "\x33\x22\x00\x01" + _endframe = "Icom Inc\x2E\x41\x38" + _num_banks = 26 + _bank_class = ID31Bank + _can_hispeed = True + + _ranges = [(0x00000, 0x15500, 32)] + + MODES = {0: "FM", 1: "NFM", 5: "DV"} + + def _get_bank(self, loc): + _bank = self._memobj.banks[loc] + if _bank.bank == 0xFF: + return None + else: + return _bank.bank + + def _set_bank(self, loc, bank): + _bank = self._memobj.banks[loc] + if bank is None: + _bank.bank = 0xFF + else: + _bank.bank = bank + + def _get_bank_index(self, loc): + _bank = self._memobj.banks[loc] + return _bank.index + + def _set_bank_index(self, loc, index): + _bank = self._memobj.banks[loc] + _bank.index = index + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 499) + rf.valid_bands = [(400000000, 479000000)] + rf.has_settings = True + rf.has_ctone = True + rf.has_bank_index = True + rf.has_bank_names = True + rf.valid_tmodes = list(TMODES) + rf.valid_tuning_steps = sorted(list(TUNING_STEPS)) + rf.valid_modes = list(self.MODES.values()) + rf.valid_skips = ["", "S", "P"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 16 + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _usd = self._memobj.used_flags[number / 8] + _skp = self._memobj.skip_flags[number / 8] + _psk = self._memobj.pskp_flags[number / 8] + + bit = (1 << (number % 8)) + + if self.MODES[int(_mem.mode)] == "DV": + mem = chirp_common.DVMemory() + else: + mem = chirp_common.Memory() + mem.number = number + + if _usd & bit: + mem.empty = True + return mem + + mem.freq, mem.offset = _get_freq(_mem) + mem.name = str(_mem.name).rstrip() + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_polarity] + mem.tuning_step = TUNING_STEPS[_mem.tune_step] + mem.mode = self.MODES[int(_mem.mode)] + + if mem.mode == "DV": + mem.dv_urcall = _decode_call(_mem.urcall).rstrip() + mem.dv_rpt1call = _decode_call(_mem.rpt1call).rstrip() + mem.dv_rpt2call = _decode_call(_mem.rpt2call).rstrip() + + if _psk & bit: + mem.skip = "P" + elif _skp & bit: + mem.skip = "S" + + return mem + + def set_memory(self, memory): + _mem = self._memobj.memory[memory.number] + _usd = self._memobj.used_flags[memory.number / 8] + _skp = self._memobj.skip_flags[memory.number / 8] + _psk = self._memobj.pskp_flags[memory.number / 8] + + bit = (1 << (memory.number % 8)) + + if memory.empty: + _usd |= bit + self._set_bank(memory.number, None) + return + + _usd &= ~bit + + _set_freq(_mem, memory.freq, memory.offset) + _mem.name = memory.name.ljust(16)[:16] + _mem.rtone = chirp_common.TONES.index(memory.rtone) + _mem.ctone = chirp_common.TONES.index(memory.ctone) + _mem.tmode = TMODES.index(memory.tmode) + _mem.duplex = DUPLEX.index(memory.duplex) + _mem.dtcs = chirp_common.DTCS_CODES.index(memory.dtcs) + _mem.dtcs_polarity = DTCS_POLARITY.index(memory.dtcs_polarity) + _mem.tune_step = TUNING_STEPS.index(memory.tuning_step) + _mem.mode = next(i for i, mode in list(self.MODES.items()) + if mode == memory.mode) + + if isinstance(memory, chirp_common.DVMemory): + _mem.urcall = _encode_call(memory.dv_urcall.ljust(8)) + _mem.rpt1call = _encode_call(memory.dv_rpt1call.ljust(8)) + _mem.rpt2call = _encode_call(memory.dv_rpt2call.ljust(8)) + elif memory.mode == "DV": + raise Exception("BUG") + + if memory.skip == "S": + _skp |= bit + _psk &= ~bit + elif memory.skip == "P": + _skp |= bit + _psk |= bit + else: + _skp &= ~bit + _psk &= ~bit + + def get_urcall_list(self): + calls = [] + for i in range(0, 200): + call = str(self._memobj.urcall[i].call) + if call == "CALLSIGN": + call = "" + calls.append(call) + return calls + + def get_mycall_list(self): + calls = [] + for i in range(0, 6): + calls.append(str(self._memobj.mycall[i].call)) + return calls + + def get_repeater_call_list(self): + calls = [] + for rptcall in self._memobj.rptcall: + call = _decode_call(rptcall.call) + if call.rstrip() and not call == "CALLSIGN": + calls.append(call) + for repeater in self._memobj.repeaters: + call = _decode_call(repeater.call) + if call == "CALLSIGN": + call = "" + calls.append(call.rstrip()) + return calls + +if __name__ == "__main__": + print(repr(_decode_call(_encode_call("KD7REX B")))) + print(repr(_decode_call(_encode_call(" B")))) + print(repr(_decode_call(_encode_call(" ")))) diff --git a/chirp/drivers/id51.py b/chirp/drivers/id51.py new file mode 100644 index 0000000..c914230 --- /dev/null +++ b/chirp/drivers/id51.py @@ -0,0 +1,136 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from builtins import bytes + +import logging + +from chirp.drivers import id31 +from chirp import directory, bitwise + +MEM_FORMAT = """ +struct { + u24 freq; + u16 offset; + u16 rtone:6, + ctone:6, + unknown2:1, + mode:3; + u8 dtcs; + u8 tune_step:4, + unknown5:4; + u8 unknown4; + u8 tmode:4, + duplex:2, + dtcs_polarity:2; + char name[16]; + u8 unknown13; + u8 urcall[7]; + u8 rpt1call[7]; + u8 rpt2call[7]; +} memory[500]; + +#seekto 0x6A40; +u8 used_flags[70]; + +#seekto 0x6A86; +u8 skip_flags[69]; + +#seekto 0x6ACB; +u8 pskp_flags[69]; + +#seekto 0x6B40; +struct { + u8 bank; + u8 index; +} banks[500]; + +#seekto 0x6FD0; +struct { + char name[16]; +} bank_names[26]; + +#seekto 0xA8C0; +struct { + u24 freq; + u16 offset; + u8 unknown1[3]; + u8 call[7]; + char name[16]; + char subname[8]; + u8 unknown3[10]; +} repeaters[750]; + +#seekto 0x1384E; +struct { + u8 call[7]; +} rptcall[750]; + +#seekto 0x14E60; +struct { + char call[8]; + char tag[4]; +} mycall[6]; + +#seekto 0x14EA8; +struct { + char call[8]; +} urcall[200]; + +""" +LOG = logging.getLogger(__name__) + + +@directory.register +class ID51Radio(id31.ID31Radio): + """Icom ID-51""" + MODEL = "ID-51" + + _memsize = 0x1FB40 + _model = "\x33\x90\x00\x01" + _endframe = "Icom Inc\x2E\x44\x41" + + _ranges = [(0x00000, 0x1FB40, 32)] + + MODES = {0: "FM", 1: "NFM", 3: "AM", 5: "DV"} + + @classmethod + def match_model(cls, filedata, filename): + """Given contents of a stored file (@filedata), return True if + this radio driver handles the represented model""" + + # The default check for ICOM is just to check memory size + # Since the ID-51 and ID-51 Plus/Anniversary have exactly + # the same memory size, we need to do a more detailed check. + if len(filedata) == cls._memsize: + LOG.debug('File has correct memory size, ' + 'checking 20 bytes at offset 0x1AF40') + snip = bytes(filedata[0x1AF40:0x1AF60]) + if snip == bytes(b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'): + LOG.debug('bytes matched ID-51 Signature') + return True + else: + LOG.debug('bytes did not match ID-51 Signature') + return False + + def get_features(self): + rf = super(ID51Radio, self).get_features() + rf.valid_bands = [(108000000, 174000000), (400000000, 479000000)] + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) diff --git a/chirp/drivers/id51plus.py b/chirp/drivers/id51plus.py new file mode 100644 index 0000000..fc4812f --- /dev/null +++ b/chirp/drivers/id51plus.py @@ -0,0 +1,173 @@ +# Copyright 2015 Eric Dropps +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from builtins import bytes + +import logging + +from chirp.drivers import id31 +from chirp import directory, bitwise + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct { + u24 freq; + u16 offset; + u16 rtone:6, + ctone:6, + unknown2:1, + mode:3; + u8 dtcs; + u8 tune_step:4, + unknown5:4; + u8 unknown4; + u8 tmode:4, + duplex:2, + dtcs_polarity:2; + char name[16]; + u8 unknown13; + u8 urcall[7]; + u8 rpt1call[7]; + u8 rpt2call[7]; +} memory[500]; + +#seekto 0x6A40; +u8 used_flags[70]; + +#seekto 0x6A86; +u8 skip_flags[69]; + +#seekto 0x6ACB; +u8 pskp_flags[69]; + +#seekto 0x6B40; +struct { + u8 unknown:3, + bank:5; + u8 index; +} banks[500]; + +#seekto 0x6FD0; +struct { + char name[16]; +} bank_names[26]; + + +#seekto 0xA8C0; +struct { + u24 freq; + u16 offset; + u8 unknown1[4]; + u8 call[7]; + char name[16]; + char subname[8]; + u8 unknown3[10]; +} repeaters[750]; + +#seekto 0x1384E; +struct { + u8 call[7]; +} rptcall[750]; + +#seekto 0x14FBE; +struct { + char name[16]; +} rptgroup_names[30]; + +#seekto 0x1519E; +struct { + char call[8]; + char tag[4]; +} mycall[6]; + +#seekto 0x151E6; +struct { + char call[8]; +} urcall[200]; + +#seekto 0x15826; +struct { + char name[16]; +} urcallname[200]; +""" + +@directory.register +class ID51PLUSRadio(id31.ID31Radio): + """Icom ID-51 Plus/50th Anniversary""" + MODEL = "ID-51 Plus" + + _memsize = 0x1FB40 + _model = "\x33\x90\x00\x02" + _endframe = "Icom Inc\x2E\x44\x41" + _bank_class = id31.ID31Bank + _ranges = [(0x00000, 0x1FB40, 32)] + + MODES = {0: "FM", 1: "NFM", 3: "AM", 5: "DV"} + + @classmethod + def match_model(cls, filedata, filename): + """Given contents of a stored file (@filedata), return True if + this radio driver handles the represented model""" + + # The default check for ICOM is just to check memory size + # Since the ID-51 and ID-51 Plus/Anniversary have exactly + # the same memory size, we need to do a more detailed check. + if len(filedata) == cls._memsize: + LOG.debug('File has correct memory size, ' + 'checking 20 bytes at offset 0x1AF40') + snip = bytes(filedata[0x1AF40:0x1AF60]) + if snip != bytes(b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'): + LOG.debug('bytes matched ID-51 Plus Signature') + return True + else: + LOG.debug('bytes did not match ID-51 Plus Signature') + return False + + def _get_bank(self, loc): + _bank = self._memobj.banks[loc] + LOG.debug("Bank Value for location %s is %s" % (loc, _bank.bank)) + if _bank.bank == 0x1F: + return None + else: + return _bank.bank + + def _set_bank(self, loc, bank): + _bank = self._memobj.banks[loc] + if bank is None: + _bank.bank = 0x1F + else: + _bank.bank = bank + + def get_features(self): + rf = super(ID51PLUSRadio, self).get_features() + rf.valid_bands = [(108000000, 174000000), (380000000, 479000000)] + return rf + + def get_repeater_call_list(self): + calls = [] + # Unlike previos DStar radios, there is not a seperate repeater + # callsign list. It's only the DV Memory banks. + for repeater in self._memobj.repeaters: + call = id31._decode_call(repeater.call) + if call == "CALLSIGN": + call = "" + calls.append(call.rstrip()) + return calls + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) diff --git a/chirp/drivers/id800.py b/chirp/drivers/id800.py new file mode 100644 index 0000000..f71ec3b --- /dev/null +++ b/chirp/drivers/id800.py @@ -0,0 +1,383 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, errors, directory, bitwise + +MEM_FORMAT = """ +#seekto 0x0020; +struct { + u24 freq; + u16 offset; + u8 unknown0:2, + rtone:6; + u8 duplex:2, + ctone:6; + u8 dtcs; + u8 tuning_step:4, + unknown1:4; + u8 unknown2; + u8 mult_flag:1, + unknown3:5, + tmode:2; + u16 dtcs_polarity:2, + usealpha:1, + empty:1, + name1:6, + name2:6; + u24 name3:6, + name4:6, + name5:6, + name6:6; + u8 unknown5; + u8 unknown6:1, + digital_code:7; + u8 urcall; + u8 rpt1call; + u8 rpt2call; + u8 unknown7:1, + mode:3, + unknown8:4; +} memory[500]; + +#seekto 0x2BF4; +struct { + u8 unknown1:1, + empty:1, + pskip:1, + skip:1, + bank:4; +} flags[500]; + +#seekto 0x3220; +struct { + char call[8]; +} mycalls[8]; + +#seekto 0x3250; +struct { + char call[8]; +} urcalls[99]; + +#seekto 0x3570; +struct { + char call[8]; +} rptcalls[59]; +""" + +MODES = ["FM", "NFM", "AM", "NAM", "DV"] +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "", "-", "+"] +DTCS_POL = ["NN", "NR", "RN", "RR"] +STEPS = [5.0, 10.0, 12.5, 15, 20.0, 25.0, 30.0, 50.0, 100.0, 200.0, 6.25] + +ID800_SPECIAL = { + "C2": 510, + "C1": 511, + } +ID800_SPECIAL_REV = { + 510: "C2", + 511: "C1", + } + +for i in range(0, 5): + idA = "%iA" % (i + 1) + idB = "%iB" % (i + 1) + num = 500 + i * 2 + ID800_SPECIAL[idA] = num + ID800_SPECIAL[idB] = num + 1 + ID800_SPECIAL_REV[num] = idA + ID800_SPECIAL_REV[num+1] = idB + +ALPHA_CHARSET = " ABCDEFGHIJKLMNOPQRSTUVWXYZ" +NUMERIC_CHARSET = "0123456789+-=*/()|" + + +def get_name(_mem): + """Decode the name from @_mem""" + def _get_char(val): + if val == 0: + return " " + elif val & 0x20: + return ALPHA_CHARSET[val & 0x1F] + else: + return NUMERIC_CHARSET[val & 0x0F] + + name_bytes = [_mem.name1, _mem.name2, _mem.name3, + _mem.name4, _mem.name5, _mem.name6] + name = "" + for val in name_bytes: + name += _get_char(val) + + return name.rstrip() + + +def set_name(_mem, name): + """Encode @name in @_mem""" + def _get_index(char): + if char == " ": + return 0 + elif char.isalpha(): + return ALPHA_CHARSET.index(char) | 0x20 + else: + return NUMERIC_CHARSET.index(char) | 0x10 + + name = name.ljust(6)[:6] + + _mem.usealpha = bool(name.strip()) + + # The element override calling convention makes this harder to automate. + # It's just six, so do it manually + _mem.name1 = _get_index(name[0]) + _mem.name2 = _get_index(name[1]) + _mem.name3 = _get_index(name[2]) + _mem.name4 = _get_index(name[3]) + _mem.name5 = _get_index(name[4]) + _mem.name6 = _get_index(name[5]) + + +@directory.register +class ID800v2Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport): + """Icom ID800""" + VENDOR = "Icom" + MODEL = "ID-800H" + VARIANT = "v2" + + _model = "\x27\x88\x02\x00" + _memsize = 14528 + _endframe = "Icom Inc\x2eCB" + _can_hispeed = True + + _memories = [] + + _ranges = [(0x0020, 0x2B18, 32), + (0x2B18, 0x2B20, 8), + (0x2B20, 0x2BE0, 32), + (0x2BE0, 0x2BF4, 20), + (0x2BF4, 0x2C00, 12), + (0x2C00, 0x2DE0, 32), + (0x2DE0, 0x2DF4, 20), + (0x2DF4, 0x2E00, 12), + (0x2E00, 0x2E20, 32), + + (0x2F00, 0x3070, 32), + + (0x30D0, 0x30E0, 16), + (0x30E0, 0x3160, 32), + (0x3160, 0x3180, 16), + (0x3180, 0x31A0, 32), + (0x31A0, 0x31B0, 16), + + (0x3220, 0x3240, 32), + (0x3240, 0x3260, 16), + (0x3260, 0x3560, 32), + (0x3560, 0x3580, 16), + (0x3580, 0x3720, 32), + (0x3720, 0x3780, 8), + + (0x3798, 0x37A0, 8), + (0x37A0, 0x37B0, 16), + (0x37B0, 0x37B1, 1), + + (0x37D8, 0x37E0, 8), + (0x37E0, 0x3898, 32), + (0x3898, 0x389A, 2), + + (0x38A8, 0x38C0, 16), ] + + MYCALL_LIMIT = (1, 7) + URCALL_LIMIT = (1, 99) + RPTCALL_LIMIT = (1, 59) + + def _get_bank(self, loc): + _flg = self._memobj.flags[loc-1] + if _flg.bank >= 0x0A: + return None + else: + return _flg.bank + + def _set_bank(self, loc, bank): + _flg = self._memobj.flags[loc-1] + if bank is None: + _flg.bank = 0x0A + else: + _flg.bank = bank + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_implicit_calls = True + rf.has_settings = True + rf.has_bank = True + rf.valid_modes = list(MODES) + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_duplexes = ["", "-", "+"] + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(118000000, 173995000), (230000000, 549995000), + (810000000, 999990000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_name_length = 6 + rf.valid_special_chans = sorted(ID800_SPECIAL.keys()) + rf.memory_bounds = (1, 499) + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_memory(self, number): + if isinstance(number, str): + try: + number = ID800_SPECIAL[number] + 1 # Because we subtract below + except KeyError: + raise errors.InvalidMemoryLocation("Unknown channel %s" % + number) + + _mem = self._memobj.memory[number-1] + _flg = self._memobj.flags[number-1] + + if MODES[_mem.mode] == "DV": + urcalls = self.get_urcall_list() + rptcalls = self.get_repeater_call_list() + mem = chirp_common.DVMemory() + mem.dv_urcall = urcalls[_mem.urcall] + mem.dv_rpt1call = rptcalls[_mem.rpt1call] + mem.dv_rpt2call = rptcalls[_mem.rpt2call] + mem.dv_code = _mem.digital_code + else: + mem = chirp_common.Memory() + + mem.number = number + if _flg.empty: + mem.empty = True + return mem + + mult = _mem.mult_flag and 6250 or 5000 + mem.freq = _mem.freq * mult + mem.offset = _mem.offset * 5000 + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = MODES[_mem.mode] + mem.tmode = TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCS_POL[_mem.dtcs_polarity] + mem.tuning_step = STEPS[_mem.tuning_step] + mem.name = get_name(_mem) + + mem.skip = _flg.pskip and "P" or _flg.skip and "S" or "" + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + _flg = self._memobj.flags[mem.number-1] + + _flg.empty = mem.empty + if mem.empty: + self._set_bank(mem.number, None) + return + + mult = chirp_common.is_fractional_step(mem.freq) and 6250 or 5000 + _mem.mult_flag = mult == 6250 + _mem.freq = mem.freq / mult + _mem.offset = mem.offset / 5000 + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.mode = MODES.index(mem.mode) + _mem.tmode = TMODES.index(mem.tmode) + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.dtcs_polarity = DTCS_POL.index(mem.dtcs_polarity) + _mem.tuning_step = STEPS.index(mem.tuning_step) + set_name(_mem, mem.name) + + _flg.pskip = mem.skip == "P" + _flg.skip = mem.skip == "S" + + if mem.mode == "DV": + urcalls = self.get_urcall_list() + rptcalls = self.get_repeater_call_list() + if not isinstance(mem, chirp_common.DVMemory): + raise errors.InvalidDataError("DV mode is not a DVMemory!") + try: + err = mem.dv_urcall + _mem.urcall = urcalls.index(mem.dv_urcall) + err = mem.dv_rpt1call + _mem.rpt1call = rptcalls.index(mem.dv_rpt1call) + err = mem.dv_rpt2call + _mem.rpt2call = rptcalls.index(mem.dv_rpt2call) + except IndexError: + raise errors.InvalidDataError("DV Call %s not in list" % err) + else: + _mem.urcall = 0 + _mem.rpt1call = 0 + _mem.rpt2call = 0 + + def sync_in(self): + icf.IcomCloneModeRadio.sync_in(self) + self.process_mmap() + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def get_urcall_list(self): + calls = ["CQCQCQ"] + + for i in range(*self.URCALL_LIMIT): + calls.append(str(self._memobj.urcalls[i-1].call).rstrip()) + + return calls + + def get_repeater_call_list(self): + calls = ["*NOTUSE*"] + + for i in range(*self.RPTCALL_LIMIT): + calls.append(str(self._memobj.rptcalls[i-1].call).rstrip()) + + return calls + + def get_mycall_list(self): + calls = [] + + for i in range(*self.MYCALL_LIMIT): + calls.append(str(self._memobj.mycalls[i-1].call).rstrip()) + + return calls + + def set_urcall_list(self, calls): + for i in range(*self.URCALL_LIMIT): + try: + call = calls[i].upper() # Skip the implicit CQCQCQ + except IndexError: + call = " " * 8 + + self._memobj.urcalls[i-1].call = call.ljust(8)[:8] + + def set_repeater_call_list(self, calls): + for i in range(*self.RPTCALL_LIMIT): + try: + call = calls[i].upper() # Skip the implicit blank + except IndexError: + call = " " * 8 + + self._memobj.rptcalls[i-1].call = call.ljust(8)[:8] + + def set_mycall_list(self, calls): + for i in range(*self.MYCALL_LIMIT): + try: + call = calls[i-1].upper() + except IndexError: + call = " " * 8 + + self._memobj.mycalls[i-1].call = call.ljust(8)[:8] diff --git a/chirp/drivers/id880.py b/chirp/drivers/id880.py new file mode 100644 index 0000000..19c2c00 --- /dev/null +++ b/chirp/drivers/id880.py @@ -0,0 +1,394 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import icf +from chirp import chirp_common, directory, bitwise + +MEM_FORMAT = """ +struct { + u24 rxmult:3, + txmult:3, + freq:18; + u16 offset; + u16 rtone:6, + ctone:6, + unknown1:1, + mode:3; + u8 dtcs; + u8 tune_step:4, + unknown2:4; + u8 unknown3; + u8 unknown4:1, + tmode:3, + duplex:2, + dtcs_polarity:2; + char name[8]; + u8 unknwon5:1, + digital_code:7; + char urcall[7]; + char r1call[7]; + char r2call[7]; +} memory[1000]; + +#seekto 0xAA80; +u8 used_flags[132]; + +#seekto 0xAB04; +u8 skip_flags[132]; +u8 pskip_flags[132]; + +#seekto 0xAD00; +struct { + u8 bank; + u8 index; +} bank_info[1000]; + +#seekto 0xB550; +struct { + char name[6]; +} bank_names[26]; + +#seekto 0xDE56; +struct { + char call[8]; + char extension[4]; +} mycall[6]; + +struct { + char call[8]; +} urcall[60]; + +struct { + char call[8]; + char extension[4]; +} rptcall[99]; + +#seekto 0x0FB8; +u8 name_flags[132]; + +""" + +TMODES = ["", "Tone", "?2", "TSQL", "DTCS", "TSQL-R", "DTCS-R", ""] +DUPLEX = ["", "-", "+", "?3"] +DTCSP = ["NN", "NR", "RN", "RR"] +MODES = ["FM", "NFM", "?2", "AM", "NAM", "DV"] +STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, + 100.0, 125.0, 200.0] +FREQ_MULTIPLIER = [5000, 6250, 6250, 8333, 9000] + + +def decode_call(sevenbytes): + """Decode a callsign from a packed region @sevenbytes""" + if len(sevenbytes) != 7: + raise Exception("%i (!=7) bytes to decode_call" % len(sevenbytes)) + + i = 0 + rem = 0 + call = "" + for byte in [ord(x) for x in sevenbytes]: + i += 1 + + # Mask is 0x01, 0x03, 0x07, etc + mask = (1 << i) - 1 + + # Code gets the upper bits of remainder plus all but the i lower + # bits of this byte + code = (byte >> i) | rem + call += chr(code) + + # Remainder for next time are the masked bits, moved to the high + # places for the next round + rem = (byte & mask) << 7 - i + + # After seven trips gathering overflow bits, we chould have seven + # left, which is the final character + call += chr(rem) + + return call.rstrip() + + +def encode_call(call): + """Encode @call into a 7-byte region""" + call = call.ljust(8) + buf = [] + + for i in range(0, 8): + byte = ord(call[i]) + if i > 0: + last = buf[i-1] + himask = ~((1 << (7-i)) - 1) & 0x7F + last |= (byte & himask) >> (7-i) + buf[i-1] = last + else: + himask = 0 + + buf.append((byte & ~himask) << (i+1)) + + return "".join([chr(x) for x in buf[:7]]) + + +def _decode_freq(freq, mult): + return int(freq) * FREQ_MULTIPLIER[mult] + + +def _encode_freq(freq): + for i, step in list(enumerate(FREQ_MULTIPLIER)): + if freq % step == 0: + return freq / step, i + raise ValueError("%d cannot be factored by multiplier table." % freq) + + +def _wipe_memory(mem, char): + mem.set_raw(char * (mem.size() // 8)) + + +class ID880Bank(icf.IcomNamedBank): + """ID880 Bank""" + def get_name(self): + _bank = self._model._radio._memobj.bank_names[self.index] + return str(_bank.name).rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_names[self.index] + _bank.name = name.ljust(6)[:6] + + +@directory.register +class ID880Radio(icf.IcomCloneModeRadio, chirp_common.IcomDstarSupport): + """Icom ID880""" + VENDOR = "Icom" + MODEL = "ID-880H" + + _model = "\x31\x67\x00\x01" + _memsize = 62976 + _endframe = "Icom Inc\x2eB1" + + _ranges = [(0x0000, 0xF5c0, 32), + (0xF5c0, 0xf5e0, 16), + (0xf5e0, 0xf600, 32)] + + _num_banks = 26 + _bank_class = ID880Bank + _can_hispeed = True + + MYCALL_LIMIT = (1, 7) + URCALL_LIMIT = (1, 60) + RPTCALL_LIMIT = (1, 99) + + def _get_bank(self, loc): + _bank = self._memobj.bank_info[loc] + if _bank.bank == 0xFF: + return None + else: + return _bank.bank + + def _set_bank(self, loc, bank): + _bank = self._memobj.bank_info[loc] + if bank is None: + _bank.bank = 0xFF + else: + _bank.bank = bank + + def _get_bank_index(self, loc): + _bank = self._memobj.bank_info[loc] + return _bank.index + + def _set_bank_index(self, loc, index): + _bank = self._memobj.bank_info[loc] + _bank.index = index + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.requires_call_lists = False + rf.has_settings = True + rf.has_bank = True + rf.has_bank_index = True + rf.has_bank_names = True + rf.valid_modes = [x for x in MODES if x is not None] + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = STEPS + rf.valid_bands = [(118000000, 173995000), (230000000, 549995000), + (810000000, 823990000), (849000000, 868990000), + (894000000, 999990000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_name_length = 8 + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + \ + "!\"#$%&'()*+,-./:;<=>?@[\]^" + rf.memory_bounds = (0, 999) + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + bytepos = number / 8 + bitpos = 1 << (number % 8) + + _mem = self._memobj.memory[number] + _used = self._memobj.used_flags[bytepos] + + is_used = ((_used & bitpos) == 0) + + if is_used and MODES[_mem.mode] == "DV": + mem = chirp_common.DVMemory() + mem.dv_urcall = decode_call(str(_mem.urcall)) + mem.dv_rpt1call = decode_call(str(_mem.r1call)) + mem.dv_rpt2call = decode_call(str(_mem.r2call)) + else: + mem = chirp_common.Memory() + + mem.number = number + + if number < 1000: + _skip = self._memobj.skip_flags[bytepos] + _pskip = self._memobj.pskip_flags[bytepos] + if _skip & bitpos: + mem.skip = "S" + elif _pskip & bitpos: + mem.skip = "P" + else: + pass # FIXME: Special memories + + if not is_used: + mem.empty = True + return mem + + mem.freq = _decode_freq(_mem.freq, _mem.rxmult) + mem.offset = _decode_freq(_mem.offset, _mem.txmult) + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.dtcs_polarity = DTCSP[_mem.dtcs_polarity] + if _mem.tune_step >= len(STEPS): + mem.tuning_step = 5.0 + else: + mem.tuning_step = STEPS[_mem.tune_step] + mem.name = str(_mem.name).rstrip() + + return mem + + def set_memory(self, mem): + bitpos = (1 << (mem.number % 8)) + bytepos = mem.number / 8 + + _mem = self._memobj.memory[mem.number] + _used = self._memobj.used_flags[bytepos] + _namf = self._memobj.name_flags[bytepos] + + was_empty = _used & bitpos + + if mem.empty: + _used |= bitpos + _wipe_memory(_mem, "\xFF") + self._set_bank(mem.number, None) + return + + _used &= ~bitpos + + if was_empty: + _wipe_memory(_mem, "\x00") + + _mem.freq, _mem.rxmult = _encode_freq(mem.freq) + _mem.offset, _mem.txmult = _encode_freq(mem.offset) + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.mode = MODES.index(mem.mode) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.dtcs_polarity = DTCSP.index(mem.dtcs_polarity) + _mem.tune_step = STEPS.index(mem.tuning_step) + _mem.name = mem.name.ljust(8) + if mem.name.strip(): + _namf |= bitpos + else: + _namf &= ~bitpos + + if isinstance(mem, chirp_common.DVMemory): + _mem.urcall = encode_call(mem.dv_urcall) + _mem.r1call = encode_call(mem.dv_rpt1call) + _mem.r2call = encode_call(mem.dv_rpt2call) + + if mem.number < 1000: + skip = self._memobj.skip_flags[bytepos] + pskip = self._memobj.pskip_flags[bytepos] + if mem.skip == "S": + skip |= bitpos + else: + skip &= ~bitpos + if mem.skip == "P": + pskip |= bitpos + else: + pskip &= ~bitpos + + def get_urcall_list(self): + _calls = self._memobj.urcall + calls = ["CQCQCQ"] + + for i in range(*self.URCALL_LIMIT): + calls.append(str(_calls[i-1].call)) + + return calls + + def get_mycall_list(self): + _calls = self._memobj.mycall + calls = [] + + for i in range(*self.MYCALL_LIMIT): + calls.append(str(_calls[i-1].call)) + + return calls + + def get_repeater_call_list(self): + _calls = self._memobj.rptcall + calls = ["*NOTUSE*"] + + for _i in range(*self.RPTCALL_LIMIT): + # FIXME: Not sure where the repeater list actually is + calls.append("UNSUPRTD") + continue + + return calls + + @classmethod + def match_model(cls, filedata, filename): + # This is a horrid hack, given that people can change the GPS-A + # destination, but it should suffice in most cases until we get + # a rich container file format + return len(filedata) == cls._memsize and "API880," in filedata + + +# This radio isn't really supported yet and detects as a conflict with +# the ID-880. So, don't register right now +@directory.register +class ID80Radio(ID880Radio): + """Icom ID80""" + MODEL = "ID-80H" + + _model = "\x31\x55\x00\x01" + + @classmethod + def match_model(cls, filedata, filename): + # This is a horrid hack, given that people can change the GPS-A + # destination, but it should suffice in most cases until we get + # a rich container file format + return len(filedata) == cls._memsize and "API80," in filedata diff --git a/chirp/drivers/idrp.py b/chirp/drivers/idrp.py new file mode 100644 index 0000000..f7c115d --- /dev/null +++ b/chirp/drivers/idrp.py @@ -0,0 +1,173 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import serial +import logging + +from chirp import chirp_common, errors, util + +LOG = logging.getLogger(__name__) + + +def parse_frames(buf): + """Parse frames from the radio""" + frames = [] + + while "\xfe\xfe" in buf: + try: + start = buf.index("\xfe\xfe") + end = buf[start:].index("\xfd") + start + 1 + except Exception: + LOG.error("Unable to parse frames") + break + + frames.append(buf[start:end]) + buf = buf[end:] + + return frames + + +def send(pipe, buf): + """Send data in @buf to @pipe""" + pipe.write("\xfe\xfe%s\xfd" % buf) + pipe.flush() + + data = "" + while True: + buf = pipe.read(4096) + if not buf: + break + + data += buf + LOG.debug("Got: \n%s" % util.hexprint(buf)) + + return parse_frames(data) + + +def send_magic(pipe): + """Send the magic wakeup call to @pipe""" + send(pipe, ("\xfe" * 15) + "\x01\x7f\x19") + + +def drain(pipe): + """Chew up any data waiting on @pipe""" + while True: + buf = pipe.read(4096) + if not buf: + break + + +def set_freq(pipe, freq): + """Set the frequency of the radio on @pipe to @freq""" + freqbcd = util.bcd_encode(freq, bigendian=False, width=9) + buf = "\x01\x7f\x05" + freqbcd + + drain(pipe) + send_magic(pipe) + resp = send(pipe, buf) + for frame in resp: + if len(frame) == 6: + if frame[4] == "\xfb": + return True + + raise errors.InvalidDataError("Repeater reported error") + + +def get_freq(pipe): + """Get the frequency of the radio attached to @pipe""" + buf = "\x01\x7f\x1a\x09" + + drain(pipe) + send_magic(pipe) + resp = send(pipe, buf) + + for frame in resp: + if frame[4] == "\x03": + els = frame[5:10] + + freq = int("%02x%02x%02x%02x%02x" % (ord(els[4]), + ord(els[3]), + ord(els[2]), + ord(els[1]), + ord(els[0]))) + LOG.debug("Freq: %f" % freq) + return freq + + raise errors.InvalidDataError("No frequency frame received") + +RP_IMMUTABLE = ["number", "skip", "bank", "extd_number", "name", "rtone", + "ctone", "dtcs", "tmode", "dtcs_polarity", "skip", "duplex", + "offset", "mode", "tuning_step", "bank_index"] + + +class IDRPx000V(chirp_common.LiveRadio): + """Icom IDRP-*""" + BAUD_RATE = 19200 + VENDOR = "Icom" + MODEL = "ID-2000V/4000V/2D/2V" + + _model = "0000" # Unknown + mem_upper_limit = 0 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_modes = ["DV"] + rf.valid_tmodes = [] + rf.valid_characters = "" + rf.valid_duplexes = [""] + rf.valid_name_length = 0 + rf.valid_skips = [] + rf.valid_tuning_steps = [] + rf.has_bank = False + rf.has_ctone = False + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_mode = False + rf.has_name = False + rf.has_offset = False + rf.has_tuning_step = False + rf.memory_bounds = (0, 0) + return rf + + def get_memory(self, number): + if number != 0: + raise errors.InvalidMemoryLocation("Repeaters have only one slot") + + mem = chirp_common.Memory() + mem.number = 0 + mem.freq = get_freq(self.pipe) + mem.name = "TX/RX" + mem.mode = "DV" + mem.offset = 0.0 + mem.immutable = RP_IMMUTABLE + + return mem + + def set_memory(self, mem): + if mem.number != 0: + raise errors.InvalidMemoryLocation("Repeaters have only one slot") + + set_freq(self.pipe, mem.freq) + + +def do_test(): + """Get the frequency of /dev/icom""" + ser = serial.Serial(port="/dev/icom", baudrate=19200, timeout=0.5) + # set_freq(pipe, 439.920) + get_freq(ser) + + +if __name__ == "__main__": + do_test() diff --git a/chirp/drivers/kenwood_hmk.py b/chirp/drivers/kenwood_hmk.py new file mode 100644 index 0000000..65a9ccc --- /dev/null +++ b/chirp/drivers/kenwood_hmk.py @@ -0,0 +1,134 @@ +# Copyright 2008 Dan Smith +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import csv +import logging + +from chirp import chirp_common, errors, directory +from chirp.drivers import generic_csv + +LOG = logging.getLogger(__name__) + + +class OmittedHeaderError(Exception): + """An internal exception to indicate that a header was omitted""" + pass + + +@directory.register +class HMKRadio(generic_csv.CSVRadio): + """Kenwood HMK format""" + VENDOR = "Kenwood" + MODEL = "HMK" + FILE_EXTENSION = "hmk" + + DUPLEX_MAP = { + " ": "", + "S": "split", + "+": "+", + "-": "-", + } + + SKIP_MAP = { + "Off": "", + "On": "S", + } + + TMODE_MAP = { + "Off": "", + "T": "Tone", + "CT": "TSQL", + "DCS": "DTCS", + "": "Cross", + } + + ATTR_MAP = { + "!!Ch": (int, "number"), + "M.Name": (str, "name"), + "Rx Freq.": (chirp_common.parse_freq, "freq"), + "Shift/Split": (lambda v: HMKRadio.DUPLEX_MAP[v], "duplex"), + "Offset": (chirp_common.parse_freq, "offset"), + "T/CT/DCS": (lambda v: HMKRadio.TMODE_MAP[v], "tmode"), + "TO Freq.": (float, "rtone"), + "CT Freq.": (float, "ctone"), + "DCS Code": (int, "dtcs"), + "Mode": (str, "mode"), + "Rx Step": (float, "tuning_step"), + "L.Out": (lambda v: HMKRadio.SKIP_MAP[v], "skip"), + } + + def load(self, filename=None): + if filename is None and self._filename is None: + raise errors.RadioError("Need a location to load from") + + if filename: + self._filename = filename + + self._blank() + + f = open(self._filename, "r") + for line in f: + if line.strip() == "// Memory Channels": + break + + reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"') + + good = 0 + lineno = 0 + for line in reader: + lineno += 1 + if lineno == 1: + header = line + continue + + if len(header) > len(line): + LOG.debug("Line %i has %i columns, expected %i" % + (lineno, len(line), len(header))) + self.errors.append("Column number mismatch on line %i" % + lineno) + continue + + # hmk stores Tx Freq. in its own field, but Chirp expects the Tx + # Freq. for odd-split channels to be in the Offset field. + # If channel is odd-split, copy Tx Freq. field to Offset field. + if line[header.index('Shift/Split')] == "S": + line[header.index('Offset')] = line[header.index('Tx Freq.')] + + # fix EU decimal + line = [i.replace(',', '.') for i in line] + + try: + mem = self._parse_csv_data_line(header, line) + if mem.number is None: + raise Exception("Invalid Location field" % lineno) + except Exception as e: + LOG.error("Line %i: %s" % (lineno, e)) + self.errors.append("Line %i: %s" % (lineno, e)) + continue + + self._grow(mem.number) + self.memories[mem.number] = mem + good += 1 + + if not good: + for e in errors: + LOG.error("kenwood_hmk: %s", e) + raise errors.InvalidDataError("No channels found") + + @classmethod + def match_model(cls, filedata, filename): + """Match files ending in .hmk""" + return filename.lower().endswith("." + cls.FILE_EXTENSION) diff --git a/chirp/drivers/kenwood_itm.py b/chirp/drivers/kenwood_itm.py new file mode 100644 index 0000000..dd7fc24 --- /dev/null +++ b/chirp/drivers/kenwood_itm.py @@ -0,0 +1,137 @@ +# Copyright 2008 Dan Smith +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import csv +import logging + +from chirp import chirp_common, errors, directory +from chirp.drivers import generic_csv + +LOG = logging.getLogger(__name__) + + +class OmittedHeaderError(Exception): + """An internal exception to indicate that a header was omitted""" + pass + + +@directory.register +class ITMRadio(generic_csv.CSVRadio): + """Kenwood ITM format""" + VENDOR = "Kenwood" + MODEL = "ITM" + FILE_EXTENSION = "itm" + + ATTR_MAP = { + "CH": (int, "number"), + "RXF": (chirp_common.parse_freq, "freq"), + "NAME": (str, "name"), + } + + def _clean_duplex(self, headers, line, mem): + try: + txfreq = chirp_common.parse_freq( + generic_csv.get_datum_by_header(headers, line, "TXF")) + except ValueError: + mem.duplex = "off" + return mem + + if mem.freq == txfreq: + mem.duplex = "" + elif txfreq: + mem.duplex = "split" + mem.offset = txfreq + + return mem + + def _clean_number(self, headers, line, mem): + zone = int(generic_csv.get_datum_by_header(headers, line, "ZN")) + mem.number = zone * 100 + mem.number + return mem + + def _clean_tmode(self, headers, line, mem): + rtone = eval(generic_csv.get_datum_by_header(headers, line, "TXSIG")) + ctone = eval(generic_csv.get_datum_by_header(headers, line, "RXSIG")) + + if rtone: + mem.tmode = "Tone" + if ctone: + mem.tmode = "TSQL" + + mem.rtone = rtone or 88.5 + mem.ctone = ctone or mem.rtone + + return mem + + def load(self, filename=None): + if filename is None and self._filename is None: + raise errors.RadioError("Need a location to load from") + + if filename: + self._filename = filename + + self._blank() + + f = file(self._filename, "r") + for line in f: + if line.strip() == "// Conventional Data": + break + + reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"') + + good = 0 + lineno = 0 + for line in reader: + lineno += 1 + if lineno == 1: + header = line + continue + + if len(line) == 0: + # End of channel data + break + + if len(header) > len(line): + LOG.error("Line %i has %i columns, expected %i" % + (lineno, len(line), len(header))) + self.errors.append("Column number mismatch on line %i" % + lineno) + continue + + # fix EU decimal + line = [i.replace(',', '.') for i in line] + + try: + mem = self._parse_csv_data_line(header, line) + if mem.number is None: + raise Exception("Invalid Location field" % lineno) + except Exception as e: + LOG.error("Line %i: %s" % (lineno, e)) + self.errors.append("Line %i: %s" % (lineno, e)) + continue + + self._grow(mem.number) + self.memories[mem.number] = mem + good += 1 + + if not good: + for e in errors: + LOG.error("kenwood_itm: %s", e) + raise errors.InvalidDataError("No channels found") + + @classmethod + def match_model(cls, filedata, filename): + return filename.lower().endswith("." + cls.FILE_EXTENSION) diff --git a/chirp/drivers/kenwood_live.py b/chirp/drivers/kenwood_live.py new file mode 100644 index 0000000..33981d5 --- /dev/null +++ b/chirp/drivers/kenwood_live.py @@ -0,0 +1,1764 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import threading +import os +import sys +import time +import logging + +from chirp import chirp_common, errors, directory, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueBoolean, \ + RadioSettingValueString, RadioSettingValueList, RadioSettings + +LOG = logging.getLogger(__name__) + +NOCACHE = "CHIRP_NOCACHE" in os.environ + +DUPLEX = {0: "", 1: "+", 2: "-"} +MODES = {0: "FM", 1: "AM"} +STEPS = list(chirp_common.TUNING_STEPS) +STEPS.append(100.0) + +KENWOOD_TONES = list(chirp_common.TONES) +KENWOOD_TONES.remove(159.8) +KENWOOD_TONES.remove(165.5) +KENWOOD_TONES.remove(171.3) +KENWOOD_TONES.remove(177.3) +KENWOOD_TONES.remove(183.5) +KENWOOD_TONES.remove(189.9) +KENWOOD_TONES.remove(196.6) +KENWOOD_TONES.remove(199.5) + +THF6_MODES = ["FM", "WFM", "AM", "LSB", "USB", "CW"] + + +RADIO_IDS = { + "ID019": "TS-2000", + "ID009": "TS-850", + "ID020": "TS-480_LiveMode", + "ID021": "TS-590S/SG_LiveMode", # S-model uses same class + "ID023": "TS-590S/SG_LiveMode" # as SG +} + +LOCK = threading.Lock() +COMMAND_RESP_BUFSIZE = 8 +LAST_BAUD = 4800 +LAST_DELIMITER = ("\r", " ") + +# The Kenwood TS-2000, TS-480, TS-590 & TS-850 use ";" +# as a CAT command message delimiter, and all others use "\n". +# Also, TS-2000 and TS-590 don't space delimite the command +# fields, but others do. + + +def command(ser, cmd, *args): + """Send @cmd to radio via @ser""" + global LOCK, LAST_DELIMITER, COMMAND_RESP_BUFSIZE + + start = time.time() + + LOCK.acquire() + + # TODO: This global use of LAST_DELIMITER breaks reentrancy + # and needs to be fixed. + if args: + cmd += LAST_DELIMITER[1] + LAST_DELIMITER[1].join(args) + cmd += LAST_DELIMITER[0] + + LOG.debug("PC->RADIO: %s" % cmd.strip()) + ser.write(cmd.encode()) + + result = "" + while not result.endswith(LAST_DELIMITER[0]): + result += ser.read(COMMAND_RESP_BUFSIZE).decode() + if (time.time() - start) > 0.5: + LOG.error("Timeout waiting for data") + break + + if result.endswith(LAST_DELIMITER[0]): + LOG.debug("RADIO->PC: %r" % result.strip()) + result = result[:-1] + else: + LOG.error("Giving up") + + LOCK.release() + + return result.strip() + + +def get_id(ser): + """Get the ID of the radio attached to @ser""" + global LAST_BAUD + bauds = [4800, 9600, 19200, 38400, 57600, 115200] + bauds.remove(LAST_BAUD) + # Make sure LAST_BAUD is last so that it is tried first below + bauds.append(LAST_BAUD) + + global LAST_DELIMITER + command_delimiters = [("\r", " "), (";", "")] + + for delimiter in command_delimiters: + # Process the baud options in reverse order so that we try the + # last one first, and then start with the high-speed ones next + for i in reversed(bauds): + LAST_DELIMITER = delimiter + LOG.info("Trying ID at baud %i with delimiter \"%s\"" % + (i, repr(delimiter))) + ser.baudrate = i + ser.write(LAST_DELIMITER[0].encode()) + ser.read(25) + resp = command(ser, "ID") + + # most kenwood radios + if " " in resp: + LAST_BAUD = i + return resp.split(" ")[1] + + # Radio responded in the right baud rate, + # but threw an error because of all the crap + # we have been hurling at it. Retry the ID at this + # baud rate, which will almost definitely work. + if "?" in resp: + resp = command(ser, "ID") + LAST_BAUD = i + if " " in resp: + return resp.split(" ")[1] + + # Kenwood radios that return ID numbers + if resp in list(RADIO_IDS.keys()): + return RADIO_IDS[resp] + + raise errors.RadioError("No response from radio") + + +def get_tmode(tone, ctcss, dcs): + """Get the tone mode based on the values of the tone, ctcss, dcs""" + if dcs and int(dcs) == 1: + return "DTCS" + elif int(ctcss): + return "TSQL" + elif int(tone): + return "Tone" + else: + return "" + + +def iserr(result): + """Returns True if the @result from a radio is an error""" + return result in ["N", "?"] + + +class KenwoodLiveRadio(chirp_common.LiveRadio): + """Base class for all live-mode kenwood radios""" + BAUD_RATE = 9600 + VENDOR = "Kenwood" + MODEL = "" + NEEDS_COMPAT_SERIAL = False + + _vfo = 0 + _upper = 200 + _kenwood_split = False + _kenwood_valid_tones = list(chirp_common.TONES) + + def __init__(self, *args, **kwargs): + chirp_common.LiveRadio.__init__(self, *args, **kwargs) + + self._memcache = {} + + if self.pipe: + self.pipe.timeout = 0.1 + radio_id = get_id(self.pipe) + if radio_id != self.MODEL.split(" ")[0]: + raise Exception("Radio reports %s (not %s)" % (radio_id, + self.MODEL)) + + command(self.pipe, "AI", "0") + + def _cmd_get_memory(self, number): + return "MR", "%i,0,%03i" % (self._vfo, number) + + def _cmd_get_memory_name(self, number): + return "MNA", "%i,%03i" % (self._vfo, number) + + def _cmd_get_split(self, number): + return "MR", "%i,1,%03i" % (self._vfo, number) + + def _cmd_set_memory(self, number, spec): + if spec: + spec = "," + spec + return "MW", "%i,0,%03i%s" % (self._vfo, number, spec) + + def _cmd_set_memory_name(self, number, name): + return "MNA", "%i,%03i,%s" % (self._vfo, number, name) + + def _cmd_set_split(self, number, spec): + return "MW", "%i,1,%03i,%s" % (self._vfo, number, spec) + + def get_raw_memory(self, number): + return command(self.pipe, *self._cmd_get_memory(number)) + + def get_memory(self, number): + if number < 0 or number > self._upper: + raise errors.InvalidMemoryLocation( + "Number must be between 0 and %i" % self._upper) + if number in self._memcache and not NOCACHE: + return self._memcache[number] + + result = command(self.pipe, *self._cmd_get_memory(number)) + if result == "N" or result == "E": + mem = chirp_common.Memory() + mem.number = number + mem.empty = True + self._memcache[mem.number] = mem + return mem + elif " " not in result: + LOG.error("Not sure what to do with this: `%s'" % result) + raise errors.RadioError("Unexpected result returned from radio") + + value = result.split(" ")[1] + spec = value.split(",") + + mem = self._parse_mem_spec(spec) + self._memcache[mem.number] = mem + + result = command(self.pipe, *self._cmd_get_memory_name(number)) + if " " in result: + value = result.split(" ", 1)[1] + if value.count(",") == 2: + _zero, _loc, mem.name = value.split(",") + else: + _loc, mem.name = value.split(",") + + if mem.duplex == "" and self._kenwood_split: + result = command(self.pipe, *self._cmd_get_split(number)) + if " " in result: + value = result.split(" ", 1)[1] + self._parse_split_spec(mem, value.split(",")) + + return mem + + def _make_mem_spec(self, mem): + pass + + def _parse_mem_spec(self, spec): + pass + + def _parse_split_spec(self, mem, spec): + mem.duplex = "split" + mem.offset = int(spec[2]) + + def _make_split_spec(self, mem): + return ("%011i" % mem.offset, "0") + + def set_memory(self, memory): + if memory.number < 0 or memory.number > self._upper: + raise errors.InvalidMemoryLocation( + "Number must be between 0 and %i" % self._upper) + + spec = self._make_mem_spec(memory) + spec = ",".join(spec) + r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec)) + if not iserr(r1): + time.sleep(0.5) + r2 = command(self.pipe, *self._cmd_set_memory_name(memory.number, + memory.name)) + if not iserr(r2): + memory.name = memory.name.rstrip() + self._memcache[memory.number] = memory + else: + raise errors.InvalidDataError("Radio refused name %i: %s" % + (memory.number, + repr(memory.name))) + else: + raise errors.InvalidDataError("Radio refused %i" % memory.number) + + if memory.duplex == "split" and self._kenwood_split: + spec = ",".join(self._make_split_spec(memory)) + result = command(self.pipe, *self._cmd_set_split(memory.number, + spec)) + if iserr(result): + raise errors.InvalidDataError("Radio refused %i" % + memory.number) + + def erase_memory(self, number): + if number not in self._memcache: + return + + resp = command(self.pipe, *self._cmd_set_memory(number, "")) + if iserr(resp): + raise errors.RadioError("Radio refused delete of %i" % number) + del self._memcache[number] + + def _kenwood_get(self, cmd): + resp = command(self.pipe, cmd) + if " " in resp: + return resp.split(" ", 1) + else: + if resp == cmd: + return [resp, ""] + else: + raise errors.RadioError("Radio refused to return %s" % cmd) + + def _kenwood_set(self, cmd, value): + resp = command(self.pipe, cmd, value) + if resp[:len(cmd)] == cmd: + return + raise errors.RadioError("Radio refused to set %s" % cmd) + + def _kenwood_get_bool(self, cmd): + _cmd, result = self._kenwood_get(cmd) + return result == "1" + + def _kenwood_set_bool(self, cmd, value): + return self._kenwood_set(cmd, str(int(value))) + + def _kenwood_get_int(self, cmd): + _cmd, result = self._kenwood_get(cmd) + return int(result) + + def _kenwood_set_int(self, cmd, value, digits=1): + return self._kenwood_set(cmd, ("%%0%ii" % digits) % value) + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + if isinstance(element.value, RadioSettingValueBoolean): + self._kenwood_set_bool(element.get_name(), element.value) + elif isinstance(element.value, RadioSettingValueList): + options = self._get_setting_options(element.get_name()) + if len(options) > 9: + digits = 2 + else: + digits = 1 + self._kenwood_set_int(element.get_name(), + options.index(str(element.value)), + digits) + elif isinstance(element.value, RadioSettingValueInteger): + if element.value.get_max() > 9: + digits = 2 + else: + digits = 1 + self._kenwood_set_int(element.get_name(), + element.value, digits) + elif isinstance(element.value, RadioSettingValueString): + self._kenwood_set(element.get_name(), str(element.value)) + else: + LOG.error("Unknown type %s" % element.value) + + +class KenwoodOldLiveRadio(KenwoodLiveRadio): + _kenwood_valid_tones = list(chirp_common.OLD_TONES) + + def set_memory(self, memory): + supported_tones = list(chirp_common.OLD_TONES) + supported_tones.remove(69.3) + if memory.rtone not in supported_tones: + raise errors.UnsupportedToneError("This radio does not support " + + "tone %.1fHz" % memory.rtone) + if memory.ctone not in supported_tones: + raise errors.UnsupportedToneError("This radio does not support " + + "tone %.1fHz" % memory.ctone) + + return KenwoodLiveRadio.set_memory(self, memory) + + +@directory.register +class THD7Radio(KenwoodOldLiveRadio): + """Kenwood TH-D7""" + MODEL = "TH-D7" + + _kenwood_split = True + + _BEP_OPTIONS = ["Off", "Key", "Key+Data", "All"] + _POSC_OPTIONS = ["Off Duty", "Enroute", "In Service", "Returning", + "Committed", "Special", "Priority", "Emergency"] + + _SETTINGS_OPTIONS = { + "BAL": ["4:0", "3:1", "2:2", "1:3", "0:4"], + "BEP": None, + "BEPT": ["Off", "Mine", "All New"], # D700 has fourth "All" + "DS": ["Data Band", "Both Bands"], + "DTB": ["A", "B"], + "DTBA": ["A", "B", "A:TX/B:RX"], # D700 has fourth A:RX/B:TX + "DTX": ["Manual", "PTT", "Auto"], + "ICO": ["Kenwood", "Runner", "House", "Tent", "Boat", "SSTV", + "Plane", "Speedboat", "Car", "Bicycle"], + "MNF": ["Name", "Frequency"], + "PKSA": ["1200", "9600"], + "POSC": None, + "PT": ["100ms", "200ms", "500ms", "750ms", + "1000ms", "1500ms", "2000ms"], + "SCR": ["Time", "Carrier", "Seek"], + "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s", + "2s", "3s", "4s", "5s"], + "TEMP": ["F", "C"], + "TXI": ["30sec", "1min", "2min", "3min", "4min", "5min", + "10min", "20min", "30min"], + "UNIT": ["English", "Metric"], + "WAY": ["Off", "6 digit NMEA", "7 digit NMEA", "8 digit NMEA", + "9 digit NMEA", "6 digit Magellan", "DGPS"], + } + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_mode = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_modes = list(MODES.values()) + rf.valid_tmodes = ["", "Tone", "TSQL"] + rf.valid_characters = \ + chirp_common.CHARSET_ALPHANUMERIC + "/.-+*)('&%$#! ~}|{" + rf.valid_name_length = 7 + rf.valid_tuning_steps = STEPS + rf.memory_bounds = (1, self._upper) + return rf + + def _make_mem_spec(self, mem): + if mem.duplex in " -+": + duplex = util.get_dict_rev(DUPLEX, mem.duplex) + offset = mem.offset + else: + duplex = 0 + offset = 0 + + spec = ( + "%011i" % mem.freq, + "%X" % STEPS.index(mem.tuning_step), + "%i" % duplex, + "0", + "%i" % (mem.tmode == "Tone"), + "%i" % (mem.tmode == "TSQL"), + "", # DCS Flag + "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1), + "", # DCS Code + "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1), + "%09i" % offset, + "%i" % util.get_dict_rev(MODES, mem.mode), + "%i" % ((mem.skip == "S") and 1 or 0)) + + return spec + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + mem.number = int(spec[2]) + mem.freq = int(spec[3], 10) + mem.tuning_step = STEPS[int(spec[4], 16)] + mem.duplex = DUPLEX[int(spec[5])] + mem.tmode = get_tmode(spec[7], spec[8], spec[9]) + mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1] + mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1] + if spec[11] and spec[11].isdigit(): + mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1] + else: + LOG.warn("Unknown or invalid DCS: %s" % spec[11]) + if spec[13]: + mem.offset = int(spec[13]) + else: + mem.offset = 0 + mem.mode = MODES[int(spec[14])] + mem.skip = int(spec[15]) and "S" or "" + + return mem + + EXTRA_BOOL_SETTINGS = { + 'main': [("LMP", "Lamp")], + 'dtmf': [("TXH", "TX Hold")], + } + EXTRA_LIST_SETTINGS = { + 'main': [("BAL", "Balance"), + ("MNF", "Memory Display Mode")], + 'save': [("SV", "Battery Save")], + } + + def _get_setting_options(self, setting): + opts = self._SETTINGS_OPTIONS[setting] + if opts is None: + opts = getattr(self, '_%s_OPTIONS' % setting) + return opts + + def get_settings(self): + main = RadioSettingGroup("main", "Main") + aux = RadioSettingGroup("aux", "Aux") + tnc = RadioSettingGroup("tnc", "TNC") + save = RadioSettingGroup("save", "Save") + display = RadioSettingGroup("display", "Display") + dtmf = RadioSettingGroup("dtmf", "DTMF") + radio = RadioSettingGroup("radio", "Radio", + aux, tnc, save, display, dtmf) + sky = RadioSettingGroup("sky", "SkyCommand") + aprs = RadioSettingGroup("aprs", "APRS") + + top = RadioSettings(main, radio, aprs, sky) + + bools = [("AMR", aprs, "APRS Message Auto-Reply"), + ("AIP", aux, "Advanced Intercept Point"), + ("ARO", aux, "Automatic Repeater Offset"), + ("BCN", aprs, "Beacon"), + ("CH", radio, "Channel Mode Display"), + # ("DIG", aprs, "APRS Digipeater"), + ("DL", main, "Dual"), + ("LK", main, "Lock"), + ("TSP", dtmf, "DTMF Fast Transmission"), + ] + + for setting, group, name in bools: + value = self._kenwood_get_bool(setting) + rs = RadioSetting(setting, name, + RadioSettingValueBoolean(value)) + group.append(rs) + + lists = [("BEP", aux, "Beep"), + ("BEPT", aprs, "APRS Beep"), + ("DS", tnc, "Data Sense"), + ("DTB", tnc, "Data Band"), + ("DTBA", aprs, "APRS Data Band"), + ("DTX", aprs, "APRS Data TX"), + # ("ICO", aprs, "APRS Icon"), + ("PKSA", aprs, "APRS Packet Speed"), + ("POSC", aprs, "APRS Position Comment"), + ("PT", dtmf, "DTMF Speed"), + ("TEMP", aprs, "APRS Temperature Units"), + ("TXI", aprs, "APRS Transmit Interval"), + # ("UNIT", aprs, "APRS Display Units"), + ("WAY", aprs, "Waypoint Mode"), + ] + + for setting, group, name in lists: + value = self._kenwood_get_int(setting) + options = self._get_setting_options(setting) + rs = RadioSetting(setting, name, + RadioSettingValueList(options, + options[value])) + group.append(rs) + + for group_name, settings in self.EXTRA_BOOL_SETTINGS.items(): + group = locals()[group_name] + for setting, name in settings: + value = self._kenwood_get_bool(setting) + rs = RadioSetting(setting, name, + RadioSettingValueBoolean(value)) + group.append(rs) + + for group_name, settings in self.EXTRA_LIST_SETTINGS.items(): + group = locals()[group_name] + for setting, name in settings: + value = self._kenwood_get_int(setting) + options = self._get_setting_options(setting) + rs = RadioSetting(setting, name, + RadioSettingValueBoolean(value)) + group.append(rs) + + ints = [("CNT", display, "Contrast", 1, 16), + ] + for setting, group, name, minv, maxv in ints: + value = self._kenwood_get_int(setting) + rs = RadioSetting(setting, name, + RadioSettingValueInteger(minv, maxv, value)) + group.append(rs) + + strings = [("MES", display, "Power-on Message", 8), + ("MYC", aprs, "APRS Callsign", 8), + ("PP", aprs, "APRS Path", 32), + ("SCC", sky, "SkyCommand Callsign", 8), + ("SCT", sky, "SkyCommand To Callsign", 8), + # ("STAT", aprs, "APRS Status Text", 32), + ] + for setting, group, name, length in strings: + _cmd, value = self._kenwood_get(setting) + rs = RadioSetting(setting, name, + RadioSettingValueString(0, length, value)) + group.append(rs) + + return top + + +@directory.register +class THD7GRadio(THD7Radio): + """Kenwood TH-D7G""" + MODEL = "TH-D7G" + + def get_features(self): + rf = super(THD7GRadio, self).get_features() + rf.valid_name_length = 8 + return rf + + +@directory.register +class TMD700Radio(THD7Radio): + """Kenwood TH-D700""" + MODEL = "TM-D700" + + _kenwood_split = True + + _BEP_OPTIONS = ["Off", "Key"] + _POSC_OPTIONS = ["Off Duty", "Enroute", "In Service", "Returning", + "Committed", "Special", "Priority", "CUSTOM 0", + "CUSTOM 1", "CUSTOM 2", "CUSTOM 4", "CUSTOM 5", + "CUSTOM 6", "Emergency"] + EXTRA_BOOL_SETTINGS = {} + EXTRA_LIST_SETTINGS = {} + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_dtcs = True + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_mode = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_modes = ["FM", "AM"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC + rf.valid_name_length = 8 + rf.valid_tuning_steps = STEPS + rf.memory_bounds = (1, self._upper) + return rf + + def _make_mem_spec(self, mem): + if mem.duplex in " -+": + duplex = util.get_dict_rev(DUPLEX, mem.duplex) + else: + duplex = 0 + spec = ( + "%011i" % mem.freq, + "%X" % STEPS.index(mem.tuning_step), + "%i" % duplex, + "0", + "%i" % (mem.tmode == "Tone"), + "%i" % (mem.tmode == "TSQL"), + "%i" % (mem.tmode == "DTCS"), + "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1), + "%03i0" % (chirp_common.DTCS_CODES.index(mem.dtcs) + 1), + "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1), + "%09i" % mem.offset, + "%i" % util.get_dict_rev(MODES, mem.mode), + "%i" % ((mem.skip == "S") and 1 or 0)) + + return spec + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + mem.number = int(spec[2]) + mem.freq = int(spec[3]) + mem.tuning_step = STEPS[int(spec[4], 16)] + mem.duplex = DUPLEX[int(spec[5])] + mem.tmode = get_tmode(spec[7], spec[8], spec[9]) + mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1] + mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1] + if spec[11] and spec[11].isdigit(): + mem.dtcs = chirp_common.DTCS_CODES[int(spec[11][:-1]) - 1] + else: + LOG.warn("Unknown or invalid DCS: %s" % spec[11]) + if spec[13]: + mem.offset = int(spec[13]) + else: + mem.offset = 0 + mem.mode = MODES[int(spec[14])] + mem.skip = int(spec[15]) and "S" or "" + + return mem + + +@directory.register +class TMV7Radio(KenwoodOldLiveRadio): + """Kenwood TM-V7""" + MODEL = "TM-V7" + + mem_upper_limit = 200 # Will be updated + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_mode = False + rf.has_tuning_step = False + rf.valid_modes = ["FM"] + rf.valid_tmodes = ["", "Tone", "TSQL"] + rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC + rf.valid_name_length = 7 + rf.valid_tuning_steps = STEPS + rf.has_sub_devices = True + rf.memory_bounds = (1, self._upper) + return rf + + def _make_mem_spec(self, mem): + spec = ( + "%011i" % mem.freq, + "%X" % STEPS.index(mem.tuning_step), + "%i" % util.get_dict_rev(DUPLEX, mem.duplex), + "0", + "%i" % (mem.tmode == "Tone"), + "%i" % (mem.tmode == "TSQL"), + "0", + "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1), + "000", + "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1), + "", + "0") + + return spec + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + mem.number = int(spec[2]) + mem.freq = int(spec[3]) + mem.tuning_step = STEPS[int(spec[4], 16)] + mem.duplex = DUPLEX[int(spec[5])] + if int(spec[7]): + mem.tmode = "Tone" + elif int(spec[8]): + mem.tmode = "TSQL" + mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1] + mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1] + + return mem + + def get_sub_devices(self): + return [TMV7RadioVHF(self.pipe), TMV7RadioUHF(self.pipe)] + + def __test_location(self, loc): + mem = self.get_memory(loc) + if not mem.empty: + # Memory was not empty, must be valid + return True + + # Mem was empty (or invalid), try to set it + if self._vfo == 0: + mem.freq = 144000000 + else: + mem.freq = 440000000 + mem.empty = False + try: + self.set_memory(mem) + except Exception: + # Failed, so we're past the limit + return False + + # Erase what we did + try: + self.erase_memory(loc) + except Exception: + pass # V7A Can't delete just yet + + return True + + def _detect_split(self): + return 50 + + +class TMV7RadioSub(TMV7Radio): + """Base class for the TM-V7 sub devices""" + def __init__(self, pipe): + TMV7Radio.__init__(self, pipe) + self._detect_split() + + +class TMV7RadioVHF(TMV7RadioSub): + """TM-V7 VHF subdevice""" + VARIANT = "VHF" + _vfo = 0 + + +class TMV7RadioUHF(TMV7RadioSub): + """TM-V7 UHF subdevice""" + VARIANT = "UHF" + _vfo = 1 + + +@directory.register +class TMG707Radio(TMV7Radio): + """Kenwood TM-G707""" + MODEL = "TM-G707" + + def get_features(self): + rf = TMV7Radio.get_features(self) + rf.has_sub_devices = False + rf.memory_bounds = (1, 180) + rf.valid_bands = [(118000000, 174000000), + (300000000, 520000000), + (800000000, 999000000)] + return rf + + +THG71_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100] + + +@directory.register +class THG71Radio(TMV7Radio): + """Kenwood TH-G71""" + MODEL = "TH-G71" + + def get_features(self): + rf = TMV7Radio.get_features(self) + rf.has_tuning_step = True + rf.valid_tuning_steps = list(THG71_STEPS) + rf.valid_name_length = 6 + rf.has_sub_devices = False + rf.valid_bands = [(118000000, 174000000), + (320000000, 470000000), + (800000000, 945000000)] + return rf + + def _make_mem_spec(self, mem): + spec = ( + "%011i" % mem.freq, + "%X" % THG71_STEPS.index(mem.tuning_step), + "%i" % util.get_dict_rev(DUPLEX, mem.duplex), + "0", + "%i" % (mem.tmode == "Tone"), + "%i" % (mem.tmode == "TSQL"), + "0", + "%02i" % (self._kenwood_valid_tones.index(mem.rtone) + 1), + "000", + "%02i" % (self._kenwood_valid_tones.index(mem.ctone) + 1), + "%09i" % mem.offset, + "%i" % ((mem.skip == "S") and 1 or 0)) + return spec + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + mem.number = int(spec[2]) + mem.freq = int(spec[3]) + mem.tuning_step = THG71_STEPS[int(spec[4], 16)] + mem.duplex = DUPLEX[int(spec[5])] + if int(spec[7]): + mem.tmode = "Tone" + elif int(spec[8]): + mem.tmode = "TSQL" + mem.rtone = self._kenwood_valid_tones[int(spec[10]) - 1] + mem.ctone = self._kenwood_valid_tones[int(spec[12]) - 1] + if spec[13]: + mem.offset = int(spec[13]) + else: + mem.offset = 0 + return mem + + +THF6A_STEPS = [5.0, 6.25, 8.33, 9.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, + 100.0] + +THF6A_DUPLEX = dict(DUPLEX) +THF6A_DUPLEX[3] = "split" + + +@directory.register +class THF6ARadio(KenwoodLiveRadio): + """Kenwood TH-F6""" + MODEL = "TH-F6" + + _upper = 399 + _kenwood_split = True + _kenwood_valid_tones = list(KENWOOD_TONES) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.can_odd_split = True + rf.valid_modes = list(THF6_MODES) + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_tuning_steps = list(THF6A_STEPS) + rf.valid_bands = [(1000, 1300000000)] + rf.valid_skips = ["", "S"] + rf.valid_duplexes = list(THF6A_DUPLEX.values()) + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 8 + rf.memory_bounds = (0, self._upper) + rf.has_settings = True + return rf + + def _cmd_set_memory(self, number, spec): + if spec: + spec = "," + spec + return "MW", "0,%03i%s" % (number, spec) + + def _cmd_get_memory(self, number): + return "MR", "0,%03i" % number + + def _cmd_get_memory_name(self, number): + return "MNA", "%03i" % number + + def _cmd_set_memory_name(self, number, name): + return "MNA", "%03i,%s" % (number, name) + + def _cmd_get_split(self, number): + return "MR", "1,%03i" % number + + def _cmd_set_split(self, number, spec): + return "MW", "1,%03i,%s" % (number, spec) + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + mem.number = int(spec[1]) + mem.freq = int(spec[2]) + mem.tuning_step = THF6A_STEPS[int(spec[3], 16)] + mem.duplex = THF6A_DUPLEX[int(spec[4])] + mem.tmode = get_tmode(spec[6], spec[7], spec[8]) + mem.rtone = self._kenwood_valid_tones[int(spec[9])] + mem.ctone = self._kenwood_valid_tones[int(spec[10])] + if spec[11] and spec[11].isdigit(): + mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])] + else: + LOG.warn("Unknown or invalid DCS: %s" % spec[11]) + if spec[12]: + mem.offset = int(spec[12]) + else: + mem.offset = 0 + mem.mode = THF6_MODES[int(spec[13])] + if spec[14] == "1": + mem.skip = "S" + + return mem + + def _make_mem_spec(self, mem): + if mem.duplex in " +-": + duplex = util.get_dict_rev(THF6A_DUPLEX, mem.duplex) + offset = mem.offset + elif mem.duplex == "split": + duplex = 0 + offset = 0 + else: + LOG.warn("Bug: unsupported duplex `%s'" % mem.duplex) + spec = ( + "%011i" % mem.freq, + "%X" % THF6A_STEPS.index(mem.tuning_step), + "%i" % duplex, + "0", + "%i" % (mem.tmode == "Tone"), + "%i" % (mem.tmode == "TSQL"), + "%i" % (mem.tmode == "DTCS"), + "%02i" % (self._kenwood_valid_tones.index(mem.rtone)), + "%02i" % (self._kenwood_valid_tones.index(mem.ctone)), + "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)), + "%09i" % offset, + "%i" % (THF6_MODES.index(mem.mode)), + "%i" % (mem.skip == "S")) + + return spec + + _SETTINGS_OPTIONS = { + "APO": ["Off", "30min", "60min"], + "BAL": ["100%:0%", "75%:25%", "50%:50%", "25%:75%", "%0:100%"], + "BAT": ["Lithium", "Alkaline"], + "CKEY": ["Call", "1750Hz"], + "DATP": ["1200bps", "9600bps"], + "LAN": ["English", "Japanese"], + "MNF": ["Name", "Frequency"], + "MRM": ["All Band", "Current Band"], + "PT": ["100ms", "250ms", "500ms", "750ms", + "1000ms", "1500ms", "2000ms"], + "SCR": ["Time", "Carrier", "Seek"], + "SV": ["Off", "0.2s", "0.4s", "0.6s", "0.8s", "1.0s", + "2s", "3s", "4s", "5s"], + "VXD": ["250ms", "500ms", "750ms", "1s", "1.5s", "2s", "3s"], + } + + def get_settings(self): + main = RadioSettingGroup("main", "Main") + aux = RadioSettingGroup("aux", "Aux") + save = RadioSettingGroup("save", "Save") + display = RadioSettingGroup("display", "Display") + dtmf = RadioSettingGroup("dtmf", "DTMF") + top = RadioSettings(main, aux, save, display, dtmf) + + lists = [("APO", save, "Automatic Power Off"), + ("BAL", main, "Balance"), + ("BAT", save, "Battery Type"), + ("CKEY", aux, "CALL Key Set Up"), + ("DATP", aux, "Data Packet Speed"), + ("LAN", display, "Language"), + ("MNF", main, "Memory Display Mode"), + ("MRM", main, "Memory Recall Method"), + ("PT", dtmf, "DTMF Speed"), + ("SCR", main, "Scan Resume"), + ("SV", save, "Battery Save"), + ("VXD", aux, "VOX Drop Delay"), + ] + + bools = [("ANT", aux, "Bar Antenna"), + ("ATT", main, "Attenuator Enabled"), + ("ARO", main, "Automatic Repeater Offset"), + ("BEP", aux, "Beep for keypad"), + ("DL", main, "Dual"), + ("DLK", dtmf, "DTMF Lockout On Transmit"), + ("ELK", aux, "Enable Locked Tuning"), + ("LK", main, "Lock"), + ("LMP", display, "Lamp"), + ("NSFT", aux, "Noise Shift"), + ("TH", aux, "Tx Hold for 1750"), + ("TSP", dtmf, "DTMF Fast Transmission"), + ("TXH", dtmf, "TX Hold DTMF"), + ("TXS", main, "Transmit Inhibit"), + ("VOX", aux, "VOX Enable"), + ("VXB", aux, "VOX On Busy"), + ] + + ints = [("CNT", display, "Contrast", 1, 16), + ("VXG", aux, "VOX Gain", 0, 9), + ] + + strings = [("MES", display, "Power-on Message", 8), + ] + + for setting, group, name in bools: + value = self._kenwood_get_bool(setting) + rs = RadioSetting(setting, name, + RadioSettingValueBoolean(value)) + group.append(rs) + + for setting, group, name in lists: + value = self._kenwood_get_int(setting) + options = self._SETTINGS_OPTIONS[setting] + rs = RadioSetting(setting, name, + RadioSettingValueList(options, + options[value])) + group.append(rs) + + for setting, group, name, minv, maxv in ints: + value = self._kenwood_get_int(setting) + rs = RadioSetting(setting, name, + RadioSettingValueInteger(minv, maxv, value)) + group.append(rs) + + for setting, group, name, length in strings: + _cmd, value = self._kenwood_get(setting) + rs = RadioSetting(setting, name, + RadioSettingValueString(0, length, value)) + group.append(rs) + + return top + + +@directory.register +class THF7ERadio(THF6ARadio): + """Kenwood TH-F7""" + MODEL = "TH-F7" + + +D710_DUPLEX = ["", "+", "-", "split"] +D710_MODES = ["FM", "NFM", "AM"] +D710_SKIP = ["", "S"] +D710_STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + + +@directory.register +class TMD710Radio(KenwoodLiveRadio): + """Kenwood TM-D710""" + MODEL = "TM-D710" + + _upper = 999 + _kenwood_valid_tones = list(KENWOOD_TONES) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.can_odd_split = True + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = D710_MODES + rf.valid_duplexes = D710_DUPLEX + rf.valid_tuning_steps = D710_STEPS + rf.valid_characters = chirp_common.CHARSET_ASCII.replace(',', '') + rf.valid_name_length = 8 + rf.valid_skips = D710_SKIP + rf.memory_bounds = (0, 999) + return rf + + def _cmd_get_memory(self, number): + return "ME", "%03i" % number + + def _cmd_get_memory_name(self, number): + return "MN", "%03i" % number + + def _cmd_set_memory(self, number, spec): + return "ME", "%03i,%s" % (number, spec) + + def _cmd_set_memory_name(self, number, name): + return "MN", "%03i,%s" % (number, name) + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + mem.number = int(spec[0]) + mem.freq = int(spec[1]) + mem.tuning_step = D710_STEPS[int(spec[2], 16)] + mem.duplex = D710_DUPLEX[int(spec[3])] + # Reverse + if int(spec[5]): + mem.tmode = "Tone" + elif int(spec[6]): + mem.tmode = "TSQL" + elif int(spec[7]): + mem.tmode = "DTCS" + mem.rtone = self._kenwood_valid_tones[int(spec[8])] + mem.ctone = self._kenwood_valid_tones[int(spec[9])] + mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])] + mem.offset = int(spec[11]) + mem.mode = D710_MODES[int(spec[12])] + # TX Frequency + if int(spec[13]): + mem.duplex = "split" + mem.offset = int(spec[13]) + # Unknown + mem.skip = D710_SKIP[int(spec[15])] # Memory Lockout + + return mem + + def _make_mem_spec(self, mem): + spec = ( + "%010i" % mem.freq, + "%X" % D710_STEPS.index(mem.tuning_step), + "%i" % (0 if mem.duplex == "split" + else D710_DUPLEX.index(mem.duplex)), + "0", # Reverse + "%i" % (mem.tmode == "Tone" and 1 or 0), + "%i" % (mem.tmode == "TSQL" and 1 or 0), + "%i" % (mem.tmode == "DTCS" and 1 or 0), + "%02i" % (self._kenwood_valid_tones.index(mem.rtone)), + "%02i" % (self._kenwood_valid_tones.index(mem.ctone)), + "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)), + "%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset + "%i" % D710_MODES.index(mem.mode), + "%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq + "0", # Unknown + "%i" % D710_SKIP.index(mem.skip), # Memory Lockout + ) + + return spec + + +@directory.register +class THD72Radio(TMD710Radio): + """Kenwood TH-D72""" + MODEL = "TH-D72 (live mode)" + HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + mem.number = int(spec[0]) + mem.freq = int(spec[1]) + mem.tuning_step = D710_STEPS[int(spec[2], 16)] + mem.duplex = D710_DUPLEX[int(spec[3])] + # Reverse + if int(spec[5]): + mem.tmode = "Tone" + elif int(spec[6]): + mem.tmode = "TSQL" + elif int(spec[7]): + mem.tmode = "DTCS" + mem.rtone = self._kenwood_valid_tones[int(spec[9])] + mem.ctone = self._kenwood_valid_tones[int(spec[10])] + mem.dtcs = chirp_common.DTCS_CODES[int(spec[11])] + mem.offset = int(spec[13]) + mem.mode = D710_MODES[int(spec[14])] + # TX Frequency + if int(spec[15]): + mem.duplex = "split" + mem.offset = int(spec[15]) + # Lockout + mem.skip = D710_SKIP[int(spec[17])] # Memory Lockout + + return mem + + def _make_mem_spec(self, mem): + spec = ( + "%010i" % mem.freq, + "%X" % D710_STEPS.index(mem.tuning_step), + "%i" % (0 if mem.duplex == "split" + else D710_DUPLEX.index(mem.duplex)), + "0", # Reverse + "%i" % (mem.tmode == "Tone" and 1 or 0), + "%i" % (mem.tmode == "TSQL" and 1 or 0), + "%i" % (mem.tmode == "DTCS" and 1 or 0), + "0", + "%02i" % (self._kenwood_valid_tones.index(mem.rtone)), + "%02i" % (self._kenwood_valid_tones.index(mem.ctone)), + "%03i" % (chirp_common.DTCS_CODES.index(mem.dtcs)), + "0", + "%08i" % (0 if mem.duplex == "split" else mem.offset), # Offset + "%i" % D710_MODES.index(mem.mode), + "%010i" % (mem.offset if mem.duplex == "split" else 0), # TX Freq + "0", # Unknown + "%i" % D710_SKIP.index(mem.skip), # Memory Lockout + ) + + return spec + + +@directory.register +class TMV71Radio(TMD710Radio): + """Kenwood TM-V71""" + MODEL = "TM-V71" + + +@directory.register +class TMD710GRadio(TMD710Radio): + """Kenwood TM-D710G""" + MODEL = "TM-D710G" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("This radio driver is currently under development, " + "and supports the same features as the TM-D710A/E. " + "There are no known issues with it, but you should " + "proceed with caution.") + return rp + + +THK2_DUPLEX = ["", "+", "-"] +THK2_MODES = ["FM", "NFM"] + +THK2_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "-/" + + +@directory.register +class THK2Radio(KenwoodLiveRadio): + """Kenwood TH-K2""" + MODEL = "TH-K2" + + _kenwood_valid_tones = list(KENWOOD_TONES) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.can_odd_split = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_tuning_step = False + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = THK2_MODES + rf.valid_duplexes = THK2_DUPLEX + rf.valid_characters = THK2_CHARS + rf.valid_name_length = 6 + rf.valid_bands = [(136000000, 173990000)] + rf.valid_skips = ["", "S"] + rf.valid_tuning_steps = [5.0] + rf.memory_bounds = (0, 49) + return rf + + def _cmd_get_memory(self, number): + return "ME", "%02i" % number + + def _cmd_get_memory_name(self, number): + return "MN", "%02i" % number + + def _cmd_set_memory(self, number, spec): + return "ME", "%02i,%s" % (number, spec) + + def _cmd_set_memory_name(self, number, name): + return "MN", "%02i,%s" % (number, name) + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + mem.number = int(spec[0]) + mem.freq = int(spec[1]) + # mem.tuning_step = + mem.duplex = THK2_DUPLEX[int(spec[3])] + if int(spec[5]): + mem.tmode = "Tone" + elif int(spec[6]): + mem.tmode = "TSQL" + elif int(spec[7]): + mem.tmode = "DTCS" + mem.rtone = self._kenwood_valid_tones[int(spec[8])] + mem.ctone = self._kenwood_valid_tones[int(spec[9])] + mem.dtcs = chirp_common.DTCS_CODES[int(spec[10])] + mem.offset = int(spec[11]) + mem.mode = THK2_MODES[int(spec[12])] + mem.skip = int(spec[16]) and "S" or "" + return mem + + def _make_mem_spec(self, mem): + try: + rti = self._kenwood_valid_tones.index(mem.rtone) + cti = self._kenwood_valid_tones.index(mem.ctone) + except ValueError: + raise errors.UnsupportedToneError() + + spec = ( + "%010i" % mem.freq, + "0", + "%i" % THK2_DUPLEX.index(mem.duplex), + "0", + "%i" % int(mem.tmode == "Tone"), + "%i" % int(mem.tmode == "TSQL"), + "%i" % int(mem.tmode == "DTCS"), + "%02i" % rti, + "%02i" % cti, + "%03i" % chirp_common.DTCS_CODES.index(mem.dtcs), + "%08i" % mem.offset, + "%i" % THK2_MODES.index(mem.mode), + "0", + "%010i" % 0, + "0", + "%i" % int(mem.skip == "S") + ) + return spec + + +TM271_STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + + +@directory.register +class TM271Radio(THK2Radio): + """Kenwood TM-271""" + MODEL = "TM-271" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.can_odd_split = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_tuning_step = False + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = THK2_MODES + rf.valid_duplexes = THK2_DUPLEX + rf.valid_characters = THK2_CHARS + rf.valid_name_length = 6 + rf.valid_bands = [(137000000, 173990000)] + rf.valid_skips = ["", "S"] + rf.valid_tuning_steps = list(TM271_STEPS) + rf.memory_bounds = (0, 99) + return rf + + def _cmd_get_memory(self, number): + return "ME", "%03i" % number + + def _cmd_get_memory_name(self, number): + return "MN", "%03i" % number + + def _cmd_set_memory(self, number, spec): + return "ME", "%03i,%s" % (number, spec) + + def _cmd_set_memory_name(self, number, name): + return "MN", "%03i,%s" % (number, name) + + +@directory.register +class TM281Radio(TM271Radio): + """Kenwood TM-281""" + MODEL = "TM-281" + # seems that this is a perfect clone of TM271 with just a different model + + +@directory.register +class TM471Radio(THK2Radio): + """Kenwood TM-471""" + MODEL = "TM-471" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.can_odd_split = False + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.has_tuning_step = False + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = THK2_MODES + rf.valid_duplexes = THK2_DUPLEX + rf.valid_characters = THK2_CHARS + rf.valid_name_length = 6 + rf.valid_bands = [(444000000, 479990000)] + rf.valid_skips = ["", "S"] + rf.valid_tuning_steps = [5.0] + rf.memory_bounds = (0, 99) + return rf + + def _cmd_get_memory(self, number): + return "ME", "%03i" % number + + def _cmd_get_memory_name(self, number): + return "MN", "%03i" % number + + def _cmd_set_memory(self, number, spec): + return "ME", "%03i,%s" % (number, spec) + + def _cmd_set_memory_name(self, number, name): + return "MN", "%03i,%s" % (number, name) + + +@directory.register +class TS590Radio(KenwoodLiveRadio): + """Kenwood TS-590S/SG""" + MODEL = "TS-590S/SG_LiveMode" + + _kenwood_valid_tones = list(KENWOOD_TONES) + _kenwood_valid_tones.append(1750) + + _upper = 99 + _duplex = ["", "-", "+"] + _skip = ["", "S"] + _modes = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", + "FSK-R", "Data+LSB", "Data+USB", "Data+FM"] + _bands = [(1800000, 2000000), # 160M Band + (3500000, 4000000), # 80M Band + (5167500, 5450000), # 60M Band + (7000000, 7300000), # 40M Band + (10100000, 10150000), # 30M Band + (14000000, 14350000), # 20M Band + (18068000, 18168000), # 17M Band + (21000000, 21450000), # 15M Band + (24890000, 24990000), # 12M Band + (28000000, 29700000), # 10M Band + (50000000, 54000000)] # 6M Band + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.can_odd_split = False + rf.has_bank = False + rf.has_ctone = True + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_name = True + rf.has_settings = False + rf.has_offset = True + rf.has_mode = True + rf.has_tuning_step = False + rf.has_nostep_tuning = True + rf.has_cross = True + rf.has_comment = False + + rf.memory_bounds = (0, self._upper) + + rf.valid_bands = self._bands + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/" + rf.valid_duplexes = ["", "-", "+"] + rf.valid_modes = self._modes + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "->Tone"] + rf.valid_name_length = 8 # 8 character channel names + + return rf + + def _my_val_list(setting, opts, obj, atrb): + """Callback:from ValueList. Set the integer index.""" + value = opts.index(str(setting.value)) + setattr(obj, atrb, value) + return + + def get_memory(self, number): + """Convert ascii channel data spec into UI columns (mem)""" + mem = chirp_common.Memory() + mem.extra = RadioSettingGroup("extra", "Extra") + # Read the base and split MR strings + mem.number = number + spec0 = command(self.pipe, "MR0 %02i" % mem.number) + spec1 = command(self.pipe, "MR1 %02i" % mem.number) + mem.name = spec0[41:49] # Max 8-Char Name if assigned + mem.name = mem.name.strip() + mem.name = mem.name.upper() + _p4 = int(spec0[6:17]) # Rx Frequency + _p4s = int(spec1[6:17]) # Offset freq (Tx) + _p5 = int(spec0[17]) # Mode + _p6 = int(spec0[18]) # Data Mode + _p7 = int(spec0[19]) # Tone Mode + _p8 = int(spec0[20:22]) # Tone Frequency Index + _p9 = int(spec0[22:24]) # CTCSS Frequency Index + _p11 = int(spec0[27]) # Filter A/B + _p14 = int(spec0[38:40]) # FM Mode + _p15 = int(spec0[40]) # Chan Lockout (Skip) + if _p4 == 0: + mem.empty = True + return mem + mem.empty = False + mem.freq = _p4 + mem.duplex = self._duplex[0] # None by default + mem.offset = 0 + if _p4 < _p4s: # + shift + mem.duplex = self._duplex[2] + mem.offset = _p4s - _p4 + if _p4 > _p4s: # - shift + mem.duplex = self._duplex[1] + mem.offset = _p4 - _p4s + mx = _p5 - 1 # CAT modes start at 1 + if _p5 == 9: # except CAT FSK-R is 9, there is no 8 + mx = 7 + if _p6: # LSB+Data= 8, USB+Data= 9, FM+Data= 10 + if _p5 == 1: # CAT LSB + mx = 8 + elif _p5 == 2: # CAT USB + mx = 9 + elif _p5 == 4: # CAT FM + mx = 10 + mem.mode = self._modes[mx] + mem.tmode = "" + mem.cross_mode = "Tone->Tone" + mem.ctone = self._kenwood_valid_tones[_p9] + mem.rtone = self._kenwood_valid_tones[_p8] + if _p7 == 1: + mem.tmode = "Tone" + elif _p7 == 2: + mem.tmode = "TSQL" + elif _p7 == 3: + mem.tmode = "Cross" + mem.skip = self._skip[_p15] + + rx = RadioSettingValueBoolean(bool(_p14)) + rset = RadioSetting("fmnrw", "FM Narrow mode (off = Wide)", rx) + mem.extra.append(rset) + return mem + + def erase_memory(self, number): + """ Send the blank string to MW0 """ + mem = chirp_common.Memory() + mem.empty = True + mem.freq = 0 + mem.offset = 0 + spx = "MW0%03i00000000000000000000000000000000000" % number + rx = command(self.pipe, spx) # Send MW0 + return mem + + def set_memory(self, mem): + """Send UI column data (mem) to radio""" + pfx = "MW0%03i" % mem.number + xmode = 0 + xtmode = 0 + xrtone = 8 + xctone = 8 + xdata = 0 + xfltr = 0 + xfm = 0 + xskip = 0 + xfreq = mem.freq + if xfreq > 0: # if empty; use those defaults + ix = self._modes.index(mem.mode) + xmode = ix + 1 # stored as CAT values, LSB= 1 + if ix == 7: # FSK-R + xmode = 9 # There is no CAT 8 + if ix > 7: # a Data mode + xdata = 1 + if ix == 8: + xmode = 1 # LSB + elif ix == 9: + xmode = 2 # USB + elif ix == 10: + xmode = 4 # FM + if mem.tmode == "Tone": + xtmode = 1 + xrtone = self._kenwood_valid_tones.index(mem.rtone) + if mem.tmode == "TSQL" or mem.tmode == "Cross": + xtmode = 2 + if mem.tmode == "Cross": + xtmode = 3 + xctone = self._kenwood_valid_tones.index(mem.ctone) + for setting in mem.extra: + if setting.get_name() == "fmnrw": + xfm = setting.value + if mem.skip == "S": + xskip = 1 + spx = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \ + % (xfreq, xmode, xdata, xtmode, xrtone, + xctone, xfltr, xfm, xskip, mem.name) + rx = command(self.pipe, pfx, spx) # Send MW0 + if mem.offset != 0: + pfx = "MW1%03i" % mem.number + xfreq = mem.freq - mem.offset + if mem.duplex == "+": + xfreq = mem.freq + mem.offset + spx = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \ + % (xfreq, xmode, xdata, xtmode, xrtone, + xctone, xfltr, xfm, xskip, mem.name) + rx = command(self.pipe, pfx, spx) # Send MW1 + + +@directory.register +class TS480Radio(KenwoodLiveRadio): + """Kenwood TS-480""" + MODEL = "TS-480_LiveMode" + + _kenwood_valid_tones = list(KENWOOD_TONES) + _kenwood_valid_tones.append(1750) + + _upper = 99 + _duplex = ["", "-", "+"] + _skip = ["", "S"] + _modes = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", "N/A", + "FSK-R"] + _bands = [(1800000, 2000000), # 160M Band + (3500000, 4000000), # 80M Band + (5167500, 5450000), # 60M Band + (7000000, 7300000), # 40M Band + (10100000, 10150000), # 30M Band + (14000000, 14350000), # 20M Band + (18068000, 18168000), # 17M Band + (21000000, 21450000), # 15M Band + (24890000, 24990000), # 12M Band + (28000000, 29700000), # 10M Band + (50000000, 54000000)] # 6M Band + + _tsteps = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5, + 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.can_odd_split = False + rf.has_bank = False + rf.has_ctone = True + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_name = True + rf.has_settings = False + rf.has_offset = True + rf.has_mode = True + rf.has_tuning_step = True + rf.has_nostep_tuning = True + rf.has_cross = True + rf.has_comment = False + + rf.memory_bounds = (0, self._upper) + + rf.valid_bands = self._bands + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/" + rf.valid_duplexes = ["", "-", "+"] + rf.valid_modes = self._modes + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "->Tone"] + rf.valid_name_length = 8 # 8 character channel names + rf.valid_tuning_steps = self._tsteps + + return rf + + def _my_val_list(setting, opts, obj, atrb): + """Callback:from ValueList. Set the integer index.""" + value = opts.index(str(setting.value)) + setattr(obj, atrb, value) + return + + def get_memory(self, number): + """Convert ascii channel data spec into UI columns (mem)""" + mem = chirp_common.Memory() + # Read the base and split MR strings + mem.number = number + spec0 = command(self.pipe, "MR0%03i" % mem.number) + spec1 = command(self.pipe, "MR1%03i" % mem.number) + # Add 1 to string idecis if refering to CAT manual + mem.name = spec0[41:49] # Max 8-Char Name if assigned + mem.name = mem.name.strip() + mem.name = mem.name.upper() + _p4 = int(spec0[6:17]) # Rx Frequency + _p4s = int(spec1[6:17]) # Offset freq (Tx) + _p5 = int(spec0[17]) # Mode + _p6 = int(spec0[18]) # Chan Lockout (Skip) + _p7 = int(spec0[19]) # Tone Mode + _p8 = int(spec0[20:22]) # Tone Frequency Index + _p9 = int(spec0[22:24]) # CTCSS Frequency Index + _p14 = int(spec0[38:40]) # Tune Step + if _p4 == 0: + mem.empty = True + return mem + mem.empty = False + mem.freq = _p4 + mem.duplex = self._duplex[0] # None by default + mem.offset = 0 + if _p4 < _p4s: # + shift + mem.duplex = self._duplex[2] + mem.offset = _p4s - _p4 + if _p4 > _p4s: # - shift + mem.duplex = self._duplex[1] + mem.offset = _p4 - _p4s + mx = _p5 - 1 # CAT modes start at 1 + mem.mode = self._modes[mx] + mem.tmode = "" + mem.cross_mode = "Tone->Tone" + mem.ctone = self._kenwood_valid_tones[_p9] + mem.rtone = self._kenwood_valid_tones[_p8] + if _p7 == 1: + mem.tmode = "Tone" + elif _p7 == 2: + mem.tmode = "TSQL" + elif _p7 == 3: + mem.tmode = "Cross" + mem.skip = self._skip[_p6] + # Tuning step depends on mode + options = [0.5, 1.0, 2.5, 5.0, 10.0] # SSB/CS/FSK + if _p14 == 4 or _p14 == 5: # AM/FM + options = self._tsteps[3:] + mem.tuning_step = options[_p14] + + return mem + + def erase_memory(self, number): + mem = chirp_common.Memory() + mem.empty = True + mem.freq = 0 + mem.offset = 0 + spx = "MW0%03i00000000000000000000000000000000000" % number + rx = command(self.pipe, spx) # Send MW0 + return mem + + def set_memory(self, mem): + """Send UI column data (mem) to radio""" + pfx = "MW0%03i" % mem.number + xtmode = 0 + xdata = 0 + xrtone = 8 + xctone = 8 + xskip = 0 + xstep = 0 + xfreq = mem.freq + if xfreq > 0: # if empty, use those defaults + ix = self._modes.index(mem.mode) + xmode = ix + 1 # stored as CAT values, LSB= 1 + if ix == 7: # FSK-R + xmode = 9 # There is no CAT 8 + if mem.tmode == "Tone": + xtmode = 1 + xrtone = self._kenwood_valid_tones.index(mem.rtone) + if mem.tmode == "TSQL" or mem.tmode == "Cross": + xtmode = 2 + if mem.tmode == "Cross": + xtmode = 3 + xctone = self._kenwood_valid_tones.index(mem.ctone) + if mem.skip == "S": + xskip = 1 + options = [0.5, 1.0, 2.5, 5.0, 10.0] # SSB/CS/FSK + if xmode == 4 or xmode == 5: + options = self._tsteps[3:] + xstep = options.index(mem.tuning_step) + spx = "%011i%1i%1i%1i%02i%02i00000000000000%02i%s" \ + % (xfreq, xmode, xskip, xtmode, xrtone, + xctone, xstep, mem.name) + rx = command(self.pipe, pfx, spx) # Send MW0 + if mem.offset != 0: # Don't send MW1 if empty + pfx = "MW1%03i" % mem.number + xfreq = mem.freq - mem.offset + if mem.duplex == "+": + xfreq = mem.freq + mem.offset + spx = "%011i%1i%1i%1i%02i%02i00000000000000%02i%s" \ + % (xfreq, xmode, xskip, xtmode, xrtone, + xctone, xstep, mem.name) + rx = command(self.pipe, pfx, spx) # Send MW1 diff --git a/chirp/drivers/kguv8d.py b/chirp/drivers/kguv8d.py new file mode 100644 index 0000000..5fe16b4 --- /dev/null +++ b/chirp/drivers/kguv8d.py @@ -0,0 +1,1044 @@ +# Copyright 2014 Ron Wellsted M0RNW +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Wouxun KG-UV8D radio management module""" + +import time +import os +import logging +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +CMD_ID = 128 +CMD_END = 129 +CMD_RD = 130 +CMD_WR = 131 + +MEM_VALID = 158 + +AB_LIST = ["A", "B"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +STEP_LIST = [str(x) for x in STEPS] +ROGER_LIST = ["Off", "BOT", "EOT", "Both"] +TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)] +VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)] +BANDWIDTH_LIST = ["Narrow", "Wide"] +VOICE_LIST = ["Off", "On"] +LANGUAGE_LIST = ["Chinese", "English"] +SCANMODE_LIST = ["TO", "CO", "SE"] +PF1KEY_LIST = ["Call", "VFTX"] +PF3KEY_LIST = ["Scan", "Lamp", "Tele Alarm", "SOS-CH", "Radio", "Disable"] +WORKMODE_LIST = ["VFO", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"] +BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \ + ["Always Off"] +OFFSET_LIST = ["+", "-"] +PONMSG_LIST = ["Bitmap", "Battery Volts"] +SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"] +DTMFST_LIST = ["DT-ST", "ANI-ST", "DT-ANI", "Off"] +DTMF_TIMES = ["%s" % x for x in range(50, 501, 10)] +RPTSET_LIST = ["X-TWRPT", "X-DIRRPT"] +ALERTS = [1750, 2100, 1000, 1450] +ALERTS_LIST = [str(x) for x in ALERTS] +PTTID_LIST = ["BOT", "EOT", "Both"] +LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)] +SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)] +SCQT_LIST = ["All", "Decoder", "Encoder"] +SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx/Rx"] +POWER_LIST = ["Lo", "Hi"] +HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)] +RPTMODE_LIST = ["Radio", "Repeater"] + +# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999) +# structure elements whose name starts with x are currently unidentified +_MEM_FORMAT = """ + #seekto 0x0044; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } uhf_limits; + + #seekto 0x0054; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } vhf_limits; + + #seekto 0x0400; + struct { + u8 model[8]; + u8 unknown[2]; + u8 oem1[10]; + u8 oem2[10]; + u8 unknown2[8]; + u8 version[10]; + u8 unknown3[6]; + u8 date[8]; + } oem_info; + + #seekto 0x0480; + struct { + u16 lower; + u16 upper; + } scan_groups[10]; + + #seekto 0x0500; + struct { + u8 call_code[6]; + } call_groups[20]; + + #seekto 0x0580; + struct { + char call_name[6]; + } call_group_name[20]; + + #seekto 0x0800; + struct { + u8 ponmsg; + char dispstr[15]; + u8 x0810; + u8 x0811; + u8 x0812; + u8 x0813; + u8 x0814; + u8 voice; + u8 timeout; + u8 toalarm; + u8 channel_menu; + u8 power_save; + u8 autolock; + u8 keylock; + u8 beep; + u8 stopwatch; + u8 vox; + u8 scan_rev; + u8 backlight; + u8 roger_beep; + u8 mode_sw_pwd[6]; + u8 reset_pwd[6]; + u16 pri_ch; + u8 ani_sw; + u8 ptt_delay; + u8 ani[6]; + u8 dtmf_st; + u8 bcl_a; + u8 bcl_b; + u8 ptt_id; + u8 prich_sw; + u8 rpt_set; + u8 rpt_spk; + u8 rpt_ptt; + u8 alert; + u8 pf1_func; + u8 pf3_func; + u8 workmode_b; + u8 workmode_a; + u8 x0845; + u8 dtmf_tx_time; + u8 dtmf_interval; + u8 main_ab; + u16 work_cha; + u16 work_chb; + u8 x084d; + u8 x084e; + u8 x084f; + u8 x0850; + u8 x0851; + u8 x0852; + u8 x0853; + u8 x0854; + u8 rpt_mode; + u8 language; + u8 x0857; + u8 x0858; + u8 x0859; + u8 x085a; + u8 x085b; + u8 x085c; + u8 x085d; + u8 x085e; + u8 single_display; + u8 ring_time; + u8 scg_a; + u8 scg_b; + u8 x0863; + u8 rpt_tone; + u8 rpt_hold; + u8 scan_det; + u8 sc_qt; + u8 x0868; + u8 smuteset; + u8 callcode; + } settings; + + #seekto 0x0880; + struct { + u32 rxfreq; + u32 txoffset; + u16 rxtone; + u16 txtone; + u8 unknown1:6, + power:1, + unknown2:1; + u8 unknown3:1, + shift_dir:2 + unknown4:2, + mute_mode:2, + iswide:1; + u8 step; + u8 squelch; + } vfoa; + + #seekto 0x08c0; + struct { + u32 rxfreq; + u32 txoffset; + u16 rxtone; + u16 txtone; + u8 unknown1:6, + power:1, + unknown2:1; + u8 unknown3:1, + shift_dir:2 + unknown4:2, + mute_mode:2, + iswide:1; + u8 step; + u8 squelch; + } vfob; + + #seekto 0x0900; + struct { + u32 rxfreq; + u32 txfreq; + u16 rxtone; + u16 txtone; + u8 unknown1:6, + power:1, + unknown2:1; + u8 unknown3:2, + scan_add:1, + unknown4:2, + mute_mode:2, + iswide:1; + u16 padding; + } memory[1000]; + + #seekto 0x4780; + struct { + u8 name[8]; + } names[1000]; + + #seekto 0x6700; + u8 valid[1000]; + """ + +# Support for the Wouxun KG-UV8D radio +# Serial coms are at 19200 baud +# The data is passed in variable length records +# Record structure: +# Offset Usage +# 0 start of record (\x7d) +# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write) +# 2 direction (\xff PC-> Radio, \x00 Radio -> PC) +# 3 length of payload (excluding header/checksum) (n) +# 4 payload (n bytes) +# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n +# +# Memory Read Records: +# the payload is 3 bytes, first 2 are offset (big endian), +# 3rd is number of bytes to read +# Memory Write Records: +# the maximum payload size (from the Wouxun software) seems to be 66 bytes +# (2 bytes location + 64 bytes data). + + +@directory.register +class KGUV8DRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + + """Wouxun KG-UV8D""" + VENDOR = "Wouxun" + MODEL = "KG-UV8D" + _model = "KG-UV8D" + _file_ident = "KGUV8D" + BAUD_RATE = 19200 + POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1), + chirp_common.PowerLevel("H", watts=5)] + _mmap = "" + + def _checksum(self, data): + cs = 0 + for byte in data: + cs += ord(byte) + return cs % 256 + + def _write_record(self, cmd, payload=None): + # build the packet + _packet = '\x7d' + chr(cmd) + '\xff' + _length = 0 + if payload: + _length = len(payload) + # update the length field + _packet += chr(_length) + if payload: + # add the chars to the packet + _packet += payload + # calculate and add the checksum to the packet + _packet += chr(self._checksum(_packet[1:])) + LOG.debug("Sent:\n%s" % util.hexprint(_packet)) + self.pipe.write(_packet) + + def _read_record(self): + # read 4 chars for the header + _header = self.pipe.read(4) + if len(_header) != 4: + raise errors.RadioError('Radio did not respond') + _length = ord(_header[3]) + _packet = self.pipe.read(_length) + _cs = self._checksum(_header[1:]) + _cs += self._checksum(_packet) + _cs %= 256 + _rcs = ord(self.pipe.read(1)) + LOG.debug("_cs =%x", _cs) + LOG.debug("_rcs=%x", _rcs) + return (_rcs != _cs, _packet) + +# Identify the radio +# +# A Gotcha: the first identify packet returns a bad checksum, subsequent +# attempts return the correct checksum... (well it does on my radio!) +# +# The ID record returned by the radio also includes the current frequency range +# as 4 bytes big-endian in 10Hz increments +# +# Offset +# 0:10 Model, zero padded (Use first 7 chars for 'KG-UV8D') +# 11:14 UHF rx lower limit (in units of 10Hz) +# 15:18 UHF rx upper limit +# 19:22 UHF tx lower limit +# 23:26 UHF tx upper limit +# 27:30 VHF rx lower limit +# 31:34 VHF rx upper limit +# 35:38 VHF tx lower limit +# 39:42 VHF tx upper limit +# + @classmethod + def match_model(cls, filedata, filename): + return cls._file_ident in filedata[0x400:0x408] + + def _identify(self): + """Do the identification dance""" + for _i in range(0, 10): + self._write_record(CMD_ID) + _chksum_err, _resp = self._read_record() + LOG.debug("Got:\n%s" % util.hexprint(_resp)) + if _chksum_err: + LOG.error("Checksum error: retrying ident...") + time.sleep(0.100) + continue + LOG.debug("Model %s" % util.hexprint(_resp[0:7])) + if _resp[0:7] == self._model: + return + if len(_resp) == 0: + raise Exception("Radio not responding") + else: + raise Exception("Unable to identify radio") + + def _finish(self): + self._write_record(CMD_END) + + def process_mmap(self): + self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + self._mmap = self._download() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + self._upload() + + # TODO: This is a dumb, brute force method of downlolading the memory. + # it would be smarter to only load the active areas and none of + # the padding/unused areas. + def _download(self): + """Talk to a wouxun KG-UV8D and do a download""" + try: + self._identify() + return self._do_download(0, 32768, 64) + except errors.RadioError: + raise + except Exception, e: + LOG.exception('Unknown error during download process') + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def _do_download(self, start, end, blocksize): + # allocate & fill memory + image = "" + for i in range(start, end, blocksize): + req = chr(i / 256) + chr(i % 256) + chr(blocksize) + self._write_record(CMD_RD, req) + cs_error, resp = self._read_record() + if cs_error: + # TODO: probably should retry a few times here + LOG.debug(util.hexprint(resp)) + raise Exception("Checksum error on read") + LOG.debug("Got:\n%s" % util.hexprint(resp)) + image += resp[2:] + if self.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning from radio" + self.status_fn(status) + self._finish() + return memmap.MemoryMap(''.join(image)) + + def _upload(self): + """Talk to a wouxun KG-UV8D and do a upload""" + try: + self._identify() + self._do_upload(0, 32768, 64) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + return + + def _do_upload(self, start, end, blocksize): + ptr = start + for i in range(start, end, blocksize): + req = chr(i / 256) + chr(i % 256) + chunk = self.get_mmap()[ptr:ptr + blocksize] + self._write_record(CMD_WR, req + chunk) + LOG.debug(util.hexprint(req + chunk)) + cserr, ack = self._read_record() + LOG.debug(util.hexprint(ack)) + j = ord(ack[0]) * 256 + ord(ack[1]) + if cserr or j != ptr: + raise Exception("Radio did not ack block %i" % ptr) + ptr += blocksize + if self.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning to radio" + self.status_fn(status) + self._finish() + + def get_features(self): + # TODO: This probably needs to be setup correctly to match the true + # features of the radio + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_bank = False + rf.can_odd_split = True + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_tuning_steps = STEPS + rf.valid_cross_modes = [ + "Tone->Tone", + "Tone->DTCS", + "DTCS->Tone", + "DTCS->", + "->Tone", + "->DTCS", + "DTCS->DTCS", + ] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_name_length = 8 + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_bands = [(134000000, 175000000), # supports 2m + (400000000, 520000000)] # supports 70cm + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.memory_bounds = (1, 999) # 999 memories + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("This radio driver is currently under development. " + "There are no known issues with it, but you should " + "proceed with caution.") + return rp + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + tpol = False + if _mem.txtone != 0xFFFF and (_mem.txtone & 0x2800) == 0x2800: + tcode, tpol = _get_dcs(_mem.txtone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.txtone != 0xFFFF and _mem.txtone != 0x0: + mem.rtone = (_mem.txtone & 0x7fff) / 10.0 + txmode = "Tone" + else: + txmode = "" + + rpol = False + if _mem.rxtone != 0xFFFF and (_mem.rxtone & 0x2800) == 0x2800: + rcode, rpol = _get_dcs(_mem.rxtone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rxtone != 0xFFFF and _mem.rxtone != 0x0: + mem.ctone = (_mem.rxtone & 0x7fff) / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + # always set it even if no dtcs is used + mem.dtcs_polarity = "%s%s" % (tpol or "N", rpol or "N") + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.txtone, rxmode, _mem.rxtone)) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + mem = chirp_common.Memory() + mem.number = number + _valid = self._memobj.valid[mem.number] + + LOG.debug("%d %s", number, _valid == MEM_VALID) + if _valid != MEM_VALID: + mem.empty = True + return mem + else: + mem.empty = False + + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _nam.name: + if char != 0: + mem.name += chr(char) + mem.name = mem.name.rstrip() + + self._get_tone(_mem, mem) + + mem.skip = "" if bool(_mem.scan_add) else "S" + + mem.power = self.POWER_LEVELS[_mem.power] + mem.mode = _mem.iswide and "FM" or "NFM" + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rxtone = txtone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + txtone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rxtone = txtone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + txtone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rxtone = int(mem.ctone * 10) + + _mem.rxtone = rxtone + _mem.txtone = txtone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.txtone, rx_mode, _mem.rxtone)) + + def set_memory(self, mem): + number = mem.number + + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + if mem.empty: + _mem.set_raw("\x00" * (_mem.size() / 8)) + self._memobj.valid[number] = 0 + self._memobj.names[number].set_raw("\x00" * (_nam.size() / 8)) + return + + _mem.rxfreq = int(mem.freq / 10) + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "split": + _mem.txfreq = int(mem.offset / 10) + elif mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10) + elif mem.duplex == "-": + _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10) + else: + _mem.txfreq = int(mem.freq / 10) + _mem.scan_add = int(mem.skip != "S") + _mem.iswide = int(mem.mode == "FM") + # set the tone + self._set_tone(mem, _mem) + # set the power + if mem.power: + _mem.power = self.POWER_LEVELS.index(mem.power) + else: + _mem.power = True + # TODO: set the correct mute mode, for now just + # set to mute mode to QT (not QT+DTMF or QT*DTMF) + _mem.mute_mode = 0 + + for i in range(0, len(_nam.name)): + if i < len(mem.name) and mem.name[i]: + _nam.name[i] = ord(mem.name[i]) + else: + _nam.name[i] = 0x0 + self._memobj.valid[mem.number] = MEM_VALID + + def _get_settings(self): + _settings = self._memobj.settings + _vfoa = self._memobj.vfoa + _vfob = self._memobj.vfob + cfg_grp = RadioSettingGroup("cfg_grp", "Configuration") + vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings") + vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings") + key_grp = RadioSettingGroup("key_grp", "Key Settings") + lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits") + oem_grp = RadioSettingGroup("oem_grp", "OEM Info") + + group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp, + key_grp, lmt_grp, oem_grp) + + # + # Configuration Settings + # + rs = RadioSetting("channel_menu", "Menu available in channel mode", + RadioSettingValueBoolean(_settings.channel_menu)) + cfg_grp.append(rs) + rs = RadioSetting("ponmsg", "Poweron message", + RadioSettingValueList( + PONMSG_LIST, PONMSG_LIST[_settings.ponmsg])) + cfg_grp.append(rs) + rs = RadioSetting("voice", "Voice Guide", + RadioSettingValueBoolean(_settings.voice)) + cfg_grp.append(rs) + rs = RadioSetting("language", "Language", + RadioSettingValueList(LANGUAGE_LIST, + LANGUAGE_LIST[_settings. + language])) + cfg_grp.append(rs) + rs = RadioSetting("timeout", "Timeout Timer", + RadioSettingValueInteger(15, 900, + _settings.timeout * 15, 15)) + cfg_grp.append(rs) + rs = RadioSetting("toalarm", "Timeout Alarm", + RadioSettingValueInteger(0, 10, _settings.toalarm)) + cfg_grp.append(rs) + rs = RadioSetting("roger_beep", "Roger Beep", + RadioSettingValueBoolean(_settings.roger_beep)) + cfg_grp.append(rs) + rs = RadioSetting("power_save", "Power save", + RadioSettingValueBoolean(_settings.power_save)) + cfg_grp.append(rs) + rs = RadioSetting("autolock", "Autolock", + RadioSettingValueBoolean(_settings.autolock)) + cfg_grp.append(rs) + rs = RadioSetting("keylock", "Keypad Lock", + RadioSettingValueBoolean(_settings.keylock)) + cfg_grp.append(rs) + rs = RadioSetting("beep", "Keypad Beep", + RadioSettingValueBoolean(_settings.beep)) + cfg_grp.append(rs) + rs = RadioSetting("stopwatch", "Stopwatch", + RadioSettingValueBoolean(_settings.stopwatch)) + cfg_grp.append(rs) + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList(BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings. + backlight])) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_st", "DTMF Sidetone", + RadioSettingValueList(DTMFST_LIST, + DTMFST_LIST[_settings. + dtmf_st])) + cfg_grp.append(rs) + rs = RadioSetting("ani-id_sw", "ANI-ID Switch", + RadioSettingValueBoolean(_settings.ani_sw)) + cfg_grp.append(rs) + rs = RadioSetting("ptt-id_delay", "PTT-ID Delay", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_settings.ptt_id])) + cfg_grp.append(rs) + rs = RadioSetting("ring_time", "Ring Time", + RadioSettingValueList(LIST_10, + LIST_10[_settings.ring_time])) + cfg_grp.append(rs) + rs = RadioSetting("scan_rev", "Scan Mode", + RadioSettingValueList(SCANMODE_LIST, + SCANMODE_LIST[_settings. + scan_rev])) + cfg_grp.append(rs) + rs = RadioSetting("vox", "VOX", + RadioSettingValueList(LIST_10, + LIST_10[_settings.vox])) + cfg_grp.append(rs) + rs = RadioSetting("prich_sw", "Priority Channel Switch", + RadioSettingValueBoolean(_settings.prich_sw)) + cfg_grp.append(rs) + rs = RadioSetting("pri_ch", "Priority Channel", + RadioSettingValueInteger(1, 999, _settings.pri_ch)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_mode", "Radio Mode", + RadioSettingValueList(RPTMODE_LIST, + RPTMODE_LIST[_settings. + rpt_mode])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_set", "Repeater Setting", + RadioSettingValueList(RPTSET_LIST, + RPTSET_LIST[_settings. + rpt_set])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_spk", "Repeater Mode Speaker", + RadioSettingValueBoolean(_settings.rpt_spk)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_ptt", "Repeater PTT", + RadioSettingValueBoolean(_settings.rpt_ptt)) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_tx_time", "DTMF Tx Duration", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[_settings. + dtmf_tx_time])) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_interval", "DTMF Interval", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[_settings. + dtmf_interval])) + cfg_grp.append(rs) + rs = RadioSetting("alert", "Alert Tone", + RadioSettingValueList(ALERTS_LIST, + ALERTS_LIST[_settings.alert])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_tone", "Repeater Tone", + RadioSettingValueBoolean(_settings.rpt_tone)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_hold", "Repeater Hold Time", + RadioSettingValueList(HOLD_TIMES, + HOLD_TIMES[_settings. + rpt_hold])) + cfg_grp.append(rs) + rs = RadioSetting("scan_det", "Scan DET", + RadioSettingValueBoolean(_settings.scan_det)) + cfg_grp.append(rs) + rs = RadioSetting("sc_qt", "SC-QT", + RadioSettingValueList(SCQT_LIST, + SCQT_LIST[_settings.smuteset])) + cfg_grp.append(rs) + rs = RadioSetting("smuteset", "SubFreq Mute", + RadioSettingValueList(SMUTESET_LIST, + SMUTESET_LIST[_settings. + smuteset])) + cfg_grp.append(rs) + _pwd = "".join(map(chr, _settings.mode_sw_pwd)) + val = RadioSettingValueString(0, 6, _pwd) + val.set_mutable(True) + rs = RadioSetting("mode_sw_pwd", "Mode Switch Password", val) + cfg_grp.append(rs) + _pwd = "".join(map(chr, _settings.reset_pwd)) + val = RadioSettingValueString(0, 6, _pwd) + val.set_mutable(True) + rs = RadioSetting("reset_pwd", "Reset Password", val) + cfg_grp.append(rs) + # + # VFO A Settings + # + rs = RadioSetting("vfoa_mode", "VFO A Workmode", + RadioSettingValueList(WORKMODE_LIST, + WORKMODE_LIST[_settings. + workmode_a])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa_chan", "VFO A Channel", + RadioSettingValueInteger(1, 999, _settings.work_cha)) + vfoa_grp.append(rs) + rs = RadioSetting("rxfreqa", "VFO A Rx Frequency", + RadioSettingValueInteger( + 134000000, 520000000, _vfoa.rxfreq * 10, 5000)) + vfoa_grp.append(rs) + rs = RadioSetting("txoffa", "VFO A Tx Offset", + RadioSettingValueInteger( + 0, 520000000, _vfoa.txoffset * 10, 5000)) + vfoa_grp.append(rs) + # u16 rxtone; + # u16 txtone; + rs = RadioSetting("vfoa_power", "VFO A Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfoa.power])) + vfoa_grp.append(rs) + # shift_dir:2 + rs = RadioSetting("vfoa_iswide", "VFO A NBFM", + RadioSettingValueList( + BANDWIDTH_LIST, BANDWIDTH_LIST[_vfoa.iswide])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa_mute_mode", "VFO A Mute", + RadioSettingValueList( + SPMUTE_LIST, SPMUTE_LIST[_vfoa.mute_mode])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa_step", "VFO A Step (kHz)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfoa.step])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa_squelch", "VFO A Squelch", + RadioSettingValueList( + LIST_10, LIST_10[_vfoa.squelch])) + vfoa_grp.append(rs) + rs = RadioSetting("bcl_a", "Busy Channel Lock-out A", + RadioSettingValueBoolean(_settings.bcl_a)) + vfoa_grp.append(rs) + # + # VFO B Settings + # + rs = RadioSetting("vfob_mode", "VFO B Workmode", + RadioSettingValueList( + WORKMODE_LIST, + WORKMODE_LIST[_settings.workmode_b])) + vfob_grp.append(rs) + rs = RadioSetting("vfob_chan", "VFO B Channel", + RadioSettingValueInteger(1, 999, _settings.work_chb)) + vfob_grp.append(rs) + rs = RadioSetting("rxfreqb", "VFO B Rx Frequency", + RadioSettingValueInteger( + 134000000, 520000000, _vfob.rxfreq * 10, 5000)) + vfob_grp.append(rs) + rs = RadioSetting("txoffb", "VFO B Tx Offset", + RadioSettingValueInteger( + 0, 520000000, _vfob.txoffset * 10, 5000)) + vfob_grp.append(rs) + # u16 rxtone; + # u16 txtone; + rs = RadioSetting("vfob_power", "VFO B Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfob.power])) + vfob_grp.append(rs) + # shift_dir:2 + rs = RadioSetting("vfob_iswide", "VFO B NBFM", + RadioSettingValueList( + BANDWIDTH_LIST, BANDWIDTH_LIST[_vfob.iswide])) + vfob_grp.append(rs) + rs = RadioSetting("vfob_mute_mode", "VFO B Mute", + RadioSettingValueList( + SPMUTE_LIST, SPMUTE_LIST[_vfob.mute_mode])) + vfob_grp.append(rs) + rs = RadioSetting("vfob_step", "VFO B Step (kHz)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfob.step])) + vfob_grp.append(rs) + rs = RadioSetting("vfob_squelch", "VFO B Squelch", + RadioSettingValueList( + LIST_10, LIST_10[_vfob.squelch])) + vfob_grp.append(rs) + rs = RadioSetting("bcl_b", "Busy Channel Lock-out B", + RadioSettingValueBoolean(_settings.bcl_b)) + vfob_grp.append(rs) + # + # Key Settings + # + _msg = str(_settings.dispstr).split("\0")[0] + val = RadioSettingValueString(0, 15, _msg) + val.set_mutable(True) + rs = RadioSetting("dispstr", "Display Message", val) + key_grp.append(rs) + _ani = "" + for i in _settings.ani: + if i < 10: + _ani += chr(i + 0x30) + else: + break + val = RadioSettingValueString(0, 6, _ani) + val.set_mutable(True) + rs = RadioSetting("ani", "ANI code", val) + key_grp.append(rs) + rs = RadioSetting("pf1_func", "PF1 Key function", + RadioSettingValueList( + PF1KEY_LIST, + PF1KEY_LIST[self._memobj.settings.pf1_func])) + key_grp.append(rs) + rs = RadioSetting("pf3_func", "PF3 Key function", + RadioSettingValueList( + PF3KEY_LIST, + PF3KEY_LIST[self._memobj.settings.pf3_func])) + key_grp.append(rs) + + # + # Scan Group Settings + # + # settings: + # u8 scg_a; + # u8 scg_b; + # + # struct { + # u16 lower; + # u16 upper; + # } scan_groups[10]; + + # + # Call group settings + # + + # + # Limits settings + # + rs = RadioSetting("urx_start", "UHF RX Lower Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.rx_start * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("urx_stop", "UHF RX Upper Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.rx_stop * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("utx_start", "UHF TX Lower Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.tx_start * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("utx_stop", "UHF TX Upper Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.tx_stop * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("vrx_start", "VHF RX Lower Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.rx_start * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("vrx_stop", "VHF RX Upper Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.rx_stop * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("vtx_start", "VHF TX Lower Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.tx_start * 10, 5000)) + lmt_grp.append(rs) + rs = RadioSetting("vtx_stop", "VHF TX Upper Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.tx_stop * 10, 5000)) + lmt_grp.append(rs) + + # + # OEM info + # + def _decode(lst): + _str = ''.join([chr(c) for c in lst + if chr(c) in chirp_common.CHARSET_ASCII]) + return _str + + _str = _decode(self._memobj.oem_info.model) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("model", "Model", val) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.oem1) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem1", "OEM String 1", val) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.oem2) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem2", "OEM String 2", val) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.version) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("version", "Software Version", val) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.date) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("date", "OEM Date", val) + oem_grp.append(rs) + + return group + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None diff --git a/chirp/drivers/kguv8dplus.py b/chirp/drivers/kguv8dplus.py new file mode 100644 index 0000000..b4a0cb8 --- /dev/null +++ b/chirp/drivers/kguv8dplus.py @@ -0,0 +1,1111 @@ +# Copyright 2017 Krystian Struzik +# Based on Ron Wellsted driver for Wouxun KG-UV8D. +# KG-UV8D Plus model has all serial data encrypted. +# Figured out how the data is encrypted and implement +# serial data encryption and decryption functions. +# The algorithm of decryption works like this: +# - the first byte of data stream is XOR by const 57h +# - each next byte is encoded by previous byte using the XOR +# including the checksum (e.g data[i - 1] xor data[i]) +# I also changed the data structure to fit radio memory +# and implement set_settings function. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Wouxun KG-UV8D Plus radio management module""" + +import time +import os +import logging +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +CMD_ID = 128 +CMD_END = 129 +CMD_RD = 130 +CMD_WR = 131 + +MEM_VALID = 158 + +AB_LIST = ["A", "B"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +STEP_LIST = [str(x) for x in STEPS] +ROGER_LIST = ["Off", "Begin", "End", "Both"] +TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)] +VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)] +BANDWIDTH_LIST = ["Narrow", "Wide"] +VOICE_LIST = ["Off", "On"] +LANGUAGE_LIST = ["Chinese", "English"] +SCANMODE_LIST = ["TO", "CO", "SE"] +PF1KEY_LIST = ["Call", "VFTX"] +PF3KEY_LIST = ["Disable", "Scan", "Lamp", "Tele Alarm", "SOS-CH", "Radio"] +WORKMODE_LIST = ["VFO", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"] +BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \ + ["Always Off"] +OFFSET_LIST = ["+", "-"] +PONMSG_LIST = ["Bitmap", "Battery Volts"] +SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"] +DTMFST_LIST = ["DT-ST", "ANI-ST", "DT-ANI", "Off"] +DTMF_TIMES = ["%s" % x for x in range(50, 501, 10)] +RPTSET_LIST = ["X-TWRPT", "X-DIRRPT"] +ALERTS = [1750, 2100, 1000, 1450] +ALERTS_LIST = [str(x) for x in ALERTS] +PTTID_LIST = ["Begin", "End", "Both"] +LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)] +SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)] +SCQT_LIST = ["Decoder", "Encoder", "All"] +SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx/Rx"] +POWER_LIST = ["Lo", "Hi"] +HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)] +RPTMODE_LIST = ["Radio", "Repeater"] + +# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999) +# structure elements whose name starts with x are currently unidentified + +_MEM_FORMAT = """ + #seekto 0x0044; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } uhf_limits; + + #seekto 0x0054; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } vhf_limits; + + #seekto 0x0400; + struct { + u8 oem1[8]; + u8 unknown[2]; + u8 unknown2[10]; + u8 unknown3[10]; + u8 unknown4[8]; + u8 model[10]; + u8 version[6]; + u8 date[8]; + u8 unknown5[1]; + u8 oem2[8]; + } oem_info; + + #seekto 0x0480; + struct { + u16 lower; + u16 upper; + } scan_groups[10]; + + #seekto 0x0500; + struct { + u8 call_code[6]; + } call_groups[20]; + + #seekto 0x0580; + struct { + char call_name[6]; + } call_group_name[20]; + + #seekto 0x0800; + struct { + u8 ponmsg; + char dispstr[15]; + u8 x0810; + u8 x0811; + u8 x0812; + u8 x0813; + u8 x0814; + u8 voice; + u8 timeout; + u8 toalarm; + u8 channel_menu; + u8 power_save; + u8 autolock; + u8 keylock; + u8 beep; + u8 stopwatch; + u8 vox; + u8 scan_rev; + u8 backlight; + u8 roger_beep; + u8 x0822[6]; + u8 x0823[6]; + u16 pri_ch; + u8 ani_sw; + u8 ptt_delay; + u8 ani_code[6]; + u8 dtmf_st; + u8 bcl_a; + u8 bcl_b; + u8 ptt_id; + u8 prich_sw; + u8 rpt_set; + u8 rpt_spk; + u8 rpt_ptt; + u8 alert; + u8 pf1_func; + u8 pf3_func; + u8 x0843; + u8 workmode_a; + u8 workmode_b; + u8 dtmf_tx_time; + u8 dtmf_interval; + u8 main_ab; + u16 work_cha; + u16 work_chb; + u8 x084d; + u8 x084e; + u8 x084f; + u8 x0850; + u8 x0851; + u8 x0852; + u8 x0853; + u8 x0854; + u8 rpt_mode; + u8 language; + u8 x0857; + u8 x0858; + u8 x0859; + u8 x085a; + u8 x085b; + u8 x085c; + u8 x085d; + u8 x085e; + u8 single_display; + u8 ring_time; + u8 scg_a; + u8 scg_b; + u8 x0863; + u8 rpt_tone; + u8 rpt_hold; + u8 scan_det; + u8 sc_qt; + u8 x0868; + u8 smuteset; + u8 callcode; + } settings; + + #seekto 0x0880; + struct { + u32 rxfreq; + u32 txoffset; + u16 rxtone; + u16 txtone; + u8 scrambler:4, + unknown1:2, + power:1, + unknown2:1; + u8 unknown3:1, + shift_dir:2 + unknown4:1, + compander:1, + mute_mode:2, + iswide:1; + u8 step; + u8 squelch; + } vfoa; + + #seekto 0x08c0; + struct { + u32 rxfreq; + u32 txoffset; + u16 rxtone; + u16 txtone; + u8 scrambler:4, + unknown1:2, + power:1, + unknown2:1; + u8 unknown3:1, + shift_dir:2 + unknown4:1, + compander:1, + mute_mode:2, + iswide:1; + u8 step; + u8 squelch; + } vfob; + + #seekto 0x0900; + struct { + u32 rxfreq; + u32 txfreq; + u16 rxtone; + u16 txtone; + u8 scrambler:4, + unknown1:2, + power:1, + unknown2:1; + u8 unknown3:2, + scan_add:1, + unknown4:1, + compander:1, + mute_mode:2, + iswide:1; + u16 padding; + } memory[1000]; + + #seekto 0x4780; + struct { + u8 name[8]; + u8 unknown[4]; + } names[1000]; + + #seekto 0x7670; + u8 valid[1000]; + """ + +# Support for the Wouxun KG-UV8D Plus radio +# Serial coms are at 19200 baud +# The data is passed in variable length records +# Record structure: +# Offset Usage +# 0 start of record (\x7a) +# 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write) +# 2 direction (\xff PC-> Radio, \x00 Radio -> PC) +# 3 length of payload (excluding header/checksum) (n) +# 4 payload (n bytes) +# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n +# +# Memory Read Records: +# the payload is 3 bytes, first 2 are offset (big endian), +# 3rd is number of bytes to read +# Memory Write Records: +# the maximum payload size (from the Wouxun software) seems to be 66 bytes +# (2 bytes location + 64 bytes data). + +@directory.register +class KGUV8DPlusRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + + """Wouxun KG-UV8D Plus""" + VENDOR = "Wouxun" + MODEL = "KG-UV8D Plus" + _model = "KG-UV8D" + _file_ident = "kguv8dplus" + BAUD_RATE = 19200 + POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1), + chirp_common.PowerLevel("H", watts=5)] + _mmap = "" + + def _checksum(self, data): + cs = 0 + for byte in data: + cs += ord(byte) + return chr(cs % 256) + + def _write_record(self, cmd, payload = None): + # build the packet + _header = '\x7a' + chr(cmd) + '\xff' + + _length = 0 + if payload: + _length = len(payload) + + # update the length field + _header += chr(_length) + if payload: + # calculate checksum then add it with the payload to the packet and encrypt + crc = self._checksum(_header[1:] + payload) + payload += crc + _header += self.encrypt(payload) + else: + # calculate and add encrypted checksum to the packet + crc = self._checksum(_header[1:]) + _header += self.strxor(crc, '\x57') + LOG.debug("Sent:\n%s" % util.hexprint(_header)) + self.pipe.write(_header) + + def _read_record(self): + # read 4 chars for the header + _header = self.pipe.read(4) + if len(_header) != 4: + raise errors.RadioError('Radio did not respond') + _length = ord(_header[3]) + _packet = self.pipe.read(_length) + _rcs_xor = _packet[-1] + _packet = self.decrypt(_packet) + _cs = ord(self._checksum(_header[1:] + _packet)) + # read the checksum and decrypt it + _rcs = ord(self.strxor(self.pipe.read(1), _rcs_xor)) + LOG.debug("_cs =%x", _cs) + LOG.debug("_rcs=%x", _rcs) + return (_rcs != _cs, _packet) + + def decrypt(self, data): + result = '' + for i in range(len(data)-1, 0, -1): + result += self.strxor(data[i], data[i - 1]) + result += self.strxor(data[0], '\x57') + return result[::-1] + + def encrypt(self, data): + result = self.strxor('\x57', data[0]) + for i in range(1, len(data), 1): + result += self.strxor(result[i - 1], data[i]) + return result + + def strxor (self, xora, xorb): + return chr(ord(xora) ^ ord(xorb)) + +# Identify the radio +# +# A Gotcha: the first identify packet returns a bad checksum, subsequent +# attempts return the correct checksum... (well it does on my radio!) +# +# The ID record returned by the radio also includes the current frequency range +# as 4 bytes big-endian in 10Hz increments +# +# Offset +# 0:10 Model, zero padded (Use first 7 chars for 'KG-UV8D') +# 11:14 UHF rx lower limit (in units of 10Hz) +# 15:18 UHF rx upper limit +# 19:22 UHF tx lower limit +# 23:26 UHF tx upper limit +# 27:30 VHF rx lower limit +# 31:34 VHF rx upper limit +# 35:38 VHF tx lower limit +# 39:42 VHF tx upper limit +# + @classmethod + def match_model(cls, filedata, filename): + return cls._file_ident in 'kg' + filedata[0x426:0x430].replace('(', '').replace(')', '').lower() + + def _identify(self): + """Do the identification dance""" + for _i in range(0, 10): + self._write_record(CMD_ID) + _chksum_err, _resp = self._read_record() + LOG.debug("Got:\n%s" % util.hexprint(_resp)) + if _chksum_err: + LOG.error("Checksum error: retrying ident...") + time.sleep(0.100) + continue + LOG.debug("Model %s" % util.hexprint(_resp[0:7])) + if _resp[0:7] == self._model: + return + if len(_resp) == 0: + raise Exception("Radio not responding") + else: + raise Exception("Unable to identify radio") + + def _finish(self): + self._write_record(CMD_END) + + def process_mmap(self): + self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + self._mmap = self._download() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + self._upload() + + # TODO: Load all memory. + # It would be smarter to only load the active areas and none of + # the padding/unused areas. Padding still need to be investigated. + def _download(self): + """Talk to a wouxun KG-UV8D Plus and do a download""" + try: + self._identify() + return self._do_download(0, 32768, 64) + except errors.RadioError: + raise + except Exception, e: + LOG.exception('Unknown error during download process') + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def _do_download(self, start, end, blocksize): + # allocate & fill memory + image = "" + for i in range(start, end, blocksize): + req = chr(i / 256) + chr(i % 256) + chr(blocksize) + self._write_record(CMD_RD, req) + cs_error, resp = self._read_record() + if cs_error: + LOG.debug(util.hexprint(resp)) + raise Exception("Checksum error on read") + LOG.debug("Got:\n%s" % util.hexprint(resp)) + image += resp[2:] + if self.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning from radio" + self.status_fn(status) + self._finish() + return memmap.MemoryMap(''.join(image)) + + def _upload(self): + """Talk to a wouxun KG-UV8D Plus and do a upload""" + try: + self._identify() + self._do_upload(0, 32768, 64) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + return + + def _do_upload(self, start, end, blocksize): + ptr = start + for i in range(start, end, blocksize): + req = chr(i / 256) + chr(i % 256) + chunk = self.get_mmap()[ptr:ptr + blocksize] + self._write_record(CMD_WR, req + chunk) + LOG.debug(util.hexprint(req + chunk)) + cserr, ack = self._read_record() + LOG.debug(util.hexprint(ack)) + j = ord(ack[0]) * 256 + ord(ack[1]) + if cserr or j != ptr: + raise Exception("Radio did not ack block %i" % ptr) + ptr += blocksize + if self.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning to radio" + self.status_fn(status) + self._finish() + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_bank = False + rf.can_odd_split = True + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = [ + "Tone->Tone", + "Tone->DTCS", + "DTCS->Tone", + "DTCS->", + "->Tone", + "->DTCS", + "DTCS->DTCS", + ] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_name_length = 8 + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_bands = [(134000000, 175000000), # supports 2m + (400000000, 520000000)] # supports 70cm + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_tuning_steps = STEPS + rf.memory_bounds = (1, 999) # 999 memories + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("This radio driver is currently under development. " + "There are no known issues with it, but you should " + "proceed with caution.") + return rp + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + tpol = False + if _mem.txtone != 0xFFFF and (_mem.txtone & 0x2800) == 0x2800: + tcode, tpol = _get_dcs(_mem.txtone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.txtone != 0xFFFF and _mem.txtone != 0x0: + mem.rtone = (_mem.txtone & 0x7fff) / 10.0 + txmode = "Tone" + else: + txmode = "" + + rpol = False + if _mem.rxtone != 0xFFFF and (_mem.rxtone & 0x2800) == 0x2800: + rcode, rpol = _get_dcs(_mem.rxtone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rxtone != 0xFFFF and _mem.rxtone != 0x0: + mem.ctone = (_mem.rxtone & 0x7fff) / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + # always set it even if no dtcs is used + mem.dtcs_polarity = "%s%s" % (tpol or "N", rpol or "N") + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.txtone, rxmode, _mem.rxtone)) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + mem = chirp_common.Memory() + mem.number = number + _valid = self._memobj.valid[mem.number] + LOG.debug("%d %s", number, _valid == MEM_VALID) + if _valid != MEM_VALID: + mem.empty = True + return mem + else: + mem.empty = False + + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _nam.name: + if char != 0: + mem.name += chr(char) + mem.name = mem.name.rstrip() + + self._get_tone(_mem, mem) + + mem.skip = "" if bool(_mem.scan_add) else "S" + + mem.power = self.POWER_LEVELS[_mem.power] + mem.mode = _mem.iswide and "FM" or "NFM" + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rxtone = txtone = 0x0000 + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + txtone = int(mem.rtone * 10) + 0x8000 + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rxtone = txtone = int(mem.ctone * 10) + 0x8000 + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + txtone = int(mem.rtone * 10) + 0x8000 + if rx_mode == "DTCS": + rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rxtone = int(mem.ctone * 10) + 0x8000 + + _mem.rxtone = rxtone + _mem.txtone = txtone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.txtone, rx_mode, _mem.rxtone)) + + def set_memory(self, mem): + number = mem.number + + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + if mem.empty: + _mem.set_raw("\x00" * (_mem.size() / 8)) + self._memobj.valid[number] = 0 + self._memobj.names[number].set_raw("\x00" * (_nam.size() / 8)) + return + + _mem.rxfreq = int(mem.freq / 10) + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "split": + _mem.txfreq = int(mem.offset / 10) + elif mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10) + elif mem.duplex == "-": + _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10) + else: + _mem.txfreq = int(mem.freq / 10) + _mem.scan_add = int(mem.skip != "S") + _mem.iswide = int(mem.mode == "FM") + # set the tone + self._set_tone(mem, _mem) + # set the scrambler and compander to off by default + _mem.scrambler = 0 + _mem.compander = 0 + # set the power + if mem.power: + _mem.power = self.POWER_LEVELS.index(mem.power) + else: + _mem.power = True + # set to mute mode to QT (not QT+DTMF or QT*DTMF) by default + _mem.mute_mode = 0 + + for i in range(0, len(_nam.name)): + if i < len(mem.name) and mem.name[i]: + _nam.name[i] = ord(mem.name[i]) + else: + _nam.name[i] = 0x0 + self._memobj.valid[mem.number] = MEM_VALID + + def _get_settings(self): + _settings = self._memobj.settings + _vfoa = self._memobj.vfoa + _vfob = self._memobj.vfob + cfg_grp = RadioSettingGroup("cfg_grp", "Configuration") + vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings") + vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings") + key_grp = RadioSettingGroup("key_grp", "Key Settings") + lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits") + uhf_lmt_grp = RadioSettingGroup("uhf_lmt_grp", "UHF") + vhf_lmt_grp = RadioSettingGroup("vhf_lmt_grp", "VHF") + oem_grp = RadioSettingGroup("oem_grp", "OEM Info") + + lmt_grp.append(uhf_lmt_grp); + lmt_grp.append(vhf_lmt_grp); + group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp, + key_grp, lmt_grp, oem_grp) + + # + # Configuration Settings + # + rs = RadioSetting("channel_menu", "Menu available in channel mode", + RadioSettingValueBoolean(_settings.channel_menu)) + cfg_grp.append(rs) + rs = RadioSetting("ponmsg", "Poweron message", + RadioSettingValueList( + PONMSG_LIST, PONMSG_LIST[_settings.ponmsg])) + cfg_grp.append(rs) + rs = RadioSetting("voice", "Voice Guide", + RadioSettingValueBoolean(_settings.voice)) + cfg_grp.append(rs) + rs = RadioSetting("language", "Language", + RadioSettingValueList(LANGUAGE_LIST, + LANGUAGE_LIST[_settings. + language])) + cfg_grp.append(rs) + rs = RadioSetting("timeout", "Timeout Timer", + RadioSettingValueList( + TIMEOUT_LIST, TIMEOUT_LIST[_settings.timeout])) + cfg_grp.append(rs) + rs = RadioSetting("toalarm", "Timeout Alarm", + RadioSettingValueInteger(0, 10, _settings.toalarm)) + cfg_grp.append(rs) + rs = RadioSetting("roger_beep", "Roger Beep", + RadioSettingValueList(ROGER_LIST, + ROGER_LIST[_settings.roger_beep])) + cfg_grp.append(rs) + rs = RadioSetting("power_save", "Power save", + RadioSettingValueBoolean(_settings.power_save)) + cfg_grp.append(rs) + rs = RadioSetting("autolock", "Autolock", + RadioSettingValueBoolean(_settings.autolock)) + cfg_grp.append(rs) + rs = RadioSetting("keylock", "Keypad Lock", + RadioSettingValueBoolean(_settings.keylock)) + cfg_grp.append(rs) + rs = RadioSetting("beep", "Keypad Beep", + RadioSettingValueBoolean(_settings.beep)) + cfg_grp.append(rs) + rs = RadioSetting("stopwatch", "Stopwatch", + RadioSettingValueBoolean(_settings.stopwatch)) + cfg_grp.append(rs) + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList(BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings. + backlight])) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_st", "DTMF Sidetone", + RadioSettingValueList(DTMFST_LIST, + DTMFST_LIST[_settings. + dtmf_st])) + cfg_grp.append(rs) + rs = RadioSetting("ani_sw", "ANI-ID Switch", + RadioSettingValueBoolean(_settings.ani_sw)) + cfg_grp.append(rs) + rs = RadioSetting("ptt_id", "PTT-ID Delay", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_settings.ptt_id])) + cfg_grp.append(rs) + rs = RadioSetting("ring_time", "Ring Time", + RadioSettingValueList(LIST_10, + LIST_10[_settings.ring_time])) + cfg_grp.append(rs) + rs = RadioSetting("scan_rev", "Scan Mode", + RadioSettingValueList(SCANMODE_LIST, + SCANMODE_LIST[_settings. + scan_rev])) + cfg_grp.append(rs) + rs = RadioSetting("vox", "VOX", + RadioSettingValueList(LIST_10, + LIST_10[_settings.vox])) + cfg_grp.append(rs) + rs = RadioSetting("prich_sw", "Priority Channel Switch", + RadioSettingValueBoolean(_settings.prich_sw)) + cfg_grp.append(rs) + rs = RadioSetting("pri_ch", "Priority Channel", + RadioSettingValueInteger(1, 999, _settings.pri_ch)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_mode", "Radio Mode", + RadioSettingValueList(RPTMODE_LIST, + RPTMODE_LIST[_settings. + rpt_mode])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_set", "Repeater Setting", + RadioSettingValueList(RPTSET_LIST, + RPTSET_LIST[_settings. + rpt_set])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_spk", "Repeater Mode Speaker", + RadioSettingValueBoolean(_settings.rpt_spk)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_ptt", "Repeater PTT", + RadioSettingValueBoolean(_settings.rpt_ptt)) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_tx_time", "DTMF Tx Duration", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[_settings. + dtmf_tx_time])) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_interval", "DTMF Interval", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[_settings. + dtmf_interval])) + cfg_grp.append(rs) + rs = RadioSetting("alert", "Alert Tone", + RadioSettingValueList(ALERTS_LIST, + ALERTS_LIST[_settings.alert])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_tone", "Repeater Tone", + RadioSettingValueBoolean(_settings.rpt_tone)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_hold", "Repeater Hold Time", + RadioSettingValueList(HOLD_TIMES, + HOLD_TIMES[_settings. + rpt_hold])) + cfg_grp.append(rs) + rs = RadioSetting("scan_det", "Scan DET", + RadioSettingValueBoolean(_settings.scan_det)) + cfg_grp.append(rs) + rs = RadioSetting("sc_qt", "SC-QT", + RadioSettingValueList(SCQT_LIST, + SCQT_LIST[_settings.sc_qt])) + cfg_grp.append(rs) + rs = RadioSetting("smuteset", "SubFreq Mute", + RadioSettingValueList(SMUTESET_LIST, + SMUTESET_LIST[_settings. + smuteset])) + cfg_grp.append(rs) + + # + # VFO A Settings + # + rs = RadioSetting("workmode_a", "VFO A Workmode", + RadioSettingValueList(WORKMODE_LIST, WORKMODE_LIST[_settings.workmode_a])) + vfoa_grp.append(rs) + rs = RadioSetting("work_cha", "VFO A Channel", + RadioSettingValueInteger(1, 999, _settings.work_cha)) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.rxfreq", "VFO A Rx Frequency", + RadioSettingValueInteger( + 134000000, 520000000, _vfoa.rxfreq * 10, 5000)) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.txoffset", "VFO A Tx Offset", + RadioSettingValueInteger( + 0, 520000000, _vfoa.txoffset * 10, 5000)) + vfoa_grp.append(rs) + # u16 rxtone; + # u16 txtone; + rs = RadioSetting("vfoa.power", "VFO A Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfoa.power])) + vfoa_grp.append(rs) + # shift_dir:2 + rs = RadioSetting("vfoa.iswide", "VFO A NBFM", + RadioSettingValueList( + BANDWIDTH_LIST, BANDWIDTH_LIST[_vfoa.iswide])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.mute_mode", "VFO A Mute", + RadioSettingValueList( + SPMUTE_LIST, SPMUTE_LIST[_vfoa.mute_mode])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.step", "VFO A Step (kHz)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfoa.step])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.squelch", "VFO A Squelch", + RadioSettingValueList( + LIST_10, LIST_10[_vfoa.squelch])) + vfoa_grp.append(rs) + rs = RadioSetting("bcl_a", "Busy Channel Lock-out A", + RadioSettingValueBoolean(_settings.bcl_a)) + vfoa_grp.append(rs) + + # + # VFO B Settings + # + rs = RadioSetting("workmode_b", "VFO B Workmode", + RadioSettingValueList(WORKMODE_LIST, WORKMODE_LIST[_settings.workmode_b])) + vfob_grp.append(rs) + rs = RadioSetting("work_chb", "VFO B Channel", + RadioSettingValueInteger(1, 999, _settings.work_chb)) + vfob_grp.append(rs) + rs = RadioSetting("vfob.rxfreq", "VFO B Rx Frequency", + RadioSettingValueInteger( + 134000000, 520000000, _vfob.rxfreq * 10, 5000)) + vfob_grp.append(rs) + rs = RadioSetting("vfob.txoffset", "VFO B Tx Offset", + RadioSettingValueInteger( + 0, 520000000, _vfob.txoffset * 10, 5000)) + vfob_grp.append(rs) + # u16 rxtone; + # u16 txtone; + rs = RadioSetting("vfob.power", "VFO B Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfob.power])) + vfob_grp.append(rs) + # shift_dir:2 + rs = RadioSetting("vfob.iswide", "VFO B NBFM", + RadioSettingValueList( + BANDWIDTH_LIST, BANDWIDTH_LIST[_vfob.iswide])) + vfob_grp.append(rs) + rs = RadioSetting("vfob.mute_mode", "VFO B Mute", + RadioSettingValueList( + SPMUTE_LIST, SPMUTE_LIST[_vfob.mute_mode])) + vfob_grp.append(rs) + rs = RadioSetting("vfob.step", "VFO B Step (kHz)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfob.step])) + vfob_grp.append(rs) + rs = RadioSetting("vfob.squelch", "VFO B Squelch", + RadioSettingValueList( + LIST_10, LIST_10[_vfob.squelch])) + vfob_grp.append(rs) + rs = RadioSetting("bcl_b", "Busy Channel Lock-out B", + RadioSettingValueBoolean(_settings.bcl_b)) + vfob_grp.append(rs) + + # + # Key Settings + # + _msg = str(_settings.dispstr).split("\0")[0] + val = RadioSettingValueString(0, 15, _msg) + val.set_mutable(True) + rs = RadioSetting("dispstr", "Display Message", val) + key_grp.append(rs) + + dtmfchars = "0123456789" + _codeobj = _settings.ani_code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x0A]) + val = RadioSettingValueString(3, 6, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("ani_code", "ANI Code", val) + def apply_ani_id(setting, obj): + value = [] + for j in range(0, 6): + try: + value.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + value.append(0xFF) + obj.ani_code = value + rs.set_apply_callback(apply_ani_id, _settings) + key_grp.append(rs) + + rs = RadioSetting("pf1_func", "PF1 Key function", + RadioSettingValueList( + PF1KEY_LIST, + PF1KEY_LIST[_settings.pf1_func])) + key_grp.append(rs) + rs = RadioSetting("pf3_func", "PF3 Key function", + RadioSettingValueList( + PF3KEY_LIST, + PF3KEY_LIST[_settings.pf3_func])) + key_grp.append(rs) + + # + # Limits settings + # + rs = RadioSetting("uhf_limits.rx_start", "UHF RX Lower Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.rx_start * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("uhf_limits.rx_stop", "UHF RX Upper Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.rx_stop * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("uhf_limits.tx_start", "UHF TX Lower Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.tx_start * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("uhf_limits.tx_stop", "UHF TX Upper Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.tx_stop * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.rx_start", "VHF RX Lower Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.rx_start * 10, 5000)) + vhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.rx_stop", "VHF RX Upper Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.rx_stop * 10, 5000)) + vhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.tx_start", "VHF TX Lower Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.tx_start * 10, 5000)) + vhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.tx_stop", "VHF TX Upper Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.tx_stop * 10, 5000)) + vhf_lmt_grp.append(rs) + + # + # OEM info + # + def _decode(lst): + _str = ''.join([chr(c) for c in lst + if chr(c) in chirp_common.CHARSET_ASCII]) + return _str + + def do_nothing(setting, obj): + return + + _str = _decode(self._memobj.oem_info.model) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.model", "Model", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.oem1) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.oem1", "OEM String 1", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.oem2) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.oem2", "OEM String 2", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.version) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.version", "Software Version", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.date) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.date", "OEM Date", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + + return group + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + if self._is_freq(element): + setattr(obj, setting, int(element.value)/10) + else: + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _is_freq(self, element): + return "rxfreq" in element.get_name() or "txoffset" in element.get_name() or "rx_start" in element.get_name() or "rx_stop" in element.get_name() or "tx_start" in element.get_name() or "tx_stop" in element.get_name() \ No newline at end of file diff --git a/chirp/drivers/kguv8e.py b/chirp/drivers/kguv8e.py new file mode 100644 index 0000000..b76f0e5 --- /dev/null +++ b/chirp/drivers/kguv8e.py @@ -0,0 +1,1146 @@ +# Copyright 2019 Pavel Milanes CO7WT +# +# Based on the work of Krystian Struzik +# who figured out the crypt used and made possible the +# Wuoxun KG-UV8D Plus driver, in which this work is based. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Wouxun KG-UV8E radio management module""" + +import time +import os +import logging +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +CMD_ID = 128 +CMD_END = 129 +CMD_RD = 130 +CMD_WR = 131 + +MEM_VALID = 158 + +AB_LIST = ["A", "B"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +STEP_LIST = [str(x) for x in STEPS] +ROGER_LIST = ["Off", "Begin", "End", "Both"] +TIMEOUT_LIST = ["Off"] + [str(x) + "s" for x in range(15, 901, 15)] +VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 10)] +BANDWIDTH_LIST = ["Narrow", "Wide"] +VOICE_LIST = ["Off", "On"] +LANGUAGE_LIST = ["Chinese", "English"] +SCANMODE_LIST = ["TO", "CO", "SE"] +PF1KEY_LIST = ["Call", "VFTX"] +PF3KEY_LIST = ["Disable", "Scan", "Lamp", "Tele Alarm", "SOS-CH", "Radio"] +WORKMODE_LIST = ["VFO", "Channel No.", "Ch. No.+Freq.", "Ch. No.+Name"] +BACKLIGHT_LIST = ["Always On"] + [str(x) + "s" for x in range(1, 21)] + \ + ["Always Off"] +OFFSET_LIST = ["+", "-"] +PONMSG_LIST = ["Bitmap", "Battery Volts"] +SPMUTE_LIST = ["QT", "QT+DTMF", "QT*DTMF"] +DTMFST_LIST = ["DT-ST", "ANI-ST", "DT-ANI", "Off"] +DTMF_TIMES = ["%s" % x for x in range(50, 501, 10)] +RPTSET_LIST = ["", "X-DIRRPT", "X-TWRPT"] # TODO < what is index 0? +ALERTS = [1750, 2100, 1000, 1450] +ALERTS_LIST = [str(x) for x in ALERTS] +PTTID_LIST = ["Begin", "End", "Both"] +LIST_10 = ["Off"] + ["%s" % x for x in range(1, 11)] +SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)] +SCQT_LIST = ["Decoder", "Encoder", "All"] +SMUTESET_LIST = ["Off", "Tx", "Rx", "Tx/Rx"] +POWER_LIST = ["Lo", "Hi"] +HOLD_TIMES = ["Off"] + ["%s" % x for x in range(100, 5001, 100)] +RPTMODE_LIST = ["Radio", "Repeater"] + +# memory slot 0 is not used, start at 1 (so need 1000 slots, not 999) +# structure elements whose name starts with x are currently unidentified + +_MEM_FORMAT = """ + #seekto 0x0030; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } vhf1_limits; + + #seekto 0x0044; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } uhf_limits; + + #seekto 0x0054; + struct { + u32 rx_start; + u32 rx_stop; + u32 tx_start; + u32 tx_stop; + } vhf_limits; + + #seekto 0x0400; + struct { + u8 oem1[8]; + u8 unknown[2]; + u8 unknown2[10]; + u8 unknown3[10]; + u8 unknown4[8]; + u8 model[10]; + u8 version[6]; + u8 date[8]; + u8 unknown5[1]; + u8 oem2[8]; + } oem_info; + + #seekto 0x0480; + struct { + u16 lower; + u16 upper; + } scan_groups[10]; + + #seekto 0x0500; + struct { + u8 call_code[6]; + } call_groups[20]; + + #seekto 0x0580; + struct { + char call_name[6]; + } call_group_name[20]; + + #seekto 0x0800; + struct { + u8 ponmsg; + char dispstr[15]; + u8 x0810; + u8 x0811; + u8 x0812; + u8 x0813; + u8 x0814; + u8 voice; + u8 timeout; + u8 toalarm; + u8 channel_menu; + u8 power_save; + u8 autolock; + u8 keylock; + u8 beep; + u8 stopwatch; + u8 vox; + u8 scan_rev; + u8 backlight; + u8 roger_beep; + u8 x0822[6]; + u8 x0823[6]; + u16 pri_ch; + u8 ani_sw; + u8 ptt_delay; + u8 ani_code[6]; + u8 dtmf_st; + u8 bcl_a; + u8 bcl_b; + u8 ptt_id; + u8 prich_sw; + u8 rpt_set; + u8 rpt_spk; + u8 rpt_ptt; + u8 alert; + u8 pf1_func; + u8 pf3_func; + u8 x0843; + u8 workmode_a; + u8 workmode_b; + u8 dtmf_tx_time; + u8 dtmf_interval; + u8 main_ab; + u16 work_cha; + u16 work_chb; + u8 x084d; + u8 x084e; + u8 x084f; + u8 x0850; + u8 x0851; + u8 x0852; + u8 x0853; + u8 x0854; + u8 rpt_mode; + u8 language; + u8 x0857; + u8 x0858; + u8 x0859; + u8 x085a; + u8 x085b; + u8 x085c; + u8 x085d; + u8 x085e; + u8 single_display; + u8 ring_time; + u8 scg_a; + u8 scg_b; + u8 x0863; + u8 rpt_tone; + u8 rpt_hold; + u8 scan_det; + u8 sc_qt; + u8 x0868; + u8 smuteset; + u8 callcode; + } settings; + + #seekto 0x0880; + struct { + u32 rxfreq; + u32 txoffset; + u16 rxtone; + u16 txtone; + u8 scrambler:4, + unknown1:2, + power:1, + unknown2:1; + u8 unknown3:1, + shift_dir:2 + unknown4:1, + compander:1, + mute_mode:2, + iswide:1; + u8 step; + u8 squelch; + } vfoa; + + #seekto 0x08c0; + struct { + u32 rxfreq; + u32 txoffset; + u16 rxtone; + u16 txtone; + u8 scrambler:4, + unknown1:2, + power:1, + unknown2:1; + u8 unknown3:1, + shift_dir:2 + unknown4:1, + compander:1, + mute_mode:2, + iswide:1; + u8 step; + u8 squelch; + } vfob; + + #seekto 0x0900; + struct { + u32 rxfreq; + u32 txfreq; + u16 rxtone; + u16 txtone; + u8 scrambler:4, + unknown1:2, + power:1, + unknown2:1; + u8 unknown3:2, + scan_add:1, + unknown4:1, + compander:1, + mute_mode:2, + iswide:1; + u16 padding; + } memory[1000]; + + #seekto 0x4780; + struct { + u8 name[8]; + u8 unknown[4]; + } names[1000]; + + #seekto 0x7670; + u8 valid[1000]; + """ + + # Support for the Wouxun KG-UV8E radio + # Serial coms are at 19200 baud + # The data is passed in variable length records + # Record structure: + # Offset Usage + # 0 start of record (\x7a) + # 1 Command (\x80 Identify \x81 End/Reboot \x82 Read \x83 Write) + # 2 direction (\xff PC-> Radio, \x00 Radio -> PC) + # 3 length of payload (excluding header/checksum) (n) + # 4 payload (n bytes) + # 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n + # + # Memory Read Records: + # the payload is 3 bytes, first 2 are offset (big endian), + # 3rd is number of bytes to read + # Memory Write Records: + # the maximum payload size (from the Wouxun software) seems to be 66 bytes + # (2 bytes location + 64 bytes data). + +@directory.register +class KGUV8ERadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + + """Wouxun KG-UV8E""" + VENDOR = "Wouxun" + MODEL = "KG-UV8E" + _model = "KG-UV8D-A" + _file_ident = "kguv8e" # lowercase + BAUD_RATE = 19200 + POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1), + chirp_common.PowerLevel("H", watts=5)] + _mmap = "" + + def _checksum(self, data): + cs = 0 + for byte in data: + cs += ord(byte) + return chr(cs % 256) + + def _write_record(self, cmd, payload = None): + # build the packet + _header = '\x7b' + chr(cmd) + '\xff' + + _length = 0 + if payload: + _length = len(payload) + + # update the length field + _header += chr(_length) + + if payload: + # calculate checksum then add it with the payload to the packet and encrypt + crc = self._checksum(_header[1:] + payload) + payload += crc + _header += self.encrypt(payload) + else: + # calculate and add encrypted checksum to the packet + crc = self._checksum(_header[1:]) + _header += self.strxor(crc, '\x57') + + try: + self.pipe.write(_header) + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def _read_record(self): + # read 4 chars for the header + _header = self.pipe.read(4) + if len(_header) != 4: + raise errors.RadioError('Radio did not respond') + _length = ord(_header[3]) + _packet = self.pipe.read(_length) + _rcs_xor = _packet[-1] + _packet = self.decrypt(_packet) + _cs = ord(self._checksum(_header[1:] + _packet)) + # read the checksum and decrypt it + _rcs = ord(self.strxor(self.pipe.read(1), _rcs_xor)) + return (_rcs != _cs, _packet) + + def decrypt(self, data): + result = '' + for i in range(len(data)-1, 0, -1): + result += self.strxor(data[i], data[i - 1]) + result += self.strxor(data[0], '\x57') + return result[::-1] + + def encrypt(self, data): + result = self.strxor('\x57', data[0]) + for i in range(1, len(data), 1): + result += self.strxor(result[i - 1], data[i]) + return result + + def strxor (self, xora, xorb): + return chr(ord(xora) ^ ord(xorb)) + + # Identify the radio + # + # A Gotcha: the first identify packet returns a bad checksum, subsequent + # attempts return the correct checksum... (well it does on my radio!) + # + # The ID record returned by the radio also includes the current frequency range + # as 4 bytes big-endian in 10Hz increments + # + # Offset + # 0:10 Model, zero padded (Use first 7 chars for 'KG-UV8D') + # 11:14 UHF rx lower limit (in units of 10Hz) + # 15:18 UHF rx upper limit + # 19:22 UHF tx lower limit + # 23:26 UHF tx upper limit + # 27:30 VHF rx lower limit + # 31:34 VHF rx upper limit + # 35:38 VHF tx lower limit + # 39:42 VHF tx upper limit + + @classmethod + def match_model(cls, filedata, filename): + id = cls._file_ident + return cls._file_ident in 'kg' + filedata[0x426:0x430].replace('(', '').replace(')', '').lower() + + def _identify(self): + """Do the identification dance""" + for _i in range(0, 10): + self._write_record(CMD_ID) + _chksum_err, _resp = self._read_record() + LOG.debug("Got:\n%s" % util.hexprint(_resp)) + if _chksum_err: + LOG.error("Checksum error: retrying ident...") + time.sleep(0.100) + continue + LOG.debug("Model %s" % util.hexprint(_resp[0:9])) + if _resp[0:9] == self._model: + return + if len(_resp) == 0: + raise Exception("Radio not responding") + else: + raise Exception("Unable to identify radio") + + def _finish(self): + self._write_record(CMD_END) + + def process_mmap(self): + self._memobj = bitwise.parse(_MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + self._mmap = self._download() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + self._upload() + + # TODO: Load all memory. + # It would be smarter to only load the active areas and none of + # the padding/unused areas. Padding still need to be investigated. + def _download(self): + """Talk to a wouxun KG-UV8E and do a download""" + try: + self._identify() + return self._do_download(0, 32768, 64) + except errors.RadioError: + raise + except Exception, e: + LOG.exception('Unknown error during download process') + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def _do_download(self, start, end, blocksize): + # allocate & fill memory + image = "" + for i in range(start, end, blocksize): + req = chr(i / 256) + chr(i % 256) + chr(blocksize) + self._write_record(CMD_RD, req) + cs_error, resp = self._read_record() + if cs_error: + LOG.debug(util.hexprint(resp)) + raise Exception("Checksum error on read") + LOG.debug("Got:\n%s" % util.hexprint(resp)) + image += resp[2:] + if self.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning from radio" + self.status_fn(status) + self._finish() + return memmap.MemoryMap(''.join(image)) + + def _upload(self): + """Talk to a wouxun KG-UV8E and do a upload""" + try: + self._identify() + self._do_upload(0, 32768, 64) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + return + + def _do_upload(self, start, end, blocksize): + ptr = start + for i in range(start, end, blocksize): + req = chr(i / 256) + chr(i % 256) + chunk = self.get_mmap()[ptr:ptr + blocksize] + self._write_record(CMD_WR, req + chunk) + # ~ LOG.debug(util.hexprint(req + chunk)) + cserr, ack = self._read_record() + # ~ LOG.debug(util.hexprint(ack)) + j = ord(ack[0]) * 256 + ord(ack[1]) + if cserr or j != ptr: + raise Exception("Radio did not ack block %i" % ptr) + ptr += blocksize + if self.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning to radio" + self.status_fn(status) + self._finish() + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_bank = False + rf.can_odd_split = True + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = [ + "Tone->Tone", + "Tone->DTCS", + "DTCS->Tone", + "DTCS->", + "->Tone", + "->DTCS", + "DTCS->DTCS", + ] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_name_length = 8 + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_bands = [(134000000, 175000000), # supports 2m + (220000000, 260000000), # supports 1.25m + (400000000, 520000000)] # supports 70cm + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.memory_bounds = (1, 999) # 999 memories + rf.valid_tuning_steps = STEPS + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental.\n' + '\n' + 'Please keep a copy of your memories with the original software ' + 'if you treasure them, this driver is new and may contain' + ' bugs.\n' + '\n' + ) + return rp + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + tpol = False + if _mem.txtone != 0xFFFF and (_mem.txtone & 0x2800) == 0x2800: + tcode, tpol = _get_dcs(_mem.txtone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.txtone != 0xFFFF and _mem.txtone != 0x0: + mem.rtone = (_mem.txtone & 0x7fff) / 10.0 + txmode = "Tone" + else: + txmode = "" + + rpol = False + if _mem.rxtone != 0xFFFF and (_mem.rxtone & 0x2800) == 0x2800: + rcode, rpol = _get_dcs(_mem.rxtone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rxtone != 0xFFFF and _mem.rxtone != 0x0: + mem.ctone = (_mem.rxtone & 0x7fff) / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + # always set it even if no dtcs is used + mem.dtcs_polarity = "%s%s" % (tpol or "N", rpol or "N") + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.txtone, rxmode, _mem.rxtone)) + + def get_memory(self, number): + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + mem = chirp_common.Memory() + mem.number = number + _valid = self._memobj.valid[mem.number] + LOG.debug("%d %s", number, _valid == MEM_VALID) + if _valid != MEM_VALID: + mem.empty = True + return mem + else: + mem.empty = False + + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _nam.name: + if char != 0: + mem.name += chr(char) + mem.name = mem.name.rstrip() + + self._get_tone(_mem, mem) + + mem.skip = "" if bool(_mem.scan_add) else "S" + + mem.power = self.POWER_LEVELS[_mem.power] + mem.mode = _mem.iswide and "FM" or "NFM" + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rxtone = txtone = 0x0000 + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + txtone = int(mem.rtone * 10) + 0x8000 + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rxtone = txtone = int(mem.ctone * 10) + 0x8000 + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + txtone = int(mem.rtone * 10) + 0x8000 + if rx_mode == "DTCS": + rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rxtone = int(mem.ctone * 10) + 0x8000 + + _mem.rxtone = rxtone + _mem.txtone = txtone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.txtone, rx_mode, _mem.rxtone)) + + def set_memory(self, mem): + number = mem.number + + _mem = self._memobj.memory[number] + _nam = self._memobj.names[number] + + if mem.empty: + _mem.set_raw("\x00" * (_mem.size() / 8)) + self._memobj.valid[number] = 0 + self._memobj.names[number].set_raw("\x00" * (_nam.size() / 8)) + return + + _mem.rxfreq = int(mem.freq / 10) + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "split": + _mem.txfreq = int(mem.offset / 10) + elif mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10) + elif mem.duplex == "-": + _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10) + else: + _mem.txfreq = int(mem.freq / 10) + _mem.scan_add = int(mem.skip != "S") + _mem.iswide = int(mem.mode == "FM") + # set the tone + self._set_tone(mem, _mem) + # set the scrambler and compander to off by default + _mem.scrambler = 0 + _mem.compander = 0 + # set the power + if mem.power: + _mem.power = self.POWER_LEVELS.index(mem.power) + else: + _mem.power = True + # set to mute mode to QT (not QT+DTMF or QT*DTMF) by default + _mem.mute_mode = 0 + + for i in range(0, len(_nam.name)): + if i < len(mem.name) and mem.name[i]: + _nam.name[i] = ord(mem.name[i]) + else: + _nam.name[i] = 0x0 + self._memobj.valid[mem.number] = MEM_VALID + + def _get_settings(self): + _settings = self._memobj.settings + _vfoa = self._memobj.vfoa + _vfob = self._memobj.vfob + cfg_grp = RadioSettingGroup("cfg_grp", "Configuration") + vfoa_grp = RadioSettingGroup("vfoa_grp", "VFO A Settings") + vfob_grp = RadioSettingGroup("vfob_grp", "VFO B Settings") + key_grp = RadioSettingGroup("key_grp", "Key Settings") + lmt_grp = RadioSettingGroup("lmt_grp", "Frequency Limits") + uhf_lmt_grp = RadioSettingGroup("uhf_lmt_grp", "UHF") + vhf_lmt_grp = RadioSettingGroup("vhf_lmt_grp", "VHF") + vhf1_lmt_grp = RadioSettingGroup("vhf1_lmt_grp", "VHF1") + oem_grp = RadioSettingGroup("oem_grp", "OEM Info") + + lmt_grp.append(vhf_lmt_grp); + lmt_grp.append(vhf1_lmt_grp); + lmt_grp.append(uhf_lmt_grp); + group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp, + key_grp, lmt_grp, oem_grp) + + # + # Configuration Settings + # + rs = RadioSetting("channel_menu", "Menu available in channel mode", + RadioSettingValueBoolean(_settings.channel_menu)) + cfg_grp.append(rs) + rs = RadioSetting("ponmsg", "Poweron message", + RadioSettingValueList( + PONMSG_LIST, PONMSG_LIST[_settings.ponmsg])) + cfg_grp.append(rs) + rs = RadioSetting("voice", "Voice Guide", + RadioSettingValueBoolean(_settings.voice)) + cfg_grp.append(rs) + rs = RadioSetting("language", "Language", + RadioSettingValueList(LANGUAGE_LIST, + LANGUAGE_LIST[_settings. + language])) + cfg_grp.append(rs) + rs = RadioSetting("timeout", "Timeout Timer", + RadioSettingValueList( + TIMEOUT_LIST, TIMEOUT_LIST[_settings.timeout])) + cfg_grp.append(rs) + rs = RadioSetting("toalarm", "Timeout Alarm", + RadioSettingValueInteger(0, 10, _settings.toalarm)) + cfg_grp.append(rs) + rs = RadioSetting("roger_beep", "Roger Beep", + RadioSettingValueList(ROGER_LIST, + ROGER_LIST[_settings.roger_beep])) + cfg_grp.append(rs) + rs = RadioSetting("power_save", "Power save", + RadioSettingValueBoolean(_settings.power_save)) + cfg_grp.append(rs) + rs = RadioSetting("autolock", "Autolock", + RadioSettingValueBoolean(_settings.autolock)) + cfg_grp.append(rs) + rs = RadioSetting("keylock", "Keypad Lock", + RadioSettingValueBoolean(_settings.keylock)) + cfg_grp.append(rs) + rs = RadioSetting("beep", "Keypad Beep", + RadioSettingValueBoolean(_settings.beep)) + cfg_grp.append(rs) + rs = RadioSetting("stopwatch", "Stopwatch", + RadioSettingValueBoolean(_settings.stopwatch)) + cfg_grp.append(rs) + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList(BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings. + backlight])) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_st", "DTMF Sidetone", + RadioSettingValueList(DTMFST_LIST, + DTMFST_LIST[_settings. + dtmf_st])) + cfg_grp.append(rs) + rs = RadioSetting("ani_sw", "ANI-ID Switch", + RadioSettingValueBoolean(_settings.ani_sw)) + cfg_grp.append(rs) + rs = RadioSetting("ptt_id", "PTT-ID Delay", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_settings.ptt_id])) + cfg_grp.append(rs) + rs = RadioSetting("ring_time", "Ring Time", + RadioSettingValueList(LIST_10, + LIST_10[_settings.ring_time])) + cfg_grp.append(rs) + rs = RadioSetting("scan_rev", "Scan Mode", + RadioSettingValueList(SCANMODE_LIST, + SCANMODE_LIST[_settings. + scan_rev])) + cfg_grp.append(rs) + rs = RadioSetting("vox", "VOX", + RadioSettingValueList(LIST_10, + LIST_10[_settings.vox])) + cfg_grp.append(rs) + rs = RadioSetting("prich_sw", "Priority Channel Switch", + RadioSettingValueBoolean(_settings.prich_sw)) + cfg_grp.append(rs) + rs = RadioSetting("pri_ch", "Priority Channel", + RadioSettingValueInteger(1, 999, _settings.pri_ch)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_mode", "Radio Mode", + RadioSettingValueList(RPTMODE_LIST, + RPTMODE_LIST[_settings. + rpt_mode])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_set", "Repeater Setting", + RadioSettingValueList(RPTSET_LIST, + RPTSET_LIST[_settings. + rpt_set])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_spk", "Repeater Mode Speaker", + RadioSettingValueBoolean(_settings.rpt_spk)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_ptt", "Repeater PTT", + RadioSettingValueBoolean(_settings.rpt_ptt)) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_tx_time", "DTMF Tx Duration", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[_settings. + dtmf_tx_time])) + cfg_grp.append(rs) + rs = RadioSetting("dtmf_interval", "DTMF Interval", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[_settings. + dtmf_interval])) + cfg_grp.append(rs) + rs = RadioSetting("alert", "Alert Tone", + RadioSettingValueList(ALERTS_LIST, + ALERTS_LIST[_settings.alert])) + cfg_grp.append(rs) + rs = RadioSetting("rpt_tone", "Repeater Tone", + RadioSettingValueBoolean(_settings.rpt_tone)) + cfg_grp.append(rs) + rs = RadioSetting("rpt_hold", "Repeater Hold Time", + RadioSettingValueList(HOLD_TIMES, + HOLD_TIMES[_settings. + rpt_hold])) + cfg_grp.append(rs) + rs = RadioSetting("scan_det", "Scan DET", + RadioSettingValueBoolean(_settings.scan_det)) + cfg_grp.append(rs) + rs = RadioSetting("sc_qt", "SC-QT", + RadioSettingValueList(SCQT_LIST, + SCQT_LIST[_settings.sc_qt])) + cfg_grp.append(rs) + rs = RadioSetting("smuteset", "SubFreq Mute", + RadioSettingValueList(SMUTESET_LIST, + SMUTESET_LIST[_settings. + smuteset])) + cfg_grp.append(rs) + + # + # VFO A Settings + # + rs = RadioSetting("workmode_a", "VFO A Workmode", + RadioSettingValueList(WORKMODE_LIST, WORKMODE_LIST[_settings.workmode_a])) + vfoa_grp.append(rs) + rs = RadioSetting("work_cha", "VFO A Channel", + RadioSettingValueInteger(1, 999, _settings.work_cha)) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.rxfreq", "VFO A Rx Frequency", + RadioSettingValueInteger( + 134000000, 520000000, _vfoa.rxfreq * 10, 5000)) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.txoffset", "VFO A Tx Offset", + RadioSettingValueInteger( + 0, 520000000, _vfoa.txoffset * 10, 5000)) + vfoa_grp.append(rs) + # u16 rxtone; + # u16 txtone; + rs = RadioSetting("vfoa.power", "VFO A Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfoa.power])) + vfoa_grp.append(rs) + # shift_dir:2 + rs = RadioSetting("vfoa.iswide", "VFO A NBFM", + RadioSettingValueList( + BANDWIDTH_LIST, BANDWIDTH_LIST[_vfoa.iswide])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.mute_mode", "VFO A Mute", + RadioSettingValueList( + SPMUTE_LIST, SPMUTE_LIST[_vfoa.mute_mode])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.step", "VFO A Step (kHz)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfoa.step])) + vfoa_grp.append(rs) + rs = RadioSetting("vfoa.squelch", "VFO A Squelch", + RadioSettingValueList( + LIST_10, LIST_10[_vfoa.squelch])) + vfoa_grp.append(rs) + rs = RadioSetting("bcl_a", "Busy Channel Lock-out A", + RadioSettingValueBoolean(_settings.bcl_a)) + vfoa_grp.append(rs) + + # + # VFO B Settings + # + rs = RadioSetting("workmode_b", "VFO B Workmode", + RadioSettingValueList(WORKMODE_LIST, WORKMODE_LIST[_settings.workmode_b])) + vfob_grp.append(rs) + rs = RadioSetting("work_chb", "VFO B Channel", + RadioSettingValueInteger(1, 999, _settings.work_chb)) + vfob_grp.append(rs) + rs = RadioSetting("vfob.rxfreq", "VFO B Rx Frequency", + RadioSettingValueInteger( + 134000000, 520000000, _vfob.rxfreq * 10, 5000)) + vfob_grp.append(rs) + rs = RadioSetting("vfob.txoffset", "VFO B Tx Offset", + RadioSettingValueInteger( + 0, 520000000, _vfob.txoffset * 10, 5000)) + vfob_grp.append(rs) + # u16 rxtone; + # u16 txtone; + rs = RadioSetting("vfob.power", "VFO B Power", + RadioSettingValueList( + POWER_LIST, POWER_LIST[_vfob.power])) + vfob_grp.append(rs) + # shift_dir:2 + rs = RadioSetting("vfob.iswide", "VFO B NBFM", + RadioSettingValueList( + BANDWIDTH_LIST, BANDWIDTH_LIST[_vfob.iswide])) + vfob_grp.append(rs) + rs = RadioSetting("vfob.mute_mode", "VFO B Mute", + RadioSettingValueList( + SPMUTE_LIST, SPMUTE_LIST[_vfob.mute_mode])) + vfob_grp.append(rs) + rs = RadioSetting("vfob.step", "VFO B Step (kHz)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfob.step])) + vfob_grp.append(rs) + rs = RadioSetting("vfob.squelch", "VFO B Squelch", + RadioSettingValueList( + LIST_10, LIST_10[_vfob.squelch])) + vfob_grp.append(rs) + rs = RadioSetting("bcl_b", "Busy Channel Lock-out B", + RadioSettingValueBoolean(_settings.bcl_b)) + vfob_grp.append(rs) + + # + # Key Settings + # + _msg = str(_settings.dispstr).split("\0")[0] + val = RadioSettingValueString(0, 15, _msg) + val.set_mutable(True) + rs = RadioSetting("dispstr", "Display Message", val) + key_grp.append(rs) + + dtmfchars = "0123456789" + _codeobj = _settings.ani_code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x0A]) + val = RadioSettingValueString(3, 6, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("ani_code", "ANI Code", val) + def apply_ani_id(setting, obj): + value = [] + for j in range(0, 6): + try: + value.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + value.append(0xFF) + obj.ani_code = value + rs.set_apply_callback(apply_ani_id, _settings) + key_grp.append(rs) + + rs = RadioSetting("pf1_func", "PF1 Key function", + RadioSettingValueList( + PF1KEY_LIST, + PF1KEY_LIST[_settings.pf1_func])) + key_grp.append(rs) + rs = RadioSetting("pf3_func", "PF3 Key function", + RadioSettingValueList( + PF3KEY_LIST, + PF3KEY_LIST[_settings.pf3_func])) + key_grp.append(rs) + + # + # Limits settings + # + rs = RadioSetting("vhf_limits.rx_start", "VHF RX Lower Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.rx_start * 10, 5000)) + vhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.rx_stop", "VHF RX Upper Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.rx_stop * 10, 5000)) + vhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.tx_start", "VHF TX Lower Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.tx_start * 10, 5000)) + vhf_lmt_grp.append(rs) + rs = RadioSetting("vhf_limits.tx_stop", "VHF TX Upper Limit", + RadioSettingValueInteger( + 134000000, 174997500, + self._memobj.vhf_limits.tx_stop * 10, 5000)) + vhf_lmt_grp.append(rs) + + rs = RadioSetting("vhf1_limits.rx_start", "VHF1 RX Lower Limit", + RadioSettingValueInteger( + 220000000, 265000000, + self._memobj.vhf1_limits.rx_start * 10, 5000)) + vhf1_lmt_grp.append(rs) + rs = RadioSetting("vhf1_limits.rx_stop", "VHF1 RX Upper Limit", + RadioSettingValueInteger( + 220000000, 265000000, + self._memobj.vhf1_limits.rx_stop * 10, 5000)) + vhf1_lmt_grp.append(rs) + rs = RadioSetting("vhf1_limits.tx_start", "VHF1 TX Lower Limit", + RadioSettingValueInteger( + 220000000, 265000000, + self._memobj.vhf1_limits.tx_start * 10, 5000)) + vhf1_lmt_grp.append(rs) + rs = RadioSetting("vhf1_limits.tx_stop", "VHF1 TX Upper Limit", + RadioSettingValueInteger( + 220000000, 265000000, + self._memobj.vhf1_limits.tx_stop * 10, 5000)) + vhf1_lmt_grp.append(rs) + + rs = RadioSetting("uhf_limits.rx_start", "UHF RX Lower Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.rx_start * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("uhf_limits.rx_stop", "UHF RX Upper Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.rx_stop * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("uhf_limits.tx_start", "UHF TX Lower Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.tx_start * 10, 5000)) + uhf_lmt_grp.append(rs) + rs = RadioSetting("uhf_limits.tx_stop", "UHF TX Upper Limit", + RadioSettingValueInteger( + 400000000, 520000000, + self._memobj.uhf_limits.tx_stop * 10, 5000)) + uhf_lmt_grp.append(rs) + + # + # OEM info + # + def _decode(lst): + _str = ''.join([chr(c) for c in lst + if chr(c) in chirp_common.CHARSET_ASCII]) + return _str + + def do_nothing(setting, obj): + return + + _str = _decode(self._memobj.oem_info.model) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.model", "Model", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.oem1) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.oem1", "OEM String 1", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.oem2) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.oem2", "OEM String 2", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.version) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.version", "Software Version", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + _str = _decode(self._memobj.oem_info.date) + val = RadioSettingValueString(0, 15, _str) + val.set_mutable(False) + rs = RadioSetting("oem_info.date", "OEM Date", val) + rs.set_apply_callback(do_nothing, _settings) + oem_grp.append(rs) + + return group + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + if self._is_freq(element): + setattr(obj, setting, int(element.value)/10) + else: + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _is_freq(self, element): + return "rxfreq" in element.get_name() or "txoffset" in element.get_name() or "rx_start" in element.get_name() or "rx_stop" in element.get_name() or "tx_start" in element.get_name() or "tx_stop" in element.get_name() diff --git a/chirp/drivers/kguv9dplus.py b/chirp/drivers/kguv9dplus.py new file mode 100644 index 0000000..b836af6 --- /dev/null +++ b/chirp/drivers/kguv9dplus.py @@ -0,0 +1,1893 @@ +# Copyright 2018 Jim Lieb +# +# Driver for Wouxon KG-UV9D Plus +# +# Borrowed from other chirp drivers, especially the KG-UV8D Plus +# by Krystian Struzik +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Wouxun KG-UV9D Plus radio management module""" + +import time +import os +import logging +import struct +import string +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingValue, \ + RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings, InvalidValueError + +LOG = logging.getLogger(__name__) + +CMD_IDENT = 0x80 +CMD_HANGUP = 0x81 +CMD_RCONF = 0x82 +CMD_WCONF = 0x83 +CMD_RCHAN = 0x84 +CMD_WCHAN = 0x85 + +cmd_name = { + CMD_IDENT: "ident", + CMD_HANGUP: "hangup", + CMD_RCONF: "read config", + CMD_WCONF: "write config", + CMD_RCHAN: "read channel memory", # Unused + CMD_WCHAN: "write channel memory" # Unused because it is a hack. + } + +# This is used to write the configuration of the radio base on info +# gleaned from the downloaded app. There are empty spaces and we honor +# them because we don't know what they are (yet) although we read the +# whole of memory. +# +# Channel memory is separate. There are 1000 (1-999) channels. +# These are read/written to the radio in 4 channel (96 byte) +# records starting at address 0xa00 and ending at +# 0x4800 (presuming the end of channel 1000 is 0x4860-1 + +config_map = ( # map address, write size, write count + (0x40, 16, 1), # Passwords + (0x740, 40, 1), # FM chan 1-20 + (0x780, 16, 1), # vfo-b-150 + (0x790, 16, 1), # vfo-b-450 + (0x800, 16, 1), # vfo-a-150 + (0x810, 16, 1), # vfo-a-450 + (0x820, 16, 1), # vfo-a-300 + (0x830, 16, 1), # vfo-a-700 + (0x840, 16, 1), # vfo-a-200 + (0x860, 16, 1), # area-a-conf + (0x870, 16, 1), # area-b-conf + (0x880, 16, 1), # radio conf 0 + (0x890, 16, 1), # radio conf 1 + (0x8a0, 16, 1), # radio conf 2 + (0x8b0, 16, 1), # radio conf 3 + (0x8c0, 16, 1), # PTT-ANI + (0x8d0, 16, 1), # SCC + (0x8e0, 16, 1), # power save + (0x8f0, 16, 1), # Display banner + (0x940, 64, 2), # Scan groups and names + (0xa00, 64, 249), # Memory Channels 1-996 + (0x4840, 48, 1), # Memory Channels 997-999 + (0x4900, 32, 249), # Memory Names 1-996 + (0x6820, 24, 1), # Memory Names 997-999 + (0x7400, 64, 5), # CALL-ID 1-20, names 1-20 + ) + + +MEM_VALID = 0xfc +MEM_INVALID = 0xff + +# Radio memory map. This matches the reads/writes above. +# structure elements whose name starts with x are currently unidentified + +_MEM_FORMAT02 = """ +#seekto 0x40; + +struct { + char reset[6]; + char x46[2]; + char mode_sw[6]; + char x4e; +} passwords; + +#seekto 0x740; + +struct { + u16 fm_freq; +} fm_chans[20]; + +// each band has its own configuration, essentially its default params + +struct vfo { + u32 freq; + u32 offset; + u16 encqt; + u16 decqt; + u8 bit7_4:3, + qt:3, + bit1_0:2; + u8 bit7:1, + scan:1, + bit5:1, + pwr:2, + mod:1, + fm_dev:2; + u8 pad2:6, + shift:2; + u8 zeros; +}; + +#seekto 0x780; + +struct { + struct vfo band_150; + struct vfo band_450; +} vfo_b; + +#seekto 0x800; + +struct { + struct vfo band_150; + struct vfo band_450; + struct vfo band_300; + struct vfo band_700; + struct vfo band_200; +} vfo_a; + +// There are two independent radios, aka areas (as described +// in the manual as the upper and lower portions of the display... + +struct area_conf { + u8 w_mode; + u8 x861; + u8 w_chan; + u8 scan_grp; + u8 bcl; + u8 sql; + u8 cset; + u8 step; + u8 scan_mode; + u8 x869; + u8 scan_range; + u8 x86b; + u8 x86c; + u8 x86d; + u8 x86e; + u8 x86f; +}; + +#seekto 0x860; + +struct area_conf a_conf; + +#seekto 0x870; + +struct area_conf b_conf; + +#seekto 0x880; + +struct { + u8 menu_avail; + u8 reset_avail; + u8 x882; + u8 x883; + u8 lang; + u8 x885; + u8 beep; + u8 auto_am; + u8 qt_sw; + u8 lock; + u8 x88a; + u8 pf1; + u8 pf2; + u8 pf3; + u8 s_mute; + u8 type_set; + u8 tot; + u8 toa; + u8 ptt_id; + u8 x893; + u8 id_dly; + u8 x895; + u8 voice_sw; + u8 s_tone; + u8 abr_lvl; + u8 ring_time; + u8 roger; + u8 x89b; + u8 abr; + u8 save_m; + u8 lock_m; + u8 auto_lk; + u8 rpt_ptt; + u8 rpt_spk; + u8 rpt_rct; + u8 prich_sw; + u16 pri_ch; + u8 x8a6; + u8 x8a7; + u8 dtmf_st; + u8 dtmf_tx; + u8 x8aa; + u8 sc_qt; + u8 apo_tmr; + u8 vox_grd; + u8 vox_dly; + u8 rpt_kpt; + struct { + u16 scan_st; + u16 scan_end; + } a; + struct { + u16 scan_st; + u16 scan_end; + } b; + u8 x8b8; + u8 x8b9; + u8 x8ba; + u8 ponmsg; + u8 blcdsw; + u8 bledsw; + u8 x8be; + u8 x8bf; +} settings; + + +#seekto 0x8c0; +struct { + u8 code[6]; + char x8c6[10]; +} my_callid; + +#seekto 0x8d0; +struct { + u8 scc[6]; + char x8d6[10]; +} stun; + +#seekto 0x8e0; +struct { + u16 wake; + u16 sleep; +} save[4]; + +#seekto 0x8f0; +struct { + char banner[16]; +} display; + +#seekto 0x940; +struct { + struct { + i16 scan_st; + i16 scan_end; + } addrs[10]; + u8 x0968[8]; + struct { + char name[8]; + } names[10]; +} scn_grps; + +// this array of structs is marshalled via the R/WCHAN commands +#seekto 0xa00; +struct { + u32 rxfreq; + u32 txfreq; + u16 encQT; + u16 decQT; + u8 bit7_5:3, // all ones + qt:3, + bit1_0:2; + u8 bit7:1, + scan:1, + bit5:1, + pwr:2, + mod:1, + fm_dev:2; + u8 state; + u8 c3; +} chan_blk[999]; + +// nobody really sees this. It is marshalled with chan_blk +// in 4 entry chunks +#seekto 0x4900; + +// Tracks with the index of chan_blk[] +struct { + char name[8]; +} chan_name[999]; + +#seekto 0x7400; +struct { + u8 cid[6]; + u8 pad[2]; +}call_ids[20]; + +// This array tracks with the index of call_ids[] +struct { + char name[6]; + char pad[2]; +} cid_names[20]; + """ + + +# Support for the Wouxun KG-UV9D Plus radio +# Serial coms are at 19200 baud +# The data is passed in variable length records +# Record structure: +# Offset Usage +# 0 start of record (\x7d) +# 1 Command (6 commands, see above) +# 2 direction (\xff PC-> Radio, \x00 Radio -> PC) +# 3 length of payload (excluding header/checksum) (n) +# 4 payload (n bytes) +# 4+n+1 checksum - byte sum (% 256) of bytes 1 -> 4+n +# +# Memory Read Records: +# the payload is 3 bytes, first 2 are offset (big endian), +# 3rd is number of bytes to read +# Memory Write Records: +# the maximum payload size (from the Wouxun software) +# seems to be 66 bytes (2 bytes location + 64 bytes data). + +def _pkt_encode(op, payload): + """Assemble a packet for the radio and encode it for transmission. + Yes indeed, the checksum we store is only 4 bits. Why? + I suspect it's a bug in the radio firmware guys didn't want to fix, + i.e. a typo 0xff -> 0xf...""" + + data = bytearray() + data.append(0x7d) # tag that marks the beginning of the packet + data.append(op) + data.append(0xff) # 0xff is from app to radio + # calc checksum from op to end + cksum = op + 0xff + if (payload): + data.append(len(payload)) + cksum += len(payload) + for byte in payload: + cksum += byte + data.append(byte) + else: + data.append(0x00) + # Yea, this is a 4 bit cksum (also known as a bug) + data.append(cksum & 0xf) + + # now obfuscate by an xor starting with first payload byte ^ 0x52 + # including the trailing cksum. + xorbits = 0x52 + for i, byte in enumerate(data[4:]): + xord = xorbits ^ byte + data[i + 4] = xord + xorbits = xord + return(data) + + +def _pkt_decode(data): + """Take a packet hot off the wire and decode it into clear text + and return the fields. We say <> here because all it + turns out to be is annoying obfuscation. + This is the inverse of pkt_decode""" + + # we don't care about data[0]. + # It is always 0x7d and not included in checksum + op = data[1] + direction = data[2] + bytecount = data[3] + + # First un-obfuscate the payload and cksum + payload = bytearray() + xorbits = 0x52 + for i, byte in enumerate(data[4:]): + payload.append(xorbits ^ byte) + xorbits = byte + + # Calculate the checksum starting with the 3 bytes of the header + cksum = op + direction + bytecount + for byte in payload[:-1]: + cksum += byte + # yes, a 4 bit cksum to match the encode + cksum_match = (cksum & 0xf) == payload[-1] + if (not cksum_match): + LOG.debug( + "Checksum missmatch: %x != %x; " % (cksum, payload[-1])) + return (cksum_match, op, payload[:-1]) + +# UI callbacks to process input for mapping UI fields to memory cells + + +def freq2int(val, min, max): + """Convert a frequency as a string to a u32. Units is Hz + """ + _freq = chirp_common.parse_freq(str(val)) + if _freq > max or _freq < min: + raise InvalidValueError("Frequency %s is not with in %s-%s" % + (chirp_common.format_freq(_freq), + chirp_common.format_freq(min), + chirp_common.format_freq(max))) + return _freq + + +def int2freq(freq): + """ + Convert a u32 frequency to a string for UI data entry/display + This is stored in the radio as units of 10Hz which we compensate to Hz. + A value of -1 indicates , i.e. unused channel. + """ + if (int(freq) > 0): + f = chirp_common.format_freq(freq) + return f + else: + return "" + + +def freq2short(val, min, max): + """Convert a frequency as a string to a u16 which is units of 10KHz + """ + _freq = chirp_common.parse_freq(str(val)) + if _freq > max or _freq < min: + raise InvalidValueError("Frequency %s is not with in %s-%s" % + (chirp_common.format_freq(_freq), + chirp_common.format_freq(min), + chirp_common.format_freq(max))) + return _freq/100000 & 0xFFFF + + +def short2freq(freq): + """ + Convert a short frequency to a string for UI data entry/display + This is stored in the radio as units of 10KHz which we + compensate to Hz. + A value of -1 indicates , i.e. unused channel. + """ + if (int(freq) > 0): + f = chirp_common.format_freq(freq * 100000) + return f + else: + return "" + + +def tone2short(t): + """Convert a string tone or DCS to an encoded u16 + """ + tone = str(t) + if tone == "----": + u16tone = 0x0000 + elif tone[0] == 'D': # This is a DCS code + c = tone[1: -1] + code = int(c, 8) + if tone[-1] == 'I': + code |= 0x4000 + u16tone = code | 0x8000 + else: # This is an analog CTCSS + u16tone = int(tone[0:-2]+tone[-1]) & 0xffff # strip the '.' + return u16tone + + +def short2tone(tone): + """ Map a binary CTCSS/DCS to a string name for the tone + """ + if tone == 0 or tone == 0xffff: + ret = "----" + else: + code = tone & 0x3fff + if tone & 0x8000: # This is a DCS + if tone & 0x4000: # This is an inverse code + ret = "D%0.3oI" % code + else: + ret = "D%0.3oN" % code + else: # Just plain old analog CTCSS + ret = "%4.1f" % (code / 10.0) + return ret + + +def callid2str(cid): + """Caller ID per MDC-1200 spec? Must be 3-6 digits (100 - 999999). + One digit (binary) per byte, terminated with '0xc' + """ + + bin2ascii = " 1234567890" + cidstr = "" + for i in range(0, 6): + b = cid[i].get_value() + if b == 0xc: # the cid EOL + break + if b == 0 or b > 0xa: + raise InvalidValueError( + "Caller ID code has illegal byte 0x%x" % b) + cidstr += bin2ascii[b] + return cidstr + + +def str2callid(val): + """ Convert caller id strings from callid2str. + """ + ascii2bin = "0123456789" + s = str(val).strip() + if len(s) < 3 or len(s) > 6: + raise InvalidValueError( + "Caller ID must be at least 3 and no more than 6 digits") + if s[0] == '0': + raise InvalidValueError( + "First digit of a Caller ID cannot be a zero '0'") + blk = bytearray() + for c in s: + if c not in ascii2bin: + raise InvalidValueError( + "Caller ID must be all digits 0x%x" % c) + b = (0xa, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9)[int(c)] + blk.append(b) + if len(blk) < 6: + blk.append(0xc) # EOL a short ID + if len(blk) < 6: + for i in range(0, (6 - len(blk))): + blk.append(0xf0) + return blk + + +def digits2str(digits, padding=' ', width=6): + """Convert a password or SCC digit string to a string + Passwords are expanded to and must be 6 chars. Fill them with '0' + """ + + bin2ascii = "0123456789" + digitsstr = "" + for i in range(0, 6): + b = digits[i].get_value() + if b == 0xc: # the digits EOL + break + if b >= 0xa: + raise InvalidValueError( + "Value has illegal byte 0x%x" % ord(b)) + digitsstr += bin2ascii[b] + digitsstr = digitsstr.ljust(width, padding) + return digitsstr + + +def str2digits(val): + """ Callback for edited strings from digits2str. + """ + ascii2bin = " 0123456789" + s = str(val).strip() + if len(s) < 3 or len(s) > 6: + raise InvalidValueError( + "Value must be at least 3 and no more than 6 digits") + blk = bytearray() + for c in s: + if c not in ascii2bin: + raise InvalidValueError("Value must be all digits 0x%x" % c) + blk.append(int(c)) + for i in range(len(blk), 6): + blk.append(0xc) # EOL a short ID + return blk + + +def name2str(name): + """ Convert a callid or scan group name to a string + Deal with fixed field padding (\0 or \0xff) + """ + + namestr = "" + for i in range(0, len(name)): + b = ord(name[i].get_value()) + if b != 0 and b != 0xff: + namestr += chr(b) + return namestr + + +def str2name(val, size=6, fillchar='\0', emptyfill='\0'): + """ Convert a string to a name. A name is a 6 element bytearray + with ascii chars. + """ + val = str(val).rstrip(' \t\r\n\0\0xff') + if len(val) == 0: + name = "".ljust(size, emptyfill) + else: + name = val.ljust(size, fillchar) + return name + + +def pw2str(pw): + """Convert a password string (6 digits) to a string + Passwords must be 6 digits. If it is shorter, pad right with '0' + """ + pwstr = "" + ascii2bin = "0123456789" + for i in range(0, len(pw)): + b = pw[i].get_value() + if b not in ascii2bin: + raise InvalidValueError("Value must be digits 0-9") + pwstr += b + pwstr = pwstr.ljust(6, '0') + return pwstr + + +def str2pw(val): + """Store a password from UI to memory obj + If we clear the password (make it go away), change the + empty string to '000000' since the radio must have *something* + Also, fill a < 6 digit pw with 0's + """ + ascii2bin = "0123456789" + val = str(val).rstrip(' \t\r\n\0\0xff') + if len(val) == 0: # a null password + val = "000000" + for i in range(0, len(val)): + b = val[i] + if b not in ascii2bin: + raise InvalidValueError("Value must be digits 0-9") + if len(val) == 0: + pw = "".ljust(6, '\0') + else: + pw = val.ljust(6, '0') + return pw + + +# Helpers to replace python2 things like confused str/byte + +def _hex_print(data, addrfmt=None): + """Return a hexdump-like encoding of @data + We expect data to be a bytearray, not a string. + Expanded from borrowed code to use the first 2 bytes as the address + per comm packet format. + """ + if addrfmt is None: + addrfmt = '%(addr)03i' + addr = 0 + else: # assume first 2 bytes are address + a = struct.unpack(">H", data[0:2]) + addr = a[0] + data = data[2:] + + block_size = 16 + + lines = (len(data) / block_size) + if (len(data) % block_size > 0): + lines += 1 + + out = "" + left = len(data) + for block in range(0, lines): + addr += block * block_size + try: + out += addrfmt % locals() + except (OverflowError, ValueError, TypeError, KeyError): + out += "%03i" % addr + out += ': ' + + if left < block_size: + limit = left + else: + limit = block_size + + for j in range(0, block_size): + if (j < limit): + out += "%02x " % data[(block * block_size) + j] + else: + out += " " + + out += " " + + for j in range(0, block_size): + + if (j < limit): + _byte = data[(block * block_size) + j] + if _byte >= 0x20 and _byte < 0x7F: + out += "%s" % chr(_byte) + else: + out += "." + else: + out += " " + out += "\n" + if (left > block_size): + left -= block_size + + return out + + +# Useful UI lists +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +S_TONES = [str(x) for x in [1000, 1450, 1750, 2100]] +STEP_LIST = [str(x)+"kHz" for x in STEPS] +ROGER_LIST = ["Off", "Begin", "End", "Both"] +TIMEOUT_LIST = [str(x) + "s" for x in range(15, 601, 15)] +TOA_LIST = ["Off"] + ["%ds" % t for t in range(1, 10)] +BANDWIDTH_LIST = ["Wide", "Narrow"] +LANGUAGE_LIST = ["English", "Chinese"] +PF1KEY_LIST = ["OFF", "call id", "r-alarm", "SOS", "SF-TX"] +PF2KEY_LIST = ["OFF", "Scan", "Second", "lamp", "SDF-DIR", "K-lamp"] +PF3KEY_LIST = ["OFF", "Call ID", "R-ALARM", "SOS", "SF-TX"] +WORKMODE_LIST = ["VFO freq", "Channel No.", "Ch. No.+Freq.", + "Ch. No.+Name"] +BACKLIGHT_LIST = ["Off"] + ["%sS" % t for t in range(1, 31)] + \ + ["Always On"] +SAVE_MODES = ["Off", "1", "2", "3", "4"] +LOCK_MODES = ["key-lk", "key+pg", "key+ptt", "all"] +APO_TIMES = ["Off"] + ["%dm" % t for t in range(15, 151, 15)] +OFFSET_LIST = ["none", "+", "-"] +PONMSG_LIST = ["Battery Volts", "Bitmap"] +SPMUTE_LIST = ["QT", "QT*T", "QT&T"] +DTMFST_LIST = ["Off", "DT-ST", "ANI-ST", "DT-ANI"] +DTMF_TIMES = ["%d" % x for x in range(80, 501, 20)] +PTTID_LIST = ["Off", "Begin", "End", "Both"] +ID_DLY_LIST = ["%dms" % t for t in range(100, 3001, 100)] +VOX_GRDS = ["Off"] + ["%dlevel" % l for l in range(1, 11)] +VOX_DLYS = ["Off"] + ["%ds" % t for t in range(1, 5)] +RPT_KPTS = ["Off"] + ["%dms" % t for t in range(100, 5001, 100)] +LIST_1_5 = ["%s" % x for x in range(1, 6)] +LIST_0_9 = ["%s" % x for x in range(0, 10)] +LIST_1_20 = ["%s" % x for x in range(1, 21)] +LIST_OFF_10 = ["Off"] + ["%s" % x for x in range(1, 11)] +SCANGRP_LIST = ["All"] + ["%s" % x for x in range(1, 11)] +SCANMODE_LIST = ["TO", "CO", "SE"] +SCANRANGE_LIST = ["Current band", "freq range", "ALL"] +SCQT_LIST = ["Decoder", "Encoder", "Both"] +S_MUTE_LIST = ["off", "rx mute", "tx mute", "r/t mute"] +POWER_LIST = ["Low", "Med", "High"] +RPTMODE_LIST = ["Radio", "One direction Repeater", + "Two direction repeater"] +TONE_LIST = ["----"] + ["%s" % str(t) for t in chirp_common.TONES] + \ + ["D%0.3dN" % dts for dts in chirp_common.DTCS_CODES] + \ + ["D%0.3dI" % dts for dts in chirp_common.DTCS_CODES] + + +@directory.register +class KGUV9DPlusRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + + """Wouxun KG-UV9D Plus""" + VENDOR = "Wouxun" + MODEL = "KG-UV9D Plus" + _model = "KG-UV9D" + _rev = "00" # default rev for the radio I know about... + _file_ident = "kg-uv9d" + BAUD_RATE = 19200 + POWER_LEVELS = [chirp_common.PowerLevel("L", watts=1), + chirp_common.PowerLevel("M", watts=2), + chirp_common.PowerLevel("H", watts=5)] + _mmap = "" + + def _read_record(self): + """ Read and validate the header of a radio reply. + A record is a formatted byte stream as follows: + 0x7D All records start with this + opcode This is in the set of legal commands. + The radio reply matches the request + dir This is the direction, 0xFF to the radio, + 0x00 from the radio. + cnt Count of bytes in payload + (not including the trailing checksum byte) + + + """ + + # first get the header and validate it + data = bytearray(self.pipe.read(4)) + if (len(data) < 4): + raise errors.RadioError('Radio did not respond') + if (data[0] != 0x7D): + raise errors.RadioError( + 'Radio reply garbled (%02x)' % data[0]) + if (data[1] not in cmd_name): + raise errors.RadioError( + "Unrecognized opcode (%02x)" % data[1]) + if (data[2] != 0x00): + raise errors.RadioError( + "Direction incorrect. Got (%02x)" % data[2]) + payload_len = data[3] + # don't forget to read the checksum byte + data.extend(self.pipe.read(payload_len + 1)) + if (len(data) != (payload_len + 5)): # we got a short read + raise errors.RadioError( + "Radio reply wrong size. Wanted %d, got %d" % + ((payload_len + 1), (len(data) - 4))) + return _pkt_decode(data) + + def _write_record(self, cmd, payload=None): + """ Write a request packet to the radio. + """ + + packet = _pkt_encode(cmd, payload) + self.pipe.write(packet) + + @classmethod + def match_model(cls, filedata, filename): + """Look for bits in the file image and see if it looks + like ours... + TODO: there is a bunch of rubbish between 0x50 and 0x160 + that is still a known unknown + """ + return cls._file_ident in filedata[0x51:0x59].lower() + + def _identify(self): + """ Identify the radio + The ident block identifies the radio and its capabilities. + This block is always 78 bytes. The rev == '01' is the base + radio and '02' seems to be the '-Plus' version. + I don't really trust the content after the model and revision. + One would assume this is pretty much constant data but I have + seen differences between my radio and the dump named + KG-UV9D-Plus-OutOfBox-Read.txt from bug #3509. The first + five bands match the OEM windows + app except the 350-400 band. The OOB trace has the 700MHz + band different. This is speculation at this point. + + TODO: This could be smarter and reject a radio not actually + a UV9D... + """ + + for _i in range(0, 10): # retry 10 times if we get junk + self._write_record(CMD_IDENT) + chksum_match, op, _resp = self._read_record() + if len(_resp) == 0: + raise Exception("Radio not responding") + if len(_resp) != 74: + LOG.error( + "Expected and IDENT reply of 78 bytes. Got (%d)" % + len(_resp)) + continue + if not chksum_match: + LOG.error("Checksum error: retrying ident...") + time.sleep(0.100) + continue + if op != CMD_IDENT: + LOG.error("Expected IDENT reply. Got (%02x)" % op) + continue + LOG.debug("Got:\n%s" % _hex_print(_resp)) + (mod, rev) = struct.unpack(">7s2s", _resp[0:9]) + LOG.debug("Model %s, rev %s" % (mod, rev)) + if mod == self._model: + self._rev = rev + return + else: + raise Exception("Unable to identify radio") + raise Exception("All retries to identify failed") + + def process_mmap(self): + if self._rev == "02" or self._rev == "00": + self._memobj = bitwise.parse(_MEM_FORMAT02, self._mmap) + else: # this is where you elif the other variants and non-Plus radios + raise errors.RadioError( + "Unrecognized model variation (%s). No memory map for it" % + self._rev) + + def sync_in(self): + """ Public sync_in + Download contents of the radio. Throw errors back + to the core if the radio does not respond. + """ + try: + self._identify() + self._mmap = self._do_download() + self._write_record(CMD_HANGUP) + except errors.RadioError: + raise + except Exception, e: + LOG.exception('Unknown error during download process') + raise errors.RadioError( + "Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + """ Public sync_out + Upload the modified memory image into the radio. + """ + + try: + self._identify() + self._do_upload() + self._write_record(CMD_HANGUP) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError( + "Failed to communicate with radio: %s" % e) + return + + def _do_download(self): + """ Read the whole of radio memory in 64 byte chunks. + We load the config space followed by loading memory channels. + The radio seems to be a "clone" type and the memory channels + are actually within the config space. There are separate + commands (CMD_RCHAN, CMD_WCHAN) for reading channel memory but + these seem to be a hack that can only do 4 channels at a time. + Since the radio only supports 999, (can only support 3 chars + in the display UI?) although the vendors app reads 1000 + channels, it hacks back to config writes (CMD_WCONF) for the + last 3 channels and names. We keep it simple and just read + the whole thing even though the vendor app doesn't. Channels + are separate in their app simply because the radio protocol + has read/write commands to access it. What they do is simply + marshal the frequency+mode bits in 4 channel chunks followed + by a separate chunk of for names. In config space, they are two + separate arrays 1..999. Given that this space is not a + multiple of 4, there is hackery on upload to do the writes to + config space. See upload for this. + """ + + mem = bytearray(0x8000) # The radio's memory map is 32k + for addr in range(0, 0x8000, 64): + req = bytearray(struct.pack(">HB", addr, 64)) + self._write_record(CMD_RCONF, req) + chksum_match, op, resp = self._read_record() + if not chksum_match: + LOG.debug(_hex_print(resp)) + raise Exception( + "Checksum error while reading configuration (0x%x)" % + addr) + pa = struct.unpack(">H", resp[0:2]) + pkt_addr = pa[0] + payload = resp[2:] + if op != CMD_RCONF or addr != pkt_addr: + raise Exception( + "Expected CMD_RCONF (%x) reply. Got (%02x: %x)" % + (addr, op, pkt_addr)) + LOG.debug("Config read (0x%x):\n%s" % + (addr, _hex_print(resp, '0x%(addr)04x'))) + for i in range(0, len(payload) - 1): + mem[addr + i] = payload[i] + if self.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = 0x8000 + status.msg = "Cloning from radio" + self.status_fn(status) + strmem = "".join([chr(x) for x in mem]) + return memmap.MemoryMap(strmem) + + def _do_upload(self): + """Walk through the config map and write updated records to + the radio. The config map contains only the regions we know + about. We don't use the channel memory commands to avoid the + hackery of using config write commands to fill in the last + 3 channel memory and names slots. As we discover other useful + goodies in the map, we can add more slots... + """ + for ar, size, count in config_map: + for addr in range(ar, ar + (size*count), size): + req = bytearray(struct.pack(">H", addr)) + req.extend(self.get_mmap()[addr:addr + size]) + self._write_record(CMD_WCONF, req) + LOG.debug("Config write (0x%x):\n%s" % + (addr, _hex_print(req))) + chksum_match, op, ack = self._read_record() + LOG.debug("Config write ack [%x]\n%s" % + (addr, _hex_print(ack))) + a = struct.unpack(">H", ack) # big endian short... + ack = a[0] + if not chksum_match or op != CMD_WCONF or addr != ack: + msg = "" + if not chksum_match: + msg += "Checksum err, " + if op != CMD_WCONF: + msg += "cmd mismatch %x != %x, " % \ + (op, CMD_WCONF) + if addr != ack: + msg += "ack error %x != %x, " % (addr, ack) + raise Exception("Radio did not ack block: %s error" % msg) + if self.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = 0x8000 + status.msg = "Update radio" + self.status_fn(status) + + def get_features(self): + """ Public get_features + Return the features of this radio once we have identified + it and gotten its bits + """ + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_bank = False + rf.can_odd_split = True + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = [ + "Tone->Tone", + "Tone->DTCS", + "DTCS->Tone", + "DTCS->", + "->Tone", + "->DTCS", + "DTCS->DTCS", + ] + rf.valid_modes = ["FM", "NFM", "AM"] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_name_length = 8 + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_bands = [(108000000, 136000000), # Aircraft AM + (136000000, 180000000), # supports 2m + (230000000, 250000000), + (350000000, 400000000), + (400000000, 520000000), # supports 70cm + (700000000, 985000000)] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_tuning_steps = STEPS + rf.memory_bounds = (1, 999) # 999 memories + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("This radio driver is currently under development. " + "There are no known issues with it, but you should " + "proceed with caution.") + return rp + + def get_raw_memory(self, number): + return repr(self._memobj.chan_blk[number]) + + def _get_tone(self, _mem, mem): + """Decode both the encode and decode CTSS/DCS codes from + the memory channel and stuff them into the UI + memory channel row. + """ + txtone = short2tone(_mem.encQT) + rxtone = short2tone(_mem.decQT) + pt = "N" + pr = "N" + + if txtone == "----": + txmode = "" + elif txtone[0] == "D": + mem.dtcs = int(txtone[1:4]) + if txtone[4] == "I": + pt = "R" + txmode = "DTCS" + else: + mem.rtone = float(txtone) + txmode = "Tone" + + if rxtone == "----": + rxmode = "" + elif rxtone[0] == "D": + mem.rx_dtcs = int(rxtone[1:4]) + if rxtone[4] == "I": + pr = "R" + rxmode = "DTCS" + else: + mem.ctone = float(rxtone) + rxmode = "Tone" + + if txmode == "Tone" and len(rxmode) == 0: + mem.tmode = "Tone" + elif (txmode == rxmode and txmode == "Tone" and + mem.rtone == mem.ctone): + mem.tmode = "TSQL" + elif (txmode == rxmode and txmode == "DTCS" and + mem.dtcs == mem.rx_dtcs): + mem.tmode = "DTCS" + elif (len(rxmode) + len(txmode)) > 0: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = pt + pr + + LOG.debug("_get_tone: Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.encQT, rxmode, _mem.decQT)) + + def get_memory(self, number): + """ Public get_memory + Return the channel memory referenced by number to the UI. + """ + _mem = self._memobj.chan_blk[number - 1] + _nam = self._memobj.chan_name[number - 1] + + mem = chirp_common.Memory() + mem.number = number + _valid = _mem.state + if _valid != MEM_VALID and _valid != 0 and _valid != 2: + # In Issue #6995 we can find _valid values of 0 and 2 in the IMG + # so these values should be treated like MEM_VALID. + mem.empty = True + return mem + else: + mem.empty = False + + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.name = name2str(_nam.name) + + self._get_tone(_mem, mem) + + mem.skip = "" if bool(_mem.scan) else "S" + + mem.power = self.POWER_LEVELS[_mem.pwr] + if _mem.mod == 1: + mem.mode = "AM" + elif _mem.fm_dev == 0: + mem.mode = "FM" + else: + mem.mode = "NFM" + # qt has no home in the UI + return mem + + def _set_tone(self, mem, _mem): + """Update the memory channel block CTCC/DCS tones + from the UI fields + """ + def _set_dcs(code, pol): + val = int("%i" % code, 8) | 0x8000 + if pol == "R": + val |= 0x4000 + return val + + rx_mode = tx_mode = None + rxtone = txtone = 0x0000 + + if mem.tmode == "Tone": + tx_mode = "Tone" + txtone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rxtone = txtone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rxtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + txtone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + txtone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rxtone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rxtone = int(mem.ctone * 10) + + _mem.decQT = rxtone + _mem.encQT = txtone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.encQT, rx_mode, _mem.decQT)) + + def set_memory(self, mem): + """ Public set_memory + Inverse of get_memory. Update the radio memory image + from the mem object + """ + number = mem.number + + _mem = self._memobj.chan_blk[number - 1] + _nam = self._memobj.chan_name[number - 1] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + _nam.name = str2name("", 8, '\0', '\0') + _mem.state = MEM_INVALID + return + + _mem.rxfreq = int(mem.freq / 10) + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "split": + _mem.txfreq = int(mem.offset / 10) + elif mem.duplex == "+": + _mem.txfreq = int(mem.freq / 10) + int(mem.offset / 10) + elif mem.duplex == "-": + _mem.txfreq = int(mem.freq / 10) - int(mem.offset / 10) + else: + _mem.txfreq = int(mem.freq / 10) + _mem.scan = int(mem.skip != "S") + if mem.mode == "FM": + _mem.mod = 0 # make sure forced AM is off + _mem.fm_dev = 0 + elif mem.mode == "NFM": + _mem.mod = 0 + _mem.fm_dev = 1 + elif mem.mode == "AM": + _mem.mod = 1 # AM on + _mem.fm_dev = 1 # set NFM bandwidth + else: + _mem.mod = 0 + _mem.fm_dev = 0 # Catchall default is FM + # set the tone + self._set_tone(mem, _mem) + # set the power + if mem.power: + _mem.pwr = self.POWER_LEVELS.index(mem.power) + else: + _mem.pwr = True + + # Set fields we can't access via the UI table to safe defaults + _mem.qt = 0 # mute mode to QT + + _nam.name = str2name(mem.name, 8, '\0', '\0') + _mem.state = MEM_VALID + +# Build the UI configuration tabs +# the channel memory tab is built by the core. +# We have no control over it + + def _core_tab(self): + """ Build Core Configuration tab + Radio settings common to all modes and areas go here. + """ + s = self._memobj.settings + + cf = RadioSettingGroup("cfg_grp", "Configuration") + + cf.append(RadioSetting("auto_am", + "Auto detect AM(53)", + RadioSettingValueBoolean(s.auto_am))) + cf.append(RadioSetting("qt_sw", + "Scan tone detect(59)", + RadioSettingValueBoolean(s.qt_sw))) + cf.append( + RadioSetting("s_mute", + "SubFreq Mute(60)", + RadioSettingValueList(S_MUTE_LIST, + S_MUTE_LIST[s.s_mute]))) + cf.append( + RadioSetting("tot", + "Transmit timeout Timer(10)", + RadioSettingValueList(TIMEOUT_LIST, + TIMEOUT_LIST[s.tot]))) + cf.append( + RadioSetting("toa", + "Transmit Timeout Alarm(11)", + RadioSettingValueList(TOA_LIST, + TOA_LIST[s.toa]))) + cf.append( + RadioSetting("ptt_id", + "PTT Caller ID mode(23)", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[s.ptt_id]))) + cf.append( + RadioSetting("id_dly", + "Caller ID Delay time(25)", + RadioSettingValueList(ID_DLY_LIST, + ID_DLY_LIST[s.id_dly]))) + cf.append(RadioSetting("voice_sw", + "Voice Guide(12)", + RadioSettingValueBoolean(s.voice_sw))) + cf.append(RadioSetting("beep", + "Keypad Beep(13)", + RadioSettingValueBoolean(s.beep))) + cf.append( + RadioSetting("s_tone", + "Side Tone(36)", + RadioSettingValueList(S_TONES, + S_TONES[s.s_tone]))) + cf.append( + RadioSetting("ring_time", + "Ring Time(26)", + RadioSettingValueList( + LIST_OFF_10, + LIST_OFF_10[s.ring_time]))) + cf.append( + RadioSetting("roger", + "Roger Beep(9)", + RadioSettingValueList(ROGER_LIST, + ROGER_LIST[s.roger]))) + cf.append(RadioSetting("blcdsw", + "Backlight(41)", + RadioSettingValueBoolean(s.blcdsw))) + cf.append( + RadioSetting("abr", + "Auto Backlight Time(1)", + RadioSettingValueList(BACKLIGHT_LIST, + BACKLIGHT_LIST[s.abr]))) + cf.append( + RadioSetting("abr_lvl", + "Backlight Brightness(27)", + RadioSettingValueList(LIST_1_5, + LIST_1_5[s.abr_lvl]))) + cf.append(RadioSetting("lock", + "Keypad Lock", + RadioSettingValueBoolean(s.lock))) + cf.append( + RadioSetting("lock_m", + "Keypad Lock Mode(35)", + RadioSettingValueList(LOCK_MODES, + LOCK_MODES[s.lock_m]))) + cf.append(RadioSetting("auto_lk", + "Keypad Autolock(34)", + RadioSettingValueBoolean(s.auto_lk))) + cf.append(RadioSetting("prich_sw", + "Priority Channel Scan(33)", + RadioSettingValueBoolean(s.prich_sw))) + cf.append(RadioSetting("pri_ch", + "Priority Channel(32)", + RadioSettingValueInteger(1, 999, + s.pri_ch))) + cf.append( + RadioSetting("dtmf_st", + "DTMF Sidetone(22)", + RadioSettingValueList(DTMFST_LIST, + DTMFST_LIST[s.dtmf_st]))) + cf.append(RadioSetting("sc_qt", + "Scan QT Save Mode(38)", + RadioSettingValueList( + SCQT_LIST, + SCQT_LIST[s.sc_qt]))) + cf.append( + RadioSetting("apo_tmr", + "Automatic Power-off(39)", + RadioSettingValueList(APO_TIMES, + APO_TIMES[s.apo_tmr]))) + cf.append( # VOX "guard" is really VOX trigger audio level + RadioSetting("vox_grd", + "VOX level(7)", + RadioSettingValueList(VOX_GRDS, + VOX_GRDS[s.vox_grd]))) + cf.append( + RadioSetting("vox_dly", + "VOX Delay(37)", + RadioSettingValueList(VOX_DLYS, + VOX_DLYS[s.vox_dly]))) + cf.append( + RadioSetting("lang", + "Menu Language(14)", + RadioSettingValueList(LANGUAGE_LIST, + LANGUAGE_LIST[s.lang]))) + cf.append(RadioSetting("ponmsg", + "Poweron message(40)", + RadioSettingValueList( + PONMSG_LIST, PONMSG_LIST[s.ponmsg]))) + cf.append(RadioSetting("bledsw", + "Receive LED(42)", + RadioSettingValueBoolean(s.bledsw))) + return cf + + def _repeater_tab(self): + """Repeater mode functions + """ + s = self._memobj.settings + cf = RadioSettingGroup("repeater", "Repeater Functions") + + cf.append( + RadioSetting("type_set", + "Radio Mode(43)", + RadioSettingValueList( + RPTMODE_LIST, + RPTMODE_LIST[s.type_set]))) + cf.append(RadioSetting("rpt_ptt", + "Repeater PTT(45)", + RadioSettingValueBoolean(s.rpt_ptt))) + cf.append(RadioSetting("rpt_spk", + "Repeater Mode Speaker(44)", + RadioSettingValueBoolean(s.rpt_spk))) + cf.append( + RadioSetting("rpt_kpt", + "Repeater Hold Time(46)", + RadioSettingValueList(RPT_KPTS, + RPT_KPTS[s.rpt_kpt]))) + cf.append(RadioSetting("rpt_rct", + "Repeater Receipt Tone(47)", + RadioSettingValueBoolean(s.rpt_rct))) + return cf + + def _admin_tab(self): + """Admin functions not present in radio menu... + These are admin functions not radio operation configuration + """ + + def apply_cid(setting, obj): + c = str2callid(setting.value) + obj.code = c + + def apply_scc(setting, obj): + c = str2digits(setting.value) + obj.scc = c + + def apply_mode_sw(setting, obj): + pw = str2pw(setting.value) + obj.mode_sw = pw + setting.value = pw2str(obj.mode_sw) + + def apply_reset(setting, obj): + pw = str2pw(setting.value) + obj.reset = pw + setting.value = pw2str(obj.reset) + + def apply_wake(setting, obj): + obj.wake = int(setting.value)/10 + + def apply_sleep(setting, obj): + obj.sleep = int(setting.value)/10 + + pw = self._memobj.passwords # admin passwords + s = self._memobj.settings + + cf = RadioSettingGroup("admin", "Admin Functions") + + cf.append(RadioSetting("menu_avail", + "Menu available in channel mode", + RadioSettingValueBoolean(s.menu_avail))) + mode_sw = RadioSettingValueString(0, 6, + pw2str(pw.mode_sw), False) + rs = RadioSetting("passwords.mode_sw", + "Mode Switch Password", mode_sw) + rs.set_apply_callback(apply_mode_sw, pw) + cf.append(rs) + + cf.append(RadioSetting("reset_avail", + "Radio Reset Available", + RadioSettingValueBoolean(s.reset_avail))) + reset = RadioSettingValueString(0, 6, pw2str(pw.reset), False) + rs = RadioSetting("passwords.reset", + "Radio Reset Password", reset) + rs.set_apply_callback(apply_reset, pw) + cf.append(rs) + + cf.append( + RadioSetting("dtmf_tx", + "DTMF Tx Duration", + RadioSettingValueList(DTMF_TIMES, + DTMF_TIMES[s.dtmf_tx]))) + cid = self._memobj.my_callid + my_callid = RadioSettingValueString(3, 6, + callid2str(cid.code), False) + rs = RadioSetting("my_callid.code", + "PTT Caller ID code(24)", my_callid) + rs.set_apply_callback(apply_cid, cid) + cf.append(rs) + + stun = self._memobj.stun + st = RadioSettingValueString(0, 6, digits2str(stun.scc), False) + rs = RadioSetting("stun.scc", "Security code", st) + rs.set_apply_callback(apply_scc, stun) + cf.append(rs) + + cf.append( + RadioSetting("settings.save_m", + "Save Mode (2)", + RadioSettingValueList(SAVE_MODES, + SAVE_MODES[s.save_m]))) + for i in range(0, 4): + sm = self._memobj.save[i] + wake = RadioSettingValueInteger(0, 18000, sm.wake * 10, 1) + wf = RadioSetting("save[%i].wake" % i, + "Save Mode %d Wake Time" % (i+1), wake) + wf.set_apply_callback(apply_wake, sm) + cf.append(wf) + + slp = RadioSettingValueInteger(0, 18000, sm.sleep * 10, 1) + wf = RadioSetting("save[%i].sleep" % i, + "Save Mode %d Sleep Time" % (i+1), slp) + wf.set_apply_callback(apply_sleep, sm) + cf.append(wf) + + _msg = str(self._memobj.display.banner).split("\0")[0] + val = RadioSettingValueString(0, 16, _msg) + val.set_mutable(True) + cf.append(RadioSetting("display.banner", + "Display Message", val)) + return cf + + def _fm_tab(self): + """FM Broadcast channels + """ + def apply_fm(setting, obj): + f = freq2short(setting.value, 76000000, 108000000) + obj.fm_freq = f + + fm = RadioSettingGroup("fm_chans", "FM Broadcast") + for ch in range(0, 20): + chan = self._memobj.fm_chans[ch] + freq = RadioSettingValueString(0, 20, + short2freq(chan.fm_freq)) + rs = RadioSetting("fm_%d" % (ch + 1), + "FM Channel %d" % (ch + 1), freq) + rs.set_apply_callback(apply_fm, chan) + fm.append(rs) + return fm + + def _scan_grp(self): + """Scan groups + """ + def apply_name(setting, obj): + name = str2name(setting.value, 8, '\0', '\0') + obj.name = name + + def apply_start(setting, obj): + """Do a callback to deal with RadioSettingInteger limitation + on memory address resolution + """ + obj.scan_st = int(setting.value) + + def apply_end(setting, obj): + """Do a callback to deal with RadioSettingInteger limitation + on memory address resolution + """ + obj.scan_end = int(setting.value) + + sgrp = self._memobj.scn_grps + scan = RadioSettingGroup("scn_grps", "Channel Scanner Groups") + for i in range(0, 10): + s_grp = sgrp.addrs[i] + s_name = sgrp.names[i] + rs_name = RadioSettingValueString(0, 8, + name2str(s_name.name)) + rs = RadioSetting("scn_grps.names[%i].name" % i, + "Group %i Name" % (i + 1), rs_name) + rs.set_apply_callback(apply_name, s_name) + scan.append(rs) + rs_st = RadioSettingValueInteger(1, 999, s_grp.scan_st) + rs = RadioSetting("scn_grps.addrs[%i].scan_st" % i, + "Starting Channel", rs_st) + rs.set_apply_callback(apply_start, s_grp) + scan.append(rs) + rs_end = RadioSettingValueInteger(1, 999, s_grp.scan_end) + rs = RadioSetting("scn_grps.addrs[%i].scan_end" % i, + "Last Channel", rs_end) + rs.set_apply_callback(apply_end, s_grp) + scan.append(rs) + return scan + + def _callid_grp(self): + """Caller IDs to be recognized by radio + This really should be a table in the UI + """ + def apply_callid(setting, obj): + c = str2callid(setting.value) + obj.cid = c + + def apply_name(setting, obj): + name = str2name(setting.value, 6, '\0', '\xff') + obj.name = name + + cid = RadioSettingGroup("callids", "Caller IDs") + for i in range(0, 20): + callid = self._memobj.call_ids[i] + name = self._memobj.cid_names[i] + c_name = RadioSettingValueString(0, 6, name2str(name.name)) + rs = RadioSetting("cid_names[%i].name" % i, + "Caller ID %i Name" % (i + 1), c_name) + rs.set_apply_callback(apply_name, name) + cid.append(rs) + c_id = RadioSettingValueString(0, 6, + callid2str(callid.cid), + False) + rs = RadioSetting("call_ids[%i].cid" % i, + "Caller ID Code", c_id) + rs.set_apply_callback(apply_callid, callid) + cid.append(rs) + return cid + + def _band_tab(self, area, band): + """ Build a band tab inside a VFO/Area + """ + def apply_freq(setting, lo, hi, obj): + f = freq2int(setting.value, lo, hi) + obj.freq = f/10 + + def apply_offset(setting, obj): + f = freq2int(setting.value, 0, 5000000) + obj.offset = f/10 + + def apply_enc(setting, obj): + t = tone2short(setting.value) + obj.encqt = t + + def apply_dec(setting, obj): + t = tone2short(setting.value) + obj.decqt = t + + if area == "a": + if band == 150: + c = self._memobj.vfo_a.band_150 + lo = 108000000 + hi = 180000000 + elif band == 200: + c = self._memobj.vfo_a.band_200 + lo = 230000000 + hi = 250000000 + elif band == 300: + c = self._memobj.vfo_a.band_300 + lo = 350000000 + hi = 400000000 + elif band == 450: + c = self._memobj.vfo_a.band_450 + lo = 400000000 + hi = 512000000 + else: # 700 + c = self._memobj.vfo_a.band_700 + lo = 700000000 + hi = 985000000 + else: # area 'b' + if band == 150: + c = self._memobj.vfo_b.band_150 + lo = 136000000 + hi = 180000000 + else: # 450 + c = self._memobj.vfo_b.band_450 + lo = 400000000 + hi = 512000000 + + prefix = "vfo_%s.band_%d" % (area, band) + bf = RadioSettingGroup(prefix, "%dMHz Band" % band) + freq = RadioSettingValueString(0, 15, int2freq(c.freq * 10)) + rs = RadioSetting(prefix + ".freq", "Rx Frequency", freq) + rs.set_apply_callback(apply_freq, lo, hi, c) + bf.append(rs) + + off = RadioSettingValueString(0, 15, int2freq(c.offset * 10)) + rs = RadioSetting(prefix + ".offset", "Tx Offset(28)", off) + rs.set_apply_callback(apply_offset, c) + bf.append(rs) + + rs = RadioSetting(prefix + ".encqt", + "Encode QT(17,19)", + RadioSettingValueList(TONE_LIST, + short2tone(c.encqt))) + rs.set_apply_callback(apply_enc, c) + bf.append(rs) + + rs = RadioSetting(prefix + ".decqt", + "Decode QT(16,18)", + RadioSettingValueList(TONE_LIST, + short2tone(c.decqt))) + rs.set_apply_callback(apply_dec, c) + bf.append(rs) + + bf.append(RadioSetting(prefix + ".qt", + "Mute Mode(21)", + RadioSettingValueList(SPMUTE_LIST, + SPMUTE_LIST[c.qt]))) + bf.append(RadioSetting(prefix + ".scan", + "Scan this(48)", + RadioSettingValueBoolean(c.scan))) + bf.append(RadioSetting(prefix + ".pwr", + "Power(5)", + RadioSettingValueList( + POWER_LIST, POWER_LIST[c.pwr]))) + bf.append(RadioSetting(prefix + ".mod", + "AM Modulation(54)", + RadioSettingValueBoolean(c.mod))) + bf.append(RadioSetting(prefix + ".fm_dev", + "FM Deviation(4)", + RadioSettingValueList( + BANDWIDTH_LIST, + BANDWIDTH_LIST[c.fm_dev]))) + bf.append( + RadioSetting(prefix + ".shift", + "Frequency Shift(6)", + RadioSettingValueList(OFFSET_LIST, + OFFSET_LIST[c.shift]))) + return bf + + def _area_tab(self, area): + """Build a VFO tab + """ + def apply_scan_st(setting, scan_lo, scan_hi, obj): + f = freq2short(setting.value, scan_lo, scan_hi) + obj.scan_st = f + + def apply_scan_end(setting, scan_lo, scan_hi, obj): + f = freq2short(setting.value, scan_lo, scan_hi) + obj.scan_end = f + + if area == "a": + desc = "Area A Settings" + c = self._memobj.a_conf + scan_lo = 108000000 + scan_hi = 985000000 + scan_rng = self._memobj.settings.a + band_list = (150, 200, 300, 450, 700) + else: + desc = "Area B Settings" + c = self._memobj.b_conf + scan_lo = 136000000 + scan_hi = 512000000 + scan_rng = self._memobj.settings.b + band_list = (150, 450) + + prefix = "%s_conf" % area + af = RadioSettingGroup(prefix, desc) + af.append( + RadioSetting(prefix + ".w_mode", + "Workmode", + RadioSettingValueList( + WORKMODE_LIST, + WORKMODE_LIST[c.w_mode]))) + af.append(RadioSetting(prefix + ".w_chan", + "Channel", + RadioSettingValueInteger(1, 999, + c.w_chan))) + af.append( + RadioSetting(prefix + ".scan_grp", + "Scan Group(49)", + RadioSettingValueList( + SCANGRP_LIST, + SCANGRP_LIST[c.scan_grp]))) + af.append(RadioSetting(prefix + ".bcl", + "Busy Channel Lock-out(15)", + RadioSettingValueBoolean(c.bcl))) + af.append( + RadioSetting(prefix + ".sql", + "Squelch Level(8)", + RadioSettingValueList(LIST_0_9, + LIST_0_9[c.sql]))) + af.append( + RadioSetting(prefix + ".cset", + "Call ID Group(52)", + RadioSettingValueList(LIST_1_20, + LIST_1_20[c.cset]))) + af.append( + RadioSetting(prefix + ".step", + "Frequency Step(3)", + RadioSettingValueList( + STEP_LIST, STEP_LIST[c.step]))) + af.append( + RadioSetting(prefix + ".scan_mode", + "Scan Mode(20)", + RadioSettingValueList( + SCANMODE_LIST, + SCANMODE_LIST[c.scan_mode]))) + af.append( + RadioSetting(prefix + ".scan_range", + "Scan Range(50)", + RadioSettingValueList( + SCANRANGE_LIST, + SCANRANGE_LIST[c.scan_range]))) + st = RadioSettingValueString(0, 15, + short2freq(scan_rng.scan_st)) + rs = RadioSetting("settings.%s.scan_st" % area, + "Frequency Scan Start", st) + rs.set_apply_callback(apply_scan_st, scan_lo, scan_hi, scan_rng) + af.append(rs) + + end = RadioSettingValueString(0, 15, + short2freq(scan_rng.scan_end)) + rs = RadioSetting("settings.%s.scan_end" % area, + "Frequency Scan End", end) + rs.set_apply_callback(apply_scan_end, scan_lo, scan_hi, + scan_rng) + af.append(rs) + # Each area has its own set of bands + for band in (band_list): + af.append(self._band_tab(area, band)) + return af + + def _key_tab(self): + """Build radio key/button menu + """ + s = self._memobj.settings + kf = RadioSettingGroup("key_grp", "Key Settings") + + kf.append(RadioSetting("settings.pf1", + "PF1 Key function(55)", + RadioSettingValueList( + PF1KEY_LIST, + PF1KEY_LIST[s.pf1]))) + kf.append(RadioSetting("settings.pf2", + "PF2 Key function(56)", + RadioSettingValueList( + PF2KEY_LIST, + PF2KEY_LIST[s.pf2]))) + kf.append(RadioSetting("settings.pf3", + "PF3 Key function(57)", + RadioSettingValueList( + PF3KEY_LIST, + PF3KEY_LIST[s.pf3]))) + return kf + + def _get_settings(self): + """Build the radio configuration settings menus + """ + + core_grp = self._core_tab() + fm_grp = self._fm_tab() + area_a_grp = self._area_tab("a") + area_b_grp = self._area_tab("b") + key_grp = self._key_tab() + scan_grp = self._scan_grp() + callid_grp = self._callid_grp() + admin_grp = self._admin_tab() + rpt_grp = self._repeater_tab() + + core_grp.append(key_grp) + core_grp.append(admin_grp) + core_grp.append(rpt_grp) + group = RadioSettings(core_grp, + area_a_grp, + area_b_grp, + fm_grp, + scan_grp, + callid_grp + ) + return group + + def get_settings(self): + """ Public build out linkage between radio settings and UI + """ + try: + return self._get_settings() + except Exception: + import traceback + LOG.error("Failed to parse settings: %s", + traceback.format_exc()) + return None + + def _is_freq(self, element): + """This is a hack to smoke out whether we need to do + frequency translations for otherwise innocent u16s and u32s + """ + return "rxfreq" in element.get_name() or \ + "txfreq" in element.get_name() or \ + "scan_st" in element.get_name() or \ + "scan_end" in element.get_name() or \ + "offset" in element.get_name() or \ + "fm_stop" in element.get_name() + + def set_settings(self, settings): + """ Public update radio settings via UI callback + A lot of this should be in common code.... + """ + + for element in settings: + if not isinstance(element, RadioSetting): + LOG.debug("set_settings: not instance %s" % + element.get_name()) + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + # decode an array index + if "[" in bit and "]" in bit: + bit, index = bit.split("[", 1) + index, junk = index.split("]", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % + (setting, element.value)) + if self._is_freq(element): + setattr(obj, setting, int(element.value)/10) + else: + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug("set_settings: Exception with %s" % + element.get_name()) + raise diff --git a/chirp/drivers/kyd.py b/chirp/drivers/kyd.py new file mode 100644 index 0000000..27ebc74 --- /dev/null +++ b/chirp/drivers/kyd.py @@ -0,0 +1,522 @@ +# Copyright 2014 Jim Unroe +# Copyright 2014 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 unknown1:3, + bcl:2, // Busy Lock + unknown2:3; + u8 unknown3:2, + highpower:1, // Power Level + wide:1, // Bandwidth + unknown4:4; + u8 unknown5[2]; +} memory[16]; + +#seekto 0x012F; +struct { + u8 voice; // Voice Annunciation + u8 tot; // Time-out Timer + u8 totalert; // Time-out Timer Pre-alert + u8 unknown1[2]; + u8 squelch; // Squelch Level + u8 save; // Battery Saver + u8 beep; // Beep + u8 unknown2[3]; + u8 vox; // VOX Gain + u8 voxdelay; // VOX Delay +} settings; + +#seekto 0x017E; +u8 skipflags[2]; // SCAN_ADD +""" + +CMD_ACK = "\x06" + +NC630A_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), + chirp_common.PowerLevel("High", watts=5.00)] + +NC630A_DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +BCL_LIST = ["Off", "Carrier", "QT/DQT"] +TIMEOUTTIMER_LIST = [""] + ["%s seconds" % x for x in range(15, 615, 15)] +TOTALERT_LIST = ["", "Off"] + ["%s seconds" % x for x in range(1, 11)] +VOICE_LIST = ["Off", "Chinese", "English"] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)] +VOXDELAY_LIST = ["0.3", "0.5", "1.0", "1.5", "2.0", "3.0"] + +SETTING_LISTS = { + "bcl": BCL_LIST, + "tot": TIMEOUTTIMER_LIST, + "totalert": TOTALERT_LIST, + "voice": VOICE_LIST, + "vox": VOX_LIST, + "voxdelay": VOXDELAY_LIST, + } + + +def _nc630a_enter_programming_mode(radio): + serial = radio.pipe + + try: + serial.write("PROGRAM") + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if not ack: + raise errors.RadioError("No response from radio") + elif ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + try: + serial.write("\x02") + ident = serial.read(8) + except: + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith(radio._fileid): + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + +def _nc630a_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _nc630a_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _nc630a_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio._block_size): + status.cur = addr + radio._block_size + radio.status_fn(status) + + block = _nc630a_read_block(radio, addr, radio._block_size) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _nc630a_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, radio._block_size): + status.cur = addr + radio._block_size + radio.status_fn(status) + _nc630a_write_block(radio, addr, radio._block_size) + + +class MT700Alias(chirp_common.Alias): + VENDOR = "Plant-Tours" + MODEL = "MT-700" + + +@directory.register +class NC630aRadio(chirp_common.CloneModeRadio): + """KYD NC-630A""" + VENDOR = "KYD" + MODEL = "NC-630A" + ALIASES = [MT700Alias] + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x0330), + ] + _memsize = 0x03C8 + _block_size = 0x08 + _fileid = "P32073" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = False + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = NC630A_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.memory_bounds = (1, 16) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + rf.valid_bands = [(400000000, 520000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + do_upload(self) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + if mem.tmode == "DTCS": + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.tx_tone, rxmode, _mem.rx_tone)) + + def get_memory(self, number): + bitpos = (1 << ((number - 1) % 8)) + bytepos = ((number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[number - 1] + _skp = self._memobj.skipflags[bytepos] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = _mem.wide and "FM" or "NFM" + + self._get_tone(_mem, mem) + + mem.power = NC630A_POWER_LEVELS[_mem.highpower] + + mem.skip = "" if (_skp & bitpos) else "S" + LOG.debug("mem.skip %s" % mem.skip) + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueList( + BCL_LIST, BCL_LIST[_mem.bcl])) + mem.extra.append(rs) + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + tx_tone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = int(mem.ctone * 10) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.tx_tone, rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + bitpos = (1 << ((mem.number - 1) % 8)) + bytepos = ((mem.number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[mem.number - 1] + _skp = self._memobj.skipflags[bytepos] + + if mem.empty: + _mem.set_raw("\xFF" * 16) + return + + _mem.set_raw("\x00" * 14 + "\xFF" * 2) + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.wide = mem.mode == "FM" + + self._set_tone(mem, _mem) + + _mem.highpower = mem.power == NC630A_POWER_LEVELS[1] + + if mem.skip != "S": + _skp |= bitpos + else: + _skp &= ~bitpos + LOG.debug("_skp %s" % _skp) + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("tot", "Time-out timer", + RadioSettingValueList( + TIMEOUTTIMER_LIST, + TIMEOUTTIMER_LIST[_settings.tot])) + basic.append(rs) + + rs = RadioSetting("totalert", "TOT Pre-alert", + RadioSettingValueList( + TOTALERT_LIST, + TOTALERT_LIST[_settings.totalert])) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Gain", + RadioSettingValueList( + VOX_LIST, VOX_LIST[_settings.vox])) + basic.append(rs) + + rs = RadioSetting("voice", "Voice Annumciation", + RadioSettingValueList( + VOICE_LIST, VOICE_LIST[_settings.voice])) + basic.append(rs) + + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("voxdelay", "VOX Delay", + RadioSettingValueList( + VOXDELAY_LIST, + VOXDELAY_LIST[_settings.voxdelay])) + basic.append(rs) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueBoolean(_settings.save)) + basic.append(rs) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = match_model = False + + # testing the file data size + if len(filedata) in [0x338, 0x3C8]: + match_size = True + + # testing model fingerprint + if filedata[0x01B8:0x01BE] == cls._fileid: + match_model = True + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/kyd_IP620.py b/chirp/drivers/kyd_IP620.py new file mode 100644 index 0000000..47507b8 --- /dev/null +++ b/chirp/drivers/kyd_IP620.py @@ -0,0 +1,629 @@ +# Copyright 2015 Lepik.stv +# based on modification of Dan Smith's and Jim Unroe original work +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""KYD IP-620 radios management module""" + +# TODO: Power on message +# TODO: Channel name +# TODO: Tuning step + +import struct +import time +import os +import logging +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +IP620_MEM_FORMAT = """ +#seekto 0x0000; +struct { // Channel memory structure + lbcd rx_freq[4]; // RX frequency + lbcd tx_freq[4]; // TX frequency + ul16 rx_tone; // RX tone + ul16 tx_tone; // TX tone + u8 unknown_1:4 // n-a + busy_loc:2, // NO-00, Crrier wave-01, SM-10 + n_a:2; // n-a + u8 unknown_2:1 // n-a + scan_add:1, // Scan add + n_a:1, // n-a + w_n:1, // Narrow-0 Wide-1 + lout:1, // LOCKOUT OFF-0 ON-1 + n_a_:1, // n-a + power:2; // Power low-00 middle-01 high-10 + u8 unknown_3; // n-a + u8 unknown_4; // n-a +} memory[200]; + +#seekto 0x1000; +struct { + u8 chan_name[6]; //Channel name + u8 unknown_1[10]; +} chan_names[200]; + +#seekto 0x0C80; +struct { // Settings memory structure ( A-Frequency mode ) + lbcd freq_a_rx[4]; + lbcd freq_a_tx[4]; + ul16 freq_a_rx_tone; // RX tone + ul16 freq_a_tx_tone; // TX tone + u8 unknown_1_5:4 + freq_a_busy_loc:2, + n_a:2; + u8 unknown_1_6:3 + freq_a_w_n:1, + n_a:1, + na:1, + freq_a_power:2; + u8 unknown_1_7; + u8 unknown_1_8; +} settings_freq_a; + +#seekto 0x0E20; +struct { + u8 chan_disp_way; // Channel display way + u8 step_freq; // Step frequency KHz + u8 rf_sql; // Squelch level + u8 bat_save; // Battery Saver + u8 chan_pri; // Channel PRI + u8 end_beep; // End beep + u8 tot; // Time-out timer + u8 vox; // VOX Gain + u8 chan_pri_num; // Channel PRI time Sec + u8 n_a_2; + u8 ch_mode; // CH mode + u8 n_a_3; + u8 call_tone; // Call tone + u8 beep; // Beep + u8 unknown_1_1[2]; + u8 unknown_1_2[8]; + u8 scan_rev; // Scan rev + u8 unknown_1_3[2]; + u8 enc; // Frequency lock + u8 vox_dly; // VOX Delay + u8 wait_back_light;// Wait back light + u8 unknown_1_4[2]; +} settings; + +#seekto 0x0E40; +struct { + u8 fm_radio; // FM radio + u8 auto_lock; // Auto lock + u8 unknown_1[8]; + u8 pon_msg[6]; //Power on msg +} settings_misc; + +#seekto 0x1C80; +struct { + u8 unknown_1[16]; + u8 unknown_2[16]; +} settings_radio_3; +""" + +CMD_ACK = "\x06" +WRITE_BLOCK_SIZE = 0x10 +READ_BLOCK_SIZE = 0x40 + +CHAR_LENGTH_MAX = 6 + +OFF_ON_LIST = ["OFF", "ON"] +ON_OFF_LIST = ["ON", "OFF"] +NO_YES_LIST = ["NO", "YES"] +STEP_LIST = ["5.0", "6.25", "10.0", "12.5", "25.0"] +BAT_SAVE_LIST = ["OFF", "0.2 Sec", "0.4 Sec", "0.6 Sec", "0.8 Sec","1.0 Sec"] +SHIFT_LIST = ["", "-", "+"] +SCANM_LIST = ["Time", "Carrier wave", "Search"] +ENDBEEP_LIST = ["OFF", "Begin", "End", "Begin/End"] +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), chirp_common.PowerLevel("Medium", watts=2.50), chirp_common.PowerLevel("High", watts=5.00)] +TIMEOUT_LIST = ["OFF", "1 Min", "3 Min", "10 Min"] +TOTALERT_LIST = ["", "OFF"] + ["%s seconds" % x for x in range(1, 11)] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)] +VOXDELAY_LIST = ["0.3 Sec", "0.5 Sec", "1.0 Sec", "1.5 Sec", "2.0 Sec", "3.0 Sec", "4.0 Sec", "5.0 Sec"] +PRI_NUM = [3, 5, 8, 10] +PRI_NUM_LIST = [str(x) for x in PRI_NUM] +CH_FLAG_LIST = ["Channel+Freq", "Channel+Name"] +BACKLIGHT_LIST = ["Always Off", "Auto", "Always On"] +BUSYLOCK_LIST = ["NO", "Carrier", "SM"] +KEYBLOCK_LIST = ["Manual", "Auto"] +CALLTONE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8", "1750"] +RFSQL_LIST = ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6","S-7", "S-8", "S-FULL"] + +IP620_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ?+-* " + +IP620_BANDS = [ + (136000000, 174000000), + (200000000, 260000000), + (300000000, 340000000), # <--- this band supports only Russian model (ARGUT A-36) + (350000000, 390000000), + (400000000, 480000000), + (420000000, 510000000), + (450000000, 520000000), +] + +@directory.register +class IP620Radio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """KYD IP-620""" + VENDOR = "KYD" + MODEL = "IP-620" + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x2000), + ] + _memsize = 0x2000 + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + filedata[0xF7E:0xF80] == "\x01\xE2" + + def _ip620_exit_programming_mode(self): + try: + self.pipe.write("\x06") + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Radio refused to exit programming mode: %s" % e) + + def _ip620_enter_programming_mode(self): + try: + self.pipe.write("iUHOUXUN") + self.pipe.write("\x02") + time.sleep(0.2) + _ack = self.pipe.read(1) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error communicating with radio: %s" % e) + if not _ack: + raise errors.RadioError("No response from radio") + elif _ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + try: + self.pipe.write("\x02") + _ident = self.pipe.read(8) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error communicating with radio: %s" % e) + if not _ident.startswith("\x06\x4B\x47\x36\x37\x01\x56\xF8"): + print util.hexprint(_ident) + raise errors.RadioError("Radio returned unknown identification string") + try: + self.pipe.write(CMD_ACK) + _ack = self.pipe.read(1) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error communicating with radio: %s" % e) + if _ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + def _ip620_write_block(self, block_addr): + _cmd = struct.pack(">cHb", 'W', block_addr, WRITE_BLOCK_SIZE) + _data = self.get_mmap()[block_addr:block_addr + WRITE_BLOCK_SIZE] + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(_cmd + _data)) + try: + self.pipe.write(_cmd + _data) + if self.pipe.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + def _ip620_read_block(self, block_addr): + _cmd = struct.pack(">cHb", 'R', block_addr, READ_BLOCK_SIZE) + _expectedresponse = "W" + _cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + try: + self.pipe.write(_cmd) + _response = self.pipe.read(4 + READ_BLOCK_SIZE) + if _response[:4] != _expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + _block_data = _response[4:] + self.pipe.write(CMD_ACK) + _ack = self.pipe.read(1) + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + if _ack != CMD_ACK: + raise Exception("No ACK reading block %04x." % (block_addr)) + return _block_data + + def _do_download(self): + self._ip620_enter_programming_mode() + _data = "" + _status = chirp_common.Status() + _status.msg = "Cloning from radio" + _status.cur = 0 + _status.max = self._memsize + for _addr in range(0, self._memsize, READ_BLOCK_SIZE): + _status.cur = _addr + READ_BLOCK_SIZE + self.status_fn(_status) + _block = self._ip620_read_block(_addr) + _data += _block + LOG.debug("Address: %04x" % _addr) + LOG.debug(util.hexprint(_block)) + self._ip620_exit_programming_mode() + return memmap.MemoryMap(_data) + + def _do_upload(self): + _status = chirp_common.Status() + _status.msg = "Uploading to radio" + self._ip620_enter_programming_mode() + _status.cur = 0 + _status.max = self._memsize + for _start_addr, _end_addr in self._ranges: + for _addr in range(_start_addr, _end_addr, WRITE_BLOCK_SIZE): + _status.cur = _addr + WRITE_BLOCK_SIZE + self.status_fn(_status) + self._ip620_write_block(_addr) + self._ip620_exit_programming_mode() + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("This radio driver is currently under development. " + "There are no known issues with it, but you should " + "proceed with caution. However, proceed at your own risk!") + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = False + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = False + rf.has_name = False + rf.valid_skips = [] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = SHIFT_LIST + rf.valid_modes = ["FM", "NFM"] + rf.memory_bounds = (1, 200) + rf.valid_bands = IP620_BANDS + rf.valid_characters = ''.join(set(IP620_CHARSET)) + rf.valid_name_length = CHAR_LENGTH_MAX + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(IP620_MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + self._mmap = self._do_download() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + self._do_upload() + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + if mem.tmode == "DTCS": + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + LOG.debug("Got TX %s (%i) RX %s (%i)" % (txmode, _mem.tx_tone, + rxmode, _mem.rx_tone)) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + _nam = self._memobj.chan_names[number - 1] + + def _is_empty(): + for i in range(0, 4): + if _mem.rx_freq[i].get_raw() != "\xFF": + return False + return True + + mem = chirp_common.Memory() + mem.number = number + + if _is_empty(): + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + if int(_mem.rx_freq) == int(_mem.tx_freq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+" + mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10 + + mem.mode = _mem.w_n and "FM" or "NFM" + self._get_tone(_mem, mem) + mem.power = POWER_LEVELS[_mem.power] + + mem.extra = RadioSettingGroup("Extra", "extra") + rs = RadioSetting("lout", "Lock out", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_mem.lout])) + mem.extra.append(rs) + + rs = RadioSetting("busy_loc", "Busy lock", + RadioSettingValueList(BUSYLOCK_LIST, + BUSYLOCK_LIST[_mem.busy_loc])) + mem.extra.append(rs) + + rs = RadioSetting("scan_add", "Scan add", + RadioSettingValueList(NO_YES_LIST, + NO_YES_LIST[_mem.scan_add])) + mem.extra.append(rs) + #TODO: Show name channel +## count = 0 +## for i in _nam.chan_name: +## if i == 0xFF: +## break +## try: +## mem.name += IP620_CHARSET[i] +## except Exception: +## LOG.error("Unknown name char %i: 0x%02x (mem %i)" % +## (count, i, number - 1)) +## mem.name += " " +## count += 1 +## mem.name = mem.name.rstrip() + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + tx_tone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = int(mem.ctone * 10) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.tx_tone, rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "OFF": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + _mem.w_n = mem.mode == "FM" + self._set_tone(mem, _mem) + _mem.power = mem.power == POWER_LEVELS[1] + + for setting in ('lout', 'busy_loc', 'scan_add'): + setattr(_mem, setting, 0) + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + _settings_misc = self._memobj.settings_misc + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("rf_sql", "Squelch level (SQL)", + RadioSettingValueList(RFSQL_LIST, + RFSQL_LIST[_settings.rf_sql])) + basic.append(rs) + + rs = RadioSetting("step_freq", "Step frequency KHz (STP)", + RadioSettingValueList(STEP_LIST, + STEP_LIST[_settings.step_freq])) + basic.append(rs) + + rs = RadioSetting("fm_radio", "FM radio (DW)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings_misc.fm_radio])) + basic.append(rs) + + rs = RadioSetting("call_tone", "Call tone (CK)", + RadioSettingValueList(CALLTONE_LIST, + CALLTONE_LIST[_settings.call_tone])) + basic.append(rs) + + rs = RadioSetting("tot", "Time-out timer (TOT)", + RadioSettingValueList(TIMEOUT_LIST, + TIMEOUT_LIST[_settings.tot])) + basic.append(rs) + + rs = RadioSetting("chan_disp_way", "Channel display way", + RadioSettingValueList(CH_FLAG_LIST, + CH_FLAG_LIST[_settings.chan_disp_way])) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Gain (VOX)", + RadioSettingValueList(VOX_LIST, + VOX_LIST[_settings.vox])) + basic.append(rs) + + rs = RadioSetting("vox_dly", "VOX Delay", + RadioSettingValueList(VOXDELAY_LIST, + VOXDELAY_LIST[_settings.vox_dly])) + basic.append(rs) + + rs = RadioSetting("beep", "Beep (BP)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings.beep])) + basic.append(rs) + + rs = RadioSetting("auto_lock", "Auto lock (KY)", + RadioSettingValueList(NO_YES_LIST, + NO_YES_LIST[_settings_misc.auto_lock])) + basic.append(rs) + + rs = RadioSetting("bat_save", "Battery Saver (SAV)", + RadioSettingValueList(BAT_SAVE_LIST, + BAT_SAVE_LIST[_settings.bat_save])) + basic.append(rs) + + rs = RadioSetting("chan_pri", "Channel PRI (PRI)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings.chan_pri])) + basic.append(rs) + + rs = RadioSetting("chan_pri_num", "Channel PRI time Sec (PRI)", + RadioSettingValueList(PRI_NUM_LIST, + PRI_NUM_LIST[_settings.chan_pri_num])) + basic.append(rs) + + rs = RadioSetting("end_beep", "End beep (ET)", + RadioSettingValueList(ENDBEEP_LIST, + ENDBEEP_LIST[_settings.end_beep])) + basic.append(rs) + + rs = RadioSetting("ch_mode", "CH mode", + RadioSettingValueList(ON_OFF_LIST, + ON_OFF_LIST[_settings.ch_mode])) + basic.append(rs) + + rs = RadioSetting("scan_rev", "Scan rev (SCAN)", + RadioSettingValueList(SCANM_LIST, + SCANM_LIST[_settings.scan_rev])) + basic.append(rs) + + rs = RadioSetting("enc", "Frequency lock (ENC)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings.enc])) + basic.append(rs) + + rs = RadioSetting("wait_back_light", "Wait back light (LED)", + RadioSettingValueList(BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings.wait_back_light])) + basic.append(rs) + + return top + + def _set_misc_settings(self, settings): + for element in settings: + try: + setattr(self._memobj.settings_misc, + element.get_name(), + element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def set_settings(self, settings): + _settings = self._memobj.settings + _settings_misc = self._memobj.settings_misc + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + if setting in ["auto_lock","fm_radio"]: + oldval = getattr(_settings_misc, setting) + else: + oldval = getattr(_settings, setting) + + newval = element.value + + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + if setting in ["auto_lock","fm_radio"]: + setattr(_settings_misc, setting, newval) + else: + setattr(_settings, setting, newval) + except Exception, e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/leixen.py b/chirp/drivers/leixen.py new file mode 100644 index 0000000..d96c5d0 --- /dev/null +++ b/chirp/drivers/leixen.py @@ -0,0 +1,1038 @@ +# Copyright 2014 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import os +import logging + +from chirp import chirp_common, directory, memmap, errors, util +from chirp import bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0184; +struct { + u8 unknown:4, + sql:4; // squelch level + u8 unknown0x0185; + u8 obeep:1, // open beep + dw_off:1, // dual watch (inverted) + kbeep:1, // key beep + rbeep:1, // roger beep + unknown:2, + ctdcsb:1, // ct/dcs busy lock + unknown:1; + u8 alarm:1, // alarm key + unknown1:1, + aliasen_off:1, // alias enable (inverted) + save:1, // battery save + unknown2:2, + mrcha:1, // mr/cha + vfomr:1; // vfo/mr + u8 keylock_off:1, // key lock (inverted) + txstop_off:1, // tx stop (inverted) + scanm:1, // scan key/mode + vir:1, // vox inhibit on receive + keylockm:2, // key lock mode + lamp:2; // backlight + u8 opendis:2, // open display + fmen_off:1, // fm enable (inverted) + unknown1:1, + fmscan_off:1, // fm scan (inverted) + fmdw:1, // fm dual watch + unknown2:2; + u8 step:4, // step + vol:4; // volume + u8 apo:4, // auto power off + tot:4; // time out timer + u8 unknown0x018C; + u8 voxdt:4, // vox delay time + voxgain:4; // vox gain + u8 unknown0x018E; + u8 unknown0x018F; + u8 unknown:3, + lptime:5; // long press time + u8 keyp2long:4, // p2 key long press + keyp2short:4; // p2 key short press + u8 keyp1long:4, // p1 key long press + keyp1short:4; // p1 key short press + u8 keyp3long:4, // p3 key long press + keyp3short:4; // p3 key short press + u8 unknown0x0194; + u8 menuen:1, // menu enable + absel:1, // a/b select + unknown:2 + keymshort:4; // m key short press + u8 unknown:4, + dtmfst:1, // dtmf sidetone + ackdecode:1, // ack decode + monitor:2; // monitor + u8 unknown1:3, + reset:1, // reset enable + unknown2:1, + keypadmic_off:1, // keypad mic (inverted) + unknown3:2; + u8 unknown0x0198; + u8 unknown1:3, + dtmftime:5; // dtmf digit time + u8 unknown1:3, + dtmfspace:5; // dtmf digit space time + u8 unknown1:2, + dtmfdelay:6; // dtmf first digit delay + u8 unknown1:1, + dtmfpretime:7; // dtmf pretime + u8 unknown1:2, + dtmfdelay2:6; // dtmf * and # digit delay + u8 unknown1:3, + smfont_off:1, // small font (inverted) + unknown:4; +} settings; + +#seekto 0x01cd; +struct { + u8 rssi136; // squelch base level (vhf) + u8 unknown0x01ce; + u8 rssi400; // squelch base level (uhf) +} service; + +#seekto 0x0900; +struct { + char user1[7]; // user message 1 + char unknown0x0907; + char unknown0x0908[8]; + char unknown0x0910[8]; + char system[7]; // system message + char unknown0x091F; + char user2[7]; // user message 2 + char unknown0x0927; +} messages; + +struct channel { + bbcd rx_freq[4]; + bbcd tx_freq[4]; + u8 rx_tone; + u8 rx_tmode_extra:6, + rx_tmode:2; + u8 tx_tone; + u8 tx_tmode_extra:6, + tx_tmode:2; + u8 unknown5; + u8 pttidoff:1, + dtmfoff:1, + %(unknownormode)s, + tailcut:1, + aliasop:1, + talkaroundoff:1, + voxoff:1, + skip:1; + u8 %(modeorpower)s, + reverseoff:1, + blckoff:1, + unknown7:1, + apro:3; + u8 unknown8; +}; + +struct name { + char name[7]; + u8 pad; +}; + +#seekto 0x%(chanstart)x; +struct channel default[%(defaults)i]; +struct channel memory[199]; + +#seekto 0x%(namestart)x; +struct name defaultname[%(defaults)i]; +struct name name[199]; +""" + + +APO_LIST = ["OFF", "10M", "20M", "30M", "40M", "50M", "60M", "90M", + "2H", "4H", "6H", "8H", "10H", "12H", "14H", "16H"] +SQL_LIST = ["%s" % x for x in range(0, 10)] +SCANM_LIST = ["CO", "TO"] +TOT_LIST = ["OFF"] + ["%s seconds" % x for x in range(10, 130, 10)] +_STEP_LIST = [2.5, 5., 6.25, 10., 12.5, 25.] +STEP_LIST = ["{} KHz".format(x) for x in _STEP_LIST] +MONITOR_LIST = ["CTC/DCS", "DTMF", "CTC/DCS and DTMF", "CTC/DCS or DTMF"] +VFOMR_LIST = ["MR", "VFO"] +MRCHA_LIST = ["MR CHA", "Freq. MR"] +VOL_LIST = ["OFF"] + ["%s" % x for x in range(1, 16)] +OPENDIS_LIST = ["All", "Lease Time", "User-defined", "Leixen"] +LAMP_LIST = ["OFF", "KEY", "CONT"] +KEYLOCKM_LIST = ["K+S", "PTT", "KEY", "ALL"] +ABSEL_LIST = ["B Channel", "A Channel"] +VOXGAIN_LIST = ["%s" % x for x in range(1, 9)] +VOXDT_LIST = ["%s seconds" % x for x in range(1, 5)] +DTMFTIME_LIST = ["%i milliseconds" % x for x in range(50, 210, 10)] +DTMFDELAY_LIST = ["%i milliseconds" % x for x in range(0, 550, 50)] +DTMFPRETIME_LIST = ["%i milliseconds" % x for x in range(100, 1100, 100)] +DTMFDELAY2_LIST = ["%i milliseconds" % x for x in range(0, 450, 50)] + +LPTIME_LIST = ["%i milliseconds" % x for x in range(500, 2600, 100)] +PFKEYLONG_LIST = ["OFF", + "FM", + "Monitor Momentary", + "Monitor Lock", + "SQ Off Momentary", + "Mute", + "SCAN", + "TX Power", + "EMG", + "VFO/MR", + "DTMF", + "CALL", + "Transmit 1750Hz", + "A/B", + "Talk Around", + "Reverse" + ] + +PFKEYSHORT_LIST = ["OFF", + "FM", + "BandChange", + "Time", + "Monitor Lock", + "Mute", + "SCAN", + "TX Power", + "EMG", + "VFO/MR", + "DTMF", + "CALL", + "Transmit 1750Hz", + "A/B", + "Talk Around", + "Reverse" + ] + +MODES = ["NFM", "FM"] +WTFTONES = map(float, xrange(56, 64)) +TONES = WTFTONES + chirp_common.TONES +DTCS_CODES = [17, 50, 645] + chirp_common.DTCS_CODES +DTCS_CODES.sort() +TMODES = ["", "Tone", "DTCS", "DTCS"] + + +def _image_ident_from_data(data): + return data[0x168:0x178] + + +def _image_ident_from_image(radio): + return _image_ident_from_data(radio.get_mmap()) + + +def checksum(frame): + x = 0 + for b in frame: + x ^= ord(b) + return chr(x) + + +def make_frame(cmd, addr, data=""): + payload = struct.pack(">H", addr) + data + header = struct.pack(">BB", ord(cmd), len(payload)) + frame = header + payload + return frame + checksum(frame) + + +def send(radio, frame): + # LOG.debug("%04i P>R: %s" % + # (len(frame), + # util.hexprint(frame).replace("\n", "\n "))) + try: + radio.pipe.write(frame) + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + +def recv(radio, readdata=True): + hdr = radio.pipe.read(4) + # LOG.debug("%04i PBBH", hdr) + length -= 2 + if readdata: + data = radio.pipe.read(length) + # LOG.debug(" PTone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 7 + rf.valid_power_levels = self._power_levels + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_skips = ["", "S"] + rf.valid_tuning_steps = _STEP_LIST + rf.valid_bands = [(136000000, 174000000), + (400000000, 470000000)] + rf.memory_bounds = (1, 199) + return rf + + def sync_in(self): + try: + self._mmap = do_download(self) + except Exception, e: + finish(self) + raise errors.RadioError("Failed to download from radio: %s" % e) + self.process_mmap() + + def process_mmap(self): + self._memobj = bitwise.parse( + MEM_FORMAT % self._mem_formatter, self._mmap) + + def sync_out(self): + try: + do_upload(self) + except errors.RadioError: + finish(self) + raise + except Exception, e: + raise errors.RadioError("Failed to upload to radio: %s" % e) + + def get_raw_memory(self, number): + name, mem = self._get_memobjs(number) + return repr(name) + repr(mem) + + def _get_tone(self, mem, _mem): + rx_tone = tx_tone = None + + tx_tmode = TMODES[_mem.tx_tmode] + rx_tmode = TMODES[_mem.rx_tmode] + + if tx_tmode == "Tone": + tx_tone = TONES[_mem.tx_tone - 1] + elif tx_tmode == "DTCS": + tx_tone = DTCS_CODES[_mem.tx_tone - 1] + + if rx_tmode == "Tone": + rx_tone = TONES[_mem.rx_tone - 1] + elif rx_tmode == "DTCS": + rx_tone = DTCS_CODES[_mem.rx_tone - 1] + + tx_pol = _mem.tx_tmode == 0x03 and "R" or "N" + rx_pol = _mem.rx_tmode == 0x03 and "R" or "N" + + chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol), + (rx_tmode, rx_tone, rx_pol)) + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.tx_freq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + def _get_memobjs(self, number): + _mem = self._memobj.memory[number - 1] + _name = self._memobj.name[number - 1] + return _mem, _name + + def get_memory(self, number): + _mem, _name = self._get_memobjs(number) + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[:4] == "\xFF\xFF\xFF\xFF": + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + if self._is_txinh(_mem): + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rx_freq) == int(_mem.tx_freq): + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rx_freq) * 10 - int(_mem.tx_freq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.tx_freq) * 10 + else: + mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+" + mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10 + + mem.name = str(_name.name).rstrip() + + self._get_tone(mem, _mem) + mem.mode = MODES[_mem.mode] + powerindex = _mem.power if _mem.power < len(self._power_levels) else -1 + mem.power = self._power_levels[powerindex] + mem.skip = _mem.skip and "S" or "" + + mem.extra = RadioSettingGroup("Extra", "extra") + + opts = ["On", "Off"] + rs = RadioSetting("blckoff", "Busy Channel Lockout", + RadioSettingValueList( + opts, opts[_mem.blckoff])) + mem.extra.append(rs) + opts = ["Off", "On"] + rs = RadioSetting("tailcut", "Squelch Tail Elimination", + RadioSettingValueList( + opts, opts[_mem.tailcut])) + mem.extra.append(rs) + apro = _mem.apro if _mem.apro < 0x5 else 0 + opts = ["Off", "Compander", "Scrambler", "TX Scrambler", + "RX Scrambler"] + rs = RadioSetting("apro", "Audio Processing", + RadioSettingValueList( + opts, opts[apro])) + mem.extra.append(rs) + opts = ["On", "Off"] + rs = RadioSetting("voxoff", "VOX", + RadioSettingValueList( + opts, opts[_mem.voxoff])) + mem.extra.append(rs) + opts = ["On", "Off"] + rs = RadioSetting("pttidoff", "PTT ID", + RadioSettingValueList( + opts, opts[_mem.pttidoff])) + mem.extra.append(rs) + opts = ["On", "Off"] + rs = RadioSetting("dtmfoff", "DTMF", + RadioSettingValueList( + opts, opts[_mem.dtmfoff])) + mem.extra.append(rs) + opts = ["Name", "Frequency"] + aliasop = RadioSetting("aliasop", "Display", + RadioSettingValueList( + opts, opts[_mem.aliasop])) + mem.extra.append(aliasop) + opts = ["On", "Off"] + rs = RadioSetting("reverseoff", "Reverse Frequency", + RadioSettingValueList( + opts, opts[_mem.reverseoff])) + mem.extra.append(rs) + opts = ["On", "Off"] + rs = RadioSetting("talkaroundoff", "Talk Around", + RadioSettingValueList( + opts, opts[_mem.talkaroundoff])) + mem.extra.append(rs) + + return mem + + def _set_tone(self, mem, _mem): + ((txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) + + _mem.tx_tmode = TMODES.index(txmode) + _mem.rx_tmode = TMODES.index(rxmode) + if txmode == "Tone": + _mem.tx_tone = TONES.index(txtone) + 1 + elif txmode == "DTCS": + _mem.tx_tmode = txpol == "R" and 0x03 or 0x02 + _mem.tx_tone = DTCS_CODES.index(txtone) + 1 + if rxmode == "Tone": + _mem.rx_tone = TONES.index(rxtone) + 1 + elif rxmode == "DTCS": + _mem.rx_tmode = rxpol == "R" and 0x03 or 0x02 + _mem.rx_tone = DTCS_CODES.index(rxtone) + 1 + + def set_memory(self, mem): + _mem, _name = self._get_memobjs(mem.number) + + if mem.empty: + _mem.set_raw("\xFF" * 16) + return + elif _mem.get_raw() == ("\xFF" * 16): + _mem.set_raw("\xFF" * 8 + "\xFF\x00\xFF\x00\xFF\xFE\xF0\xFC") + + _mem.rx_freq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.tx_freq = mem.offset / 10 + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + self._set_tone(mem, _mem) + + _mem.power = mem.power and self._power_levels.index(mem.power) or 0 + _mem.mode = MODES.index(mem.mode) + _mem.skip = mem.skip == "S" + _name.name = mem.name.ljust(7) + + # autoset display to name if filled, else show frequency + if mem.extra: + # mem.extra only seems to be populated when called from edit panel + aliasop = mem.extra["aliasop"] + else: + aliasop = None + if mem.name: + _mem.aliasop = False + if aliasop and not aliasop.changed(): + aliasop.value = "Name" + else: + _mem.aliasop = True + if aliasop and not aliasop.changed(): + aliasop.value = "Frequency" + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def _get_settings(self): + _settings = self._memobj.settings + _service = self._memobj.service + _msg = self._memobj.messages + cfg_grp = RadioSettingGroup("cfg_grp", "Basic Settings") + adv_grp = RadioSettingGroup("adv_grp", "Advanced Settings") + key_grp = RadioSettingGroup("key_grp", "Key Assignment") + group = RadioSettings(cfg_grp, adv_grp, key_grp) + + # + # Basic Settings + # + rs = RadioSetting("apo", "Auto Power Off", + RadioSettingValueList( + APO_LIST, APO_LIST[_settings.apo])) + cfg_grp.append(rs) + rs = RadioSetting("sql", "Squelch Level", + RadioSettingValueList( + SQL_LIST, SQL_LIST[_settings.sql])) + cfg_grp.append(rs) + rs = RadioSetting("scanm", "Scan Mode", + RadioSettingValueList( + SCANM_LIST, SCANM_LIST[_settings.scanm])) + cfg_grp.append(rs) + rs = RadioSetting("tot", "Time Out Timer", + RadioSettingValueList( + TOT_LIST, TOT_LIST[_settings.tot])) + cfg_grp.append(rs) + rs = RadioSetting("step", "Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_settings.step])) + cfg_grp.append(rs) + rs = RadioSetting("monitor", "Monitor", + RadioSettingValueList( + MONITOR_LIST, MONITOR_LIST[_settings.monitor])) + cfg_grp.append(rs) + rs = RadioSetting("vfomr", "VFO/MR", + RadioSettingValueList( + VFOMR_LIST, VFOMR_LIST[_settings.vfomr])) + cfg_grp.append(rs) + rs = RadioSetting("mrcha", "MR/CHA", + RadioSettingValueList( + MRCHA_LIST, MRCHA_LIST[_settings.mrcha])) + cfg_grp.append(rs) + rs = RadioSetting("vol", "Volume", + RadioSettingValueList( + VOL_LIST, VOL_LIST[_settings.vol])) + cfg_grp.append(rs) + rs = RadioSetting("opendis", "Open Display", + RadioSettingValueList( + OPENDIS_LIST, OPENDIS_LIST[_settings.opendis])) + cfg_grp.append(rs) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + LOG.debug("Filtered: %s" % filtered) + return filtered + + rs = RadioSetting("messages.user1", "User-defined Message 1", + RadioSettingValueString(0, 7, _filter(_msg.user1))) + cfg_grp.append(rs) + rs = RadioSetting("messages.user2", "User-defined Message 2", + RadioSettingValueString(0, 7, _filter(_msg.user2))) + cfg_grp.append(rs) + + val = RadioSettingValueString(0, 7, _filter(_msg.system)) + val.set_mutable(False) + rs = RadioSetting("messages.system", "System Message", val) + cfg_grp.append(rs) + + rs = RadioSetting("lamp", "Backlight", + RadioSettingValueList( + LAMP_LIST, LAMP_LIST[_settings.lamp])) + cfg_grp.append(rs) + rs = RadioSetting("keylockm", "Key Lock Mode", + RadioSettingValueList( + KEYLOCKM_LIST, + KEYLOCKM_LIST[_settings.keylockm])) + cfg_grp.append(rs) + rs = RadioSetting("absel", "A/B Select", + RadioSettingValueList(ABSEL_LIST, + ABSEL_LIST[_settings.absel])) + cfg_grp.append(rs) + + rs = RadioSetting("obeep", "Open Beep", + RadioSettingValueBoolean(_settings.obeep)) + cfg_grp.append(rs) + rs = RadioSetting("rbeep", "Roger Beep", + RadioSettingValueBoolean(_settings.rbeep)) + cfg_grp.append(rs) + rs = RadioSetting("keylock_off", "Key Lock", + RadioSettingValueBoolean(not _settings.keylock_off)) + cfg_grp.append(rs) + rs = RadioSetting("ctdcsb", "CT/DCS Busy Lock", + RadioSettingValueBoolean(_settings.ctdcsb)) + cfg_grp.append(rs) + rs = RadioSetting("alarm", "Alarm Key", + RadioSettingValueBoolean(_settings.alarm)) + cfg_grp.append(rs) + rs = RadioSetting("save", "Battery Save", + RadioSettingValueBoolean(_settings.save)) + cfg_grp.append(rs) + rs = RadioSetting("kbeep", "Key Beep", + RadioSettingValueBoolean(_settings.kbeep)) + cfg_grp.append(rs) + rs = RadioSetting("reset", "Reset Enable", + RadioSettingValueBoolean(_settings.reset)) + cfg_grp.append(rs) + rs = RadioSetting("smfont_off", "Small Font", + RadioSettingValueBoolean(not _settings.smfont_off)) + cfg_grp.append(rs) + rs = RadioSetting("aliasen_off", "Alias Enable", + RadioSettingValueBoolean(not _settings.aliasen_off)) + cfg_grp.append(rs) + rs = RadioSetting("txstop_off", "TX Stop", + RadioSettingValueBoolean(not _settings.txstop_off)) + cfg_grp.append(rs) + rs = RadioSetting("dw_off", "Dual Watch", + RadioSettingValueBoolean(not _settings.dw_off)) + cfg_grp.append(rs) + rs = RadioSetting("fmen_off", "FM Enable", + RadioSettingValueBoolean(not _settings.fmen_off)) + cfg_grp.append(rs) + rs = RadioSetting("fmdw", "FM Dual Watch", + RadioSettingValueBoolean(_settings.fmdw)) + cfg_grp.append(rs) + rs = RadioSetting("fmscan_off", "FM Scan", + RadioSettingValueBoolean( + not _settings.fmscan_off)) + cfg_grp.append(rs) + rs = RadioSetting("keypadmic_off", "Keypad MIC", + RadioSettingValueBoolean( + not _settings.keypadmic_off)) + cfg_grp.append(rs) + rs = RadioSetting("voxgain", "VOX Gain", + RadioSettingValueList( + VOXGAIN_LIST, VOXGAIN_LIST[_settings.voxgain])) + cfg_grp.append(rs) + rs = RadioSetting("voxdt", "VOX Delay Time", + RadioSettingValueList( + VOXDT_LIST, VOXDT_LIST[_settings.voxdt])) + cfg_grp.append(rs) + rs = RadioSetting("vir", "VOX Inhibit on Receive", + RadioSettingValueBoolean(_settings.vir)) + cfg_grp.append(rs) + + # + # Advanced Settings + # + val = (_settings.dtmftime) - 5 + rs = RadioSetting("dtmftime", "DTMF Digit Time", + RadioSettingValueList( + DTMFTIME_LIST, DTMFTIME_LIST[val])) + adv_grp.append(rs) + val = (_settings.dtmfspace) - 5 + rs = RadioSetting("dtmfspace", "DTMF Digit Space Time", + RadioSettingValueList( + DTMFTIME_LIST, DTMFTIME_LIST[val])) + adv_grp.append(rs) + val = (_settings.dtmfdelay) / 5 + rs = RadioSetting("dtmfdelay", "DTMF 1st Digit Delay", + RadioSettingValueList( + DTMFDELAY_LIST, DTMFDELAY_LIST[val])) + adv_grp.append(rs) + val = (_settings.dtmfpretime) / 10 - 1 + rs = RadioSetting("dtmfpretime", "DTMF Pretime", + RadioSettingValueList( + DTMFPRETIME_LIST, DTMFPRETIME_LIST[val])) + adv_grp.append(rs) + val = (_settings.dtmfdelay2) / 5 + rs = RadioSetting("dtmfdelay2", "DTMF * and # Digit Delay", + RadioSettingValueList( + DTMFDELAY2_LIST, DTMFDELAY2_LIST[val])) + adv_grp.append(rs) + rs = RadioSetting("ackdecode", "ACK Decode", + RadioSettingValueBoolean(_settings.ackdecode)) + adv_grp.append(rs) + rs = RadioSetting("dtmfst", "DTMF Sidetone", + RadioSettingValueBoolean(_settings.dtmfst)) + adv_grp.append(rs) + + rs = RadioSetting("service.rssi400", "Squelch Base Level (UHF)", + RadioSettingValueInteger(0, 255, _service.rssi400)) + adv_grp.append(rs) + rs = RadioSetting("service.rssi136", "Squelch Base Level (VHF)", + RadioSettingValueInteger(0, 255, _service.rssi136)) + adv_grp.append(rs) + + # + # Key Settings + # + val = (_settings.lptime) - 5 + rs = RadioSetting("lptime", "Long Press Time", + RadioSettingValueList( + LPTIME_LIST, LPTIME_LIST[val])) + key_grp.append(rs) + rs = RadioSetting("keyp1long", "P1 Long Key", + RadioSettingValueList( + PFKEYLONG_LIST, + PFKEYLONG_LIST[_settings.keyp1long])) + key_grp.append(rs) + rs = RadioSetting("keyp1short", "P1 Short Key", + RadioSettingValueList( + PFKEYSHORT_LIST, + PFKEYSHORT_LIST[_settings.keyp1short])) + key_grp.append(rs) + rs = RadioSetting("keyp2long", "P2 Long Key", + RadioSettingValueList( + PFKEYLONG_LIST, + PFKEYLONG_LIST[_settings.keyp2long])) + key_grp.append(rs) + rs = RadioSetting("keyp2short", "P2 Short Key", + RadioSettingValueList( + PFKEYSHORT_LIST, + PFKEYSHORT_LIST[_settings.keyp2short])) + key_grp.append(rs) + rs = RadioSetting("keyp3long", "P3 Long Key", + RadioSettingValueList( + PFKEYLONG_LIST, + PFKEYLONG_LIST[_settings.keyp3long])) + key_grp.append(rs) + rs = RadioSetting("keyp3short", "P3 Short Key", + RadioSettingValueList( + PFKEYSHORT_LIST, + PFKEYSHORT_LIST[_settings.keyp3short])) + key_grp.append(rs) + + val = RadioSettingValueList(PFKEYSHORT_LIST, + PFKEYSHORT_LIST[_settings.keymshort]) + val.set_mutable(_settings.menuen == 0) + rs = RadioSetting("keymshort", "M Short Key", val) + key_grp.append(rs) + val = RadioSettingValueBoolean(_settings.menuen) + rs = RadioSetting("menuen", "Menu Enable", val) + key_grp.append(rs) + + return group + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "keylock_off": + setattr(obj, setting, not int(element.value)) + elif setting == "smfont_off": + setattr(obj, setting, not int(element.value)) + elif setting == "aliasen_off": + setattr(obj, setting, not int(element.value)) + elif setting == "txstop_off": + setattr(obj, setting, not int(element.value)) + elif setting == "dw_off": + setattr(obj, setting, not int(element.value)) + elif setting == "fmen_off": + setattr(obj, setting, not int(element.value)) + elif setting == "fmscan_off": + setattr(obj, setting, not int(element.value)) + elif setting == "keypadmic_off": + setattr(obj, setting, not int(element.value)) + elif setting == "dtmftime": + setattr(obj, setting, int(element.value) + 5) + elif setting == "dtmfspace": + setattr(obj, setting, int(element.value) + 5) + elif setting == "dtmfdelay": + setattr(obj, setting, int(element.value) * 5) + elif setting == "dtmfpretime": + setattr(obj, setting, (int(element.value) + 1) * 10) + elif setting == "dtmfdelay2": + setattr(obj, setting, int(element.value) * 5) + elif setting == "lptime": + setattr(obj, setting, int(element.value) + 5) + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + if filedata[0x168:0x170].startswith(cls._file_ident) and \ + filedata[0x170:0x178].startswith(cls._model_ident): + return True + else: + return False + + +@directory.register +class JetstreamJT270MRadio(LeixenVV898Radio): + + """Jetstream JT270M""" + VENDOR = "Jetstream" + MODEL = "JT270M" + + _file_ident = "JET" + _model_ident = 'LX-\x89\x85\x53' + + +@directory.register +class JetstreamJT270MHRadio(LeixenVV898Radio): + + """Jetstream JT270MH""" + VENDOR = "Jetstream" + MODEL = "JT270MH" + + _file_ident = "Leixen" + _model_ident = 'LX-\x89\x85\x85' + _ranges = [(0x0C00, 0x2000)] + _mem_formatter = {'unknownormode': 'mode:1', + 'modeorpower': 'power:2', + 'chanstart': 0x0C00, + 'namestart': 0x1900, + 'defaults': 6} + _power_levels = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=10), + chirp_common.PowerLevel("High", watts=25)] + + def get_features(self): + rf = super(JetstreamJT270MHRadio, self).get_features() + rf.has_sub_devices = self.VARIANT == '' + rf.memory_bounds = (1, 99) + return rf + + def get_sub_devices(self): + return [JetstreamJT270MHRadioA(self._mmap), + JetstreamJT270MHRadioB(self._mmap)] + + def _get_memobjs(self, number): + number = number * 2 - self._offset + _mem = self._memobj.memory[number] + _name = self._memobj.name[number] + return _mem, _name + + +class JetstreamJT270MHRadioA(JetstreamJT270MHRadio): + VARIANT = 'A Band' + _offset = 1 + + +class JetstreamJT270MHRadioB(JetstreamJT270MHRadio): + VARIANT = 'B Band' + _offset = 2 + + +class VV898E(chirp_common.Alias): + + '''Leixen has called this radio both 898E and S historically, ident is + identical''' + VENDOR = "Leixen" + MODEL = "VV-898E" + + +@directory.register +class LeixenVV898SRadio(LeixenVV898Radio): + + """Leixen VV-898S, also VV-898E which is identical""" + VENDOR = "Leixen" + MODEL = "VV-898S" + ALIASES = [VV898E, ] + + _model_ident = 'LX-\x89\x85\x75' + _mem_formatter = {'unknownormode': 'mode:1', + 'modeorpower': 'power:2', + 'chanstart': 0x0D00, + 'namestart': 0x19B0, + 'defaults': 3} + _power_levels = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Med", watts=10), + chirp_common.PowerLevel("High", watts=25)] diff --git a/chirp/drivers/lt725uv.py b/chirp/drivers/lt725uv.py new file mode 100644 index 0000000..a21a9c5 --- /dev/null +++ b/chirp/drivers/lt725uv.py @@ -0,0 +1,1446 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), # +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings,InvalidValueError +from textwrap import dedent + +MEM_FORMAT = """ +#seekto 0x0200; +struct { + u8 init_bank; + u8 volume; + u16 fm_freq; + u8 wtled; + u8 rxled; + u8 txled; + u8 ledsw; + u8 beep; + u8 ring; + u8 bcl; + u8 tot; + u16 sig_freq; + u16 dtmf_txms; + u8 init_sql; + u8 rptr_mode; +} settings; + +#seekto 0x0240; +struct { + u8 dtmf1_cnt; + u8 dtmf1[7]; + u8 dtmf2_cnt; + u8 dtmf2[7]; + u8 dtmf3_cnt; + u8 dtmf3[7]; + u8 dtmf4_cnt; + u8 dtmf4[7]; + u8 dtmf5_cnt; + u8 dtmf5[7]; + u8 dtmf6_cnt; + u8 dtmf6[7]; + u8 dtmf7_cnt; + u8 dtmf7[7]; + u8 dtmf8_cnt; + u8 dtmf8[7]; +} dtmf_tab; + +#seekto 0x0280; +struct { + u8 native_id_cnt; + u8 native_id_code[7]; + u8 master_id_cnt; + u8 master_id_code[7]; + u8 alarm_cnt; + u8 alarm_code[5]; + u8 id_disp_cnt; + u8 id_disp_code[5]; + u8 revive_cnt; + u8 revive_code[5]; + u8 stun_cnt; + u8 stun_code[5]; + u8 kill_cnt; + u8 kill_code[5]; + u8 monitor_cnt; + u8 monitor_code[5]; + u8 state_now; +} codes; + +#seekto 0x02d0; +struct { + u8 hello1_cnt; + char hello1[7]; + u8 hello2_cnt; + char hello2[7]; + u32 vhf_low; + u32 vhf_high; + u32 uhf_low; + u32 uhf_high; + u8 lims_on; +} hello_lims; + +struct vfo { + u8 frq_chn_mode; + u8 chan_num; + u32 rxfreq; + u16 is_rxdigtone:1, + rxdtcs_pol:1, + rx_tone:14; + u8 rx_mode; + u8 unknown_ff; + u16 is_txdigtone:1, + txdtcs_pol:1, + tx_tone:14; + u8 launch_sig; + u8 tx_end_sig; + u8 bpower; + u8 fm_bw; + u8 cmp_nder; + u8 scrm_blr; + u8 shift; + u32 offset; + u16 step; + u8 sql; +}; + +#seekto 0x0300; +struct { + struct vfo vfoa; +} upper; + +#seekto 0x0380; +struct { + struct vfo vfob; +} lower; + +struct mem { + u32 rxfreq; + u16 is_rxdigtone:1, + rxdtcs_pol:1, + rxtone:14; + u8 recvmode; + u32 txfreq; + u16 is_txdigtone:1, + txdtcs_pol:1, + txtone:14; + u8 botsignal; + u8 eotsignal; + u8 power:1, + wide:1, + compandor:1 + scrambler:1 + unknown:4; + u8 namelen; + u8 name[7]; +}; + +#seekto 0x0400; +struct mem upper_memory[128]; + +#seekto 0x1000; +struct mem lower_memory[128]; + +#seekto 0x1C00; +struct { + char mod_num[6]; +} mod_id; +""" + +MEM_SIZE = 0x1C00 +BLOCK_SIZE = 0x40 +STIMEOUT = 2 +# Channel power: 2 levels +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("High", watts=30.00)] + +LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"] +LIST_SIGNAL = ["Off"] + ["DTMF%s" % x for x in range(1, 9)] + \ + ["DTMF%s + Identity" % x for x in range(1, 9)] + \ + ["Identity code"] +# Band Power settings, can be different than channel power +LIST_BPOWER = ["Low", "Mid", "High"] # Tri-power models +LIST_COLOR = ["Off", "Orange", "Blue", "Purple"] +LIST_LEDSW = ["Auto", "On"] +LIST_RING = ["Off"] + ["%s" % x for x in range(1, 10)] +LIST_TDR_DEF = ["A-Upper", "B-Lower"] +LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 630, 30)] +LIST_VFOMODE = ["Frequency Mode", "Channel Mode"] +# Tones are numeric, Defined in \chirp\chirp_common.py +TONES_CTCSS = sorted(chirp_common.TONES) +# Converted to strings +LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS] +# Now append the DxxxN and DxxxI DTCS codes from chirp_common +for x in chirp_common.DTCS_CODES: + LIST_CTCSS.append("D{:03d}N".format(x)) +for x in chirp_common.DTCS_CODES: + LIST_CTCSS.append("D{:03d}R".format(x)) +LIST_BW = ["Narrow", "Wide"] +LIST_SHIFT = ["Off"," + ", " - "] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] +LIST_STEPS = [str(x) for x in STEPS] +LIST_STATE = ["Normal", "Stun", "Kill"] +LIST_SSF = ["1000", "1450", "1750", "2100"] +LIST_DTMFTX = ["50", "100", "150", "200", "300","500"] + +SETTING_LISTS = { +"init_bank": LIST_TDR_DEF , +"tot": LIST_TIMEOUT, +"wtled": LIST_COLOR, +"rxled": LIST_COLOR, +"txled": LIST_COLOR, +"sig_freq": LIST_SSF, +"dtmf_txms": LIST_DTMFTX, +"ledsw": LIST_LEDSW, +"frq_chn_mode": LIST_VFOMODE, +"rx_tone": LIST_CTCSS, +"tx_tone": LIST_CTCSS, +"rx_mode": LIST_RECVMODE, +"launch_sig": LIST_SIGNAL, +"tx_end_sig": LIST_SIGNAL, +"bpower":LIST_BPOWER, +"fm_bw": LIST_BW, +"shift": LIST_SHIFT, +"step": LIST_STEPS, +"ring": LIST_RING, +"state_now": LIST_STATE +} + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + Log.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + _exit_program_mode(radio) + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + _exit_program_mode(radio) + msg = "Error reading from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">4sHH", cmd, addr, length) + # Add the data if set + if len(data) != 0: + frame += data + # Return the data + return frame + + +def _recv(radio, addr, length): + """Get data from the radio """ + + data = _rawrecv(radio, length) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(data)) + + return data + + +def _do_ident(radio): + """Put the radio in PROGRAM mode & identify it""" + # Set the serial discipline + radio.pipe.baudrate = 19200 + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # Flush input buffer + _clean_buffer(radio) + + magic = "PROM_LIN" + + _rawsend(radio, magic) + + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond") + + return True + + +def _exit_program_mode(radio): + endframe = "EXIT" + _rawsend(radio, endframe) + + +def _download(radio): + """Get the memory map""" + + # Put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + frame = _make_frame("READ", addr, BLOCK_SIZE) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # Sending the read request + _rawsend(radio, frame) + + # Now we read + d = _recv(radio, addr, BLOCK_SIZE) + + # Aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + data += radio.MODEL.ljust(8) + + return data + + +def _upload(radio): + """Upload procedure""" + + # Put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # The fun starts here + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # Sending the data + data = radio.get_mmap()[addr:addr + BLOCK_SIZE] + + frame = _make_frame("WRIE", addr, BLOCK_SIZE, data) + + _rawsend(radio, frame) + + # Receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + if len(data) == 0x1C08: + rid = data[0x1C00:0x1C08] + return rid.startswith(cls.MODEL) + else: + return False + + +def _split(rf, f1, f2): + """Returns False if the two freqs are in the same band (no split) + or True otherwise""" + + # Determine if the two freqs are in the same band + for low, high in rf.valid_bands: + if f1 >= low and f1 <= high and \ + f2 >= low and f2 <= high: + # If the two freqs are on the same Band this is not a split + return False + + # If you get here is because the freq pairs are split + return True + + +@directory.register +class LT725UV(chirp_common.CloneModeRadio): + """LUITON LT-725UV Radio""" + VENDOR = "LUITON" + MODEL = "LT-725UV" + MODES = ["NFM", "FM"] + TONES = chirp_common.TONES + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + NAME_LENGTH = 7 + DTMF_CHARS = list("0123456789ABCD*#") + + VALID_BANDS = [(136000000, 176000000), + (400000000, 480000000)] + + # Valid chars on the LCD + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = \ + ('Some notes about POWER settings:\n' + '- The individual channel power settings are ignored' + ' by the radio.\n' + ' They are allowed to be set (and downloaded) in hopes of' + ' a future firmware update.\n' + '- Power settings done \'Live\' in the radio apply to the' + ' entire upper or lower band.\n' + '- Tri-power radio models will set and download the three' + ' band-power' + ' levels, but they are converted to just Low and High at' + ' upload.' + ' The Mid setting reverts to Low.' + ) + + rp.pre_download = _(dedent("""\ + Follow this instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.has_sub_devices = self.VARIANT == "" + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = [] + rf.valid_power_levels = POWER_LEVELS + rf.valid_name_length = self.NAME_LENGTH + rf.valid_dtcs_codes = self.DTCS_CODES + rf.valid_bands = self.VALID_BANDS + rf.memory_bounds = (1, 128) + rf.valid_tuning_steps = STEPS + return rf + + def get_sub_devices(self): + return [LT725UVUpper(self._mmap), LT725UVLower(self._mmap)] + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + _upload(self) + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _memory_obj(self, suffix=""): + return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix)) + + def _get_dcs(self, val): + return int(str(val)[2:-18]) + + def _set_dcs(self, val): + return int(str(val), 16) + + def get_memory(self, number): + _mem = self._memory_obj()[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10): + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _mem.name[:_mem.namelen]: + mem.name += chr(char) + + dtcs_pol = ["N", "N"] + + if _mem.rxtone == 0x3FFF: + rxmode = "" + elif _mem.is_rxdigtone == 0: + # CTCSS + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + else: + # Digital + rxmode = "DTCS" + mem.rx_dtcs = self._get_dcs(_mem.rxtone) + if _mem.rxdtcs_pol == 1: + dtcs_pol[1] = "R" + + if _mem.txtone == 0x3FFF: + txmode = "" + elif _mem.is_txdigtone == 0: + # CTCSS + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + else: + # Digital + txmode = "DTCS" + mem.dtcs = self._get_dcs(_mem.txtone) + if _mem.txdtcs_pol == 1: + dtcs_pol[0] = "R" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.power = POWER_LEVELS[_mem.power] + + # Extra + mem.extra = RadioSettingGroup("extra", "Extra") + + if _mem.recvmode == 0xFF: + val = 0x00 + else: + val = _mem.recvmode + recvmode = RadioSetting("recvmode", "Receiving mode", + RadioSettingValueList(LIST_RECVMODE, + LIST_RECVMODE[val])) + mem.extra.append(recvmode) + + if _mem.botsignal == 0xFF: + val = 0x00 + else: + val = _mem.botsignal + botsignal = RadioSetting("botsignal", "Launch signaling", + RadioSettingValueList(LIST_SIGNAL, + LIST_SIGNAL[val])) + mem.extra.append(botsignal) + + if _mem.eotsignal == 0xFF: + val = 0x00 + else: + val = _mem.eotsignal + + rx = RadioSettingValueList(LIST_SIGNAL, LIST_SIGNAL[val]) + eotsignal = RadioSetting("eotsignal", "Transmit end signaling", rx) + mem.extra.append(eotsignal) + + rx = RadioSettingValueBoolean(bool(_mem.compandor)) + compandor = RadioSetting("compandor", "Compandor", rx) + mem.extra.append(compandor) + + rx = RadioSettingValueBoolean(bool(_mem.scrambler)) + scrambler = RadioSetting("scrambler", "Scrambler", rx) + mem.extra.append(scrambler) + + return mem + + def set_memory(self, mem): + _mem = self._memory_obj()[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xff" * 24) + _mem.namelen = 0 + return + + _mem.set_raw("\xFF" * 15 + "\x00\x00" + "\xFF" * 7) + + _mem.rxfreq = mem.freq / 10 + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.namelen = len(mem.name) + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _mem.name[i] = ord(mem.name[i]) + except IndexError: + _mem.name[i] = 0xFF + + rxmode = "" + txmode = "" + + if mem.tmode == "Tone": + txmode = "Tone" + elif mem.tmode == "TSQL": + rxmode = "Tone" + txmode = "TSQL" + elif mem.tmode == "DTCS": + rxmode = "DTCSSQL" + txmode = "DTCS" + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + + if rxmode == "": + _mem.rxdtcs_pol = 1 + _mem.is_rxdigtone = 1 + _mem.rxtone = 0x3FFF + elif rxmode == "Tone": + _mem.rxdtcs_pol = 0 + _mem.is_rxdigtone = 0 + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCSSQL": + _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 + _mem.is_rxdigtone = 1 + _mem.rxtone = self._set_dcs(mem.dtcs) + elif rxmode == "DTCS": + _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 + _mem.is_rxdigtone = 1 + _mem.rxtone = self._set_dcs(mem.rx_dtcs) + + if txmode == "": + _mem.txdtcs_pol = 1 + _mem.is_txdigtone = 1 + _mem.txtone = 0x3FFF + elif txmode == "Tone": + _mem.txdtcs_pol = 0 + _mem.is_txdigtone = 0 + _mem.txtone = int(mem.rtone * 10) + elif txmode == "TSQL": + _mem.txdtcs_pol = 0 + _mem.is_txdigtone = 0 + _mem.txtone = int(mem.ctone * 10) + elif txmode == "DTCS": + _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0 + _mem.is_txdigtone = 1 + _mem.txtone = self._set_dcs(mem.dtcs) + + _mem.wide = self.MODES.index(mem.mode) + _mem.power = mem.power == POWER_LEVELS[1] + + # Extra settings + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + # Define mem struct write-back shortcuts + _sets = self._memobj.settings + _vfoa = self._memobj.upper.vfoa + _vfob = self._memobj.lower.vfob + _lims = self._memobj.hello_lims + _codes = self._memobj.codes + _dtmf = self._memobj.dtmf_tab + + basic = RadioSettingGroup("basic", "Basic Settings") + a_band = RadioSettingGroup("a_band", "VFO A-Upper Settings") + b_band = RadioSettingGroup("b_band", "VFO B-Lower Settings") + codes = RadioSettingGroup("codes", "Codes & DTMF Groups") + lims = RadioSettingGroup("lims", "PowerOn & Freq Limits") + group = RadioSettings(basic, a_band, b_band, lims, codes) + + # Basic Settings + bnd_mode = RadioSetting("settings.init_bank", "TDR Band Default", + RadioSettingValueList(LIST_TDR_DEF, + LIST_TDR_DEF[ _sets.init_bank])) + basic.append(bnd_mode) + + volume = RadioSetting("settings.volume", "Volume", + RadioSettingValueInteger(0, 20, _sets.volume)) + basic.append(volume) + + val = _vfoa.bpower # 2bits values 0,1,2= Low, Mid, High + rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val]) + powera = RadioSetting("upper.vfoa.bpower", "Power (Upper)", rx) + basic.append(powera) + + val = _vfob.bpower + rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val]) + powerb = RadioSetting("lower.vfob.bpower", "Power (Lower)", rx) + basic.append(powerb) + + def my_word2raw(setting, obj, atrb, mlt=10): + """Callback function to convert UI floating value to u16 int""" + if str(setting.value) == "Off": + frq = 0x0FFFF + else: + frq = int(float(str(setting.value)) * float(mlt)) + if frq == 0: + frq = 0xFFFF + setattr(obj, atrb, frq) + return + + def my_adjraw(setting, obj, atrb, fix): + """Callback: add or subtract fix from value.""" + vx = int(str(setting.value)) + value = vx + int(fix) + if value < 0: + value = 0 + if atrb == "frq_chn_mode" and int(str(setting.value)) == 2: + value = vx * 2 # Special handling for frq_chn_mode + setattr(obj, atrb, value) + return + + def my_dbl2raw(setting, obj, atrb, flg=1): + """Callback: convert from freq 146.7600 to 14760000 U32.""" + value = chirp_common.parse_freq(str(setting.value)) / 10 + # flg=1 means 0 becomes ff, else leave as possible 0 + if flg == 1 and value == 0: + value = 0xFFFFFFFF + setattr(obj, atrb, value) + return + + def my_val_list(setting, obj, atrb): + """Callback:from ValueList with non-sequential, actual values.""" + value = int(str(setting.value)) # Get the integer value + if atrb == "tot": + value = int(value / 30) # 30 second increments + setattr(obj, atrb, value) + return + + def my_spcl(setting, obj, atrb): + """Callback: Special handling based on atrb.""" + if atrb == "frq_chn_mode": + idx = LIST_VFOMODE.index (str(setting.value)) # Returns 0 or 1 + value = idx * 2 # Set bit 1 + setattr(obj, atrb, value) + return + + def my_tone_strn(obj, is_atr, pol_atr, tone_atr): + """Generate the CTCS/DCS tone code string.""" + vx = int(getattr(obj, tone_atr)) + if vx == 16383 or vx == 0: + return "Off" # 16383 is all bits set + if getattr(obj, is_atr) == 0: # Simple CTCSS code + tstr = str(vx / 10.0) + else: # DCS + if getattr(obj, pol_atr) == 0: + tstr = "D{:03x}R".format(vx) + else: + tstr = "D{:03x}N".format(vx) + return tstr + + def my_set_tone(setting, obj, is_atr, pol_atr, tone_atr): + """Callback- create the tone setting from string code.""" + sx = str(setting.value) # '131.8' or 'D231N' or 'Off' + if sx == "Off": + isx = 1 + polx = 1 + tonx = 0x3FFF + elif sx[0] == "D": # DCS + isx = 1 + if sx[4] == "N": + polx = 1 + else: + polx = 0 + tonx = int(sx[1:4], 16) + else: # CTCSS + isx = 0 + polx = 0 + tonx = int(float(sx) * 10.0) + setattr(obj, is_atr, isx) + setattr(obj, pol_atr, polx) + setattr(obj, tone_atr, tonx) + return + + val = _sets.fm_freq / 10.0 + if val == 0: + val = 88.9 # 0 is not valid + rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1) + rs = RadioSetting("settings.fm_freq", "FM Broadcast Freq (MHz)", rx) + rs.set_apply_callback(my_word2raw, _sets, "fm_freq") + basic.append(rs) + + wtled = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _sets.wtled])) + basic.append(wtled) + + rxled = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _sets.rxled])) + basic.append(rxled) + + txled = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _sets.txled])) + basic.append(txled) + + ledsw = RadioSetting("settings.ledsw", "Back light mode", + RadioSettingValueList(LIST_LEDSW, LIST_LEDSW[ + _sets.ledsw])) + basic.append(ledsw) + + beep = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(bool(_sets.beep))) + basic.append(beep) + + ring = RadioSetting("settings.ring", "Ring", + RadioSettingValueList(LIST_RING, LIST_RING[ + _sets.ring])) + basic.append(ring) + + bcl = RadioSetting("settings.bcl", "Busy channel lockout", + RadioSettingValueBoolean(bool(_sets.bcl))) + basic.append(bcl) + + if _vfoa.sql == 0xFF: + val = 0x04 + else: + val = _vfoa.sql + sqla = RadioSetting("upper.vfoa.sql", "Squelch (Upper)", + RadioSettingValueInteger(0, 9, val)) + basic.append(sqla) + + if _vfob.sql == 0xFF: + val = 0x04 + else: + val = _vfob.sql + sqlb = RadioSetting("lower.vfob.sql", "Squelch (Lower)", + RadioSettingValueInteger(0, 9, val)) + basic.append(sqlb) + + tmp = str(int(_sets.tot) * 30) # 30 sec step counter + rs = RadioSetting("settings.tot", "Transmit Timeout (Secs)", + RadioSettingValueList(LIST_TIMEOUT, tmp)) + rs.set_apply_callback(my_val_list, _sets, "tot") + basic.append(rs) + + tmp = str(int(_sets.sig_freq)) + rs = RadioSetting("settings.sig_freq", "Single Signaling Tone (Htz)", + RadioSettingValueList(LIST_SSF, tmp)) + rs.set_apply_callback(my_val_list, _sets, "sig_freq") + basic.append(rs) + + tmp = str(int(_sets.dtmf_txms)) + rs = RadioSetting("settings.dtmf_txms", "DTMF Tx Duration (mSecs)", + RadioSettingValueList(LIST_DTMFTX, tmp)) + rs.set_apply_callback(my_val_list, _sets, "dtmf_txms") + basic.append(rs) + + rs = RadioSetting("settings.rptr_mode", "Repeater Mode", + RadioSettingValueBoolean(bool(_sets.rptr_mode))) + basic.append(rs) + + # UPPER BAND SETTINGS + + # Freq Mode, convert bit 1 state to index pointer + val = _vfoa.frq_chn_mode / 2 + + rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val]) + rs = RadioSetting("upper.vfoa.frq_chn_mode", "Default Mode", rx) + rs.set_apply_callback(my_spcl, _vfoa, "frq_chn_mode") + a_band.append(rs) + + val =_vfoa.chan_num + 1 # Add 1 for 1-128 displayed + rs = RadioSetting("upper.vfoa.chan_num", "Initial Chan", + RadioSettingValueInteger(1, 128, val)) + rs.set_apply_callback(my_adjraw, _vfoa, "chan_num", -1) + a_band.append(rs) + + val = _vfoa.rxfreq / 100000.0 + if (val < 136.0 or val > 176.0): + val = 146.520 # 2m calling + rs = RadioSetting("upper.vfoa.rxfreq ", "Default Recv Freq (MHz)", + RadioSettingValueFloat(136.0, 176.0, val, 0.001, 5)) + rs.set_apply_callback(my_dbl2raw, _vfoa, "rxfreq") + a_band.append(rs) + + tmp = my_tone_strn(_vfoa, "is_rxdigtone", "rxdtcs_pol", "rx_tone") + rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfoa, "is_rxdigtone", + "rxdtcs_pol", "rx_tone") + a_band.append(rs) + + rx = RadioSettingValueList(LIST_RECVMODE, + LIST_RECVMODE[_vfoa.rx_mode]) + rs = RadioSetting("upper.vfoa.rx_mode", "Default Recv Mode", rx) + a_band.append(rs) + + tmp = my_tone_strn(_vfoa, "is_txdigtone", "txdtcs_pol", "tx_tone") + rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfoa, "is_txdigtone", + "txdtcs_pol", "tx_tone") + a_band.append(rs) + + rs = RadioSetting("upper.vfoa.launch_sig", "Launch Signaling", + RadioSettingValueList(LIST_SIGNAL, + LIST_SIGNAL[_vfoa.launch_sig])) + a_band.append(rs) + + rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfoa.tx_end_sig]) + rs = RadioSetting("upper.vfoa.tx_end_sig", "Xmit End Signaling", rx) + a_band.append(rs) + + rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfoa.fm_bw]) + rs = RadioSetting("upper.vfoa.fm_bw", "Wide/Narrow Band", rx) + a_band.append(rs) + + rx = RadioSettingValueBoolean(bool(_vfoa.cmp_nder)) + rs = RadioSetting("upper.vfoa.cmp_nder", "Compandor", rx) + a_band.append(rs) + + rs = RadioSetting("upper.vfoa.scrm_blr", "Scrambler", + RadioSettingValueBoolean(bool(_vfoa.scrm_blr))) + a_band.append(rs) + + rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfoa.shift]) + rs = RadioSetting("upper.vfoa.shift", "Xmit Shift", rx) + a_band.append(rs) + + val = _vfoa.offset / 100000.0 + rs = RadioSetting("upper.vfoa.offset", "Xmit Offset (MHz)", + RadioSettingValueFloat(0, 100.0, val, 0.001, 3)) + # Allow zero value + rs.set_apply_callback(my_dbl2raw, _vfoa, "offset", 0) + a_band.append(rs) + + tmp = str(_vfoa.step / 100.0) + rs = RadioSetting("step", "Freq step (KHz)", + RadioSettingValueList(LIST_STEPS, tmp)) + rs.set_apply_callback(my_word2raw, _vfoa,"step", 100) + a_band.append(rs) + + # LOWER BAND SETTINGS + + val = _vfob.frq_chn_mode / 2 + rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val]) + rs = RadioSetting("lower.vfob.frq_chn_mode", "Default Mode", rx) + rs.set_apply_callback(my_spcl, _vfob, "frq_chn_mode") + b_band.append(rs) + + val = _vfob.chan_num + 1 + rs = RadioSetting("lower.vfob.chan_num", "Initial Chan", + RadioSettingValueInteger(0, 127, val)) + rs.set_apply_callback(my_adjraw, _vfob, "chan_num", -1) + b_band.append(rs) + + val = _vfob.rxfreq / 100000.0 + if (val < 400.0 or val > 480.0): + val = 446.0 # UHF calling + rs = RadioSetting("lower.vfob.rxfreq ", "Default Recv Freq (MHz)", + RadioSettingValueFloat(400.0, 480.0, val, 0.001, 5)) + rs.set_apply_callback(my_dbl2raw, _vfob, "rxfreq") + b_band.append(rs) + + tmp = my_tone_strn(_vfob, "is_rxdigtone", "rxdtcs_pol", "rx_tone") + rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfob, "is_rxdigtone", + "rxdtcs_pol", "rx_tone") + b_band.append(rs) + + rx = RadioSettingValueList(LIST_RECVMODE, LIST_RECVMODE[_vfob.rx_mode]) + rs = RadioSetting("lower.vfob.rx_mode", "Default Recv Mode", rx) + b_band.append(rs) + + tmp = my_tone_strn(_vfob, "is_txdigtone", "txdtcs_pol", "tx_tone") + rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfob, "is_txdigtone", + "txdtcs_pol", "tx_tone") + b_band.append(rs) + + rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.launch_sig]) + rs = RadioSetting("lower.vfob.launch_sig", "Launch Signaling", rx) + b_band.append(rs) + + rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.tx_end_sig]) + rs = RadioSetting("lower.vfob.tx_end_sig", "Xmit End Signaling", rx) + b_band.append(rs) + + rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfob.fm_bw]) + rs = RadioSetting("lower.vfob.fm_bw", "Wide/Narrow Band", rx) + b_band.append(rs) + + rs = RadioSetting("lower.vfob.cmp_nder", "Compandor", + RadioSettingValueBoolean(bool(_vfob.cmp_nder))) + b_band.append(rs) + + rs = RadioSetting("lower.vfob.scrm_blr", "Scrambler", + RadioSettingValueBoolean(bool(_vfob.scrm_blr))) + b_band.append(rs) + + rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfob.shift]) + rs = RadioSetting("lower.vfob.shift", "Xmit Shift", rx) + b_band.append(rs) + + val = _vfob.offset / 100000.0 + rs = RadioSetting("lower.vfob.offset", "Xmit Offset (MHz)", + RadioSettingValueFloat(0, 100.0, val, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _vfob, "offset", 0) + b_band.append(rs) + + tmp = str(_vfob.step / 100.0) + rs = RadioSetting("step", "Freq step (KHz)", + RadioSettingValueList(LIST_STEPS, tmp)) + rs.set_apply_callback(my_word2raw, _vfob, "step", 100) + b_band.append(rs) + + # PowerOn & Freq Limits Settings + + def chars2str(cary, knt): + """Convert raw memory char array to a string: NOT a callback.""" + stx = "" + for char in cary[:knt]: + stx += chr(char) + return stx + + def my_str2ary(setting, obj, atrba, atrbc): + """Callback: convert 7-char string to char array with count.""" + ary = "" + knt = 7 + for j in range (6, -1, -1): # Strip trailing spaces + if str(setting.value)[j] == "" or str(setting.value)[j] == " ": + knt = knt - 1 + else: + break + for j in range(0, 7, 1): + if j < knt: ary += str(setting.value)[j] + else: ary += chr(0xFF) + setattr(obj, atrba, ary) + setattr(obj, atrbc, knt) + return + + tmp = chars2str(_lims.hello1, _lims.hello1_cnt) + rs = RadioSetting("hello_lims.hello1", "Power-On Message 1", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_str2ary, _lims, "hello1", "hello1_cnt") + lims.append(rs) + + tmp = chars2str(_lims.hello2, _lims.hello2_cnt) + rs = RadioSetting("hello_lims.hello2", "Power-On Message 2", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_str2ary, _lims,"hello2", "hello2_cnt") + lims.append(rs) + + # VALID_BANDS = [(136000000, 176000000),400000000, 480000000)] + + lval = _lims.vhf_low / 100000.0 + uval = _lims.vhf_high / 100000.0 + if lval >= uval: + lval = 144.0 + uval = 158.0 + + rs = RadioSetting("hello_lims.vhf_low", "Lower VHF Band Limit (MHz)", + RadioSettingValueFloat(136.0, 176.0, lval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "vhf_low") + lims.append(rs) + + rs = RadioSetting("hello_lims.vhf_high", "Upper VHF Band Limit (MHz)", + RadioSettingValueFloat(136.0, 176.0, uval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "vhf_high") + lims.append(rs) + + lval = _lims.uhf_low / 100000.0 + uval = _lims.uhf_high / 100000.0 + if lval >= uval: + lval = 420.0 + uval = 470.0 + + rs = RadioSetting("hello_lims.uhf_low", "Lower UHF Band Limit (MHz)", + RadioSettingValueFloat(400.0, 480.0, lval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "uhf_low") + lims.append(rs) + + rs = RadioSetting("hello_lims.uhf_high", "Upper UHF Band Limit (MHz)", + RadioSettingValueFloat(400.0, 480.0, uval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "uhf_high") + lims.append(rs) + + # Codes and DTMF Groups Settings + + def make_dtmf(ary, knt): + """Generate the DTMF code 1-8, NOT a callback.""" + tmp = "" + if knt > 0 and knt != 0xff: + for val in ary[:knt]: + if val > 0 and val <= 9: + tmp += chr(val + 48) + elif val == 0x0a: + tmp += "0" + elif val == 0x0d: + tmp += "A" + elif val == 0x0e: + tmp += "B" + elif val == 0x0f: + tmp += "C" + elif val == 0x00: + tmp += "D" + elif val == 0x0b: + tmp += "*" + elif val == 0x0c: + tmp += "#" + else: + msg = ("Invalid Character. Must be: 0-9,A,B,C,D,*,#") + raise InvalidValueError(msg) + return tmp + + def my_dtmf2raw(setting, obj, atrba, atrbc, syz=7): + """Callback: DTMF Code; sends 5 or 7-byte string.""" + draw = [] + knt = syz + for j in range (syz - 1, -1, -1): # Strip trailing spaces + if str(setting.value)[j] == "" or str(setting.value)[j] == " ": + knt = knt - 1 + else: + break + for j in range(0, syz): + bx = str(setting.value)[j] + obx = ord(bx) + dig = 0x0ff + if j < knt and knt > 0: # (Else) is pads + if bx == "0": + dig = 0x0a + elif bx == "A": + dig = 0x0d + elif bx == "B": + dig = 0x0e + elif bx == "C": + dig = 0x0f + elif bx == "D": + dig = 0x00 + elif bx == "*": + dig = 0x0b + elif bx == "#": + dig = 0x0c + elif obx >= 49 and obx <= 57: + dig = obx - 48 + else: + msg = ("Must be: 0-9,A,B,C,D,*,#") + raise InvalidValueError(msg) + # - End if/elif/else for bx + # - End if J<=knt + draw.append(dig) # Generate string of bytes + # - End for j + setattr(obj, atrba, draw) + setattr(obj, atrbc, knt) + return + + tmp = make_dtmf(_codes.native_id_code, _codes.native_id_cnt) + rs = RadioSetting("codes.native_id_code", "Native ID Code", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "native_id_code", + "native_id_cnt", 7) + codes.append(rs) + + tmp = make_dtmf(_codes.master_id_code, _codes.master_id_cnt) + rs = RadioSetting("codes.master_id_code", "Master Control ID Code", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "master_id_code", + "master_id_cnt",7) + codes.append(rs) + + tmp = make_dtmf(_codes.alarm_code, _codes.alarm_cnt) + rs = RadioSetting("codes.alarm_code", "Alarm Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "alarm_code", + "alarm_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.id_disp_code, _codes.id_disp_cnt) + rs = RadioSetting("codes.id_disp_code", "Identify Display Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "id_disp_code", + "id_disp_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.revive_code, _codes.revive_cnt) + rs = RadioSetting("codes.revive_code", "Revive Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes,"revive_code", + "revive_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.stun_code, _codes.stun_cnt) + rs = RadioSetting("codes.stun_code", "Remote Stun Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "stun_code", + "stun_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.kill_code, _codes.kill_cnt) + rs = RadioSetting("codes.kill_code", "Remote KILL Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "kill_code", + "kill_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.monitor_code, _codes.monitor_cnt) + rs = RadioSetting("codes.monitor_code", "Monitor Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "monitor_code", + "monitor_cnt", 5) + codes.append(rs) + + val = _codes.state_now + if val > 2: + val = 0 + + rx = RadioSettingValueList(LIST_STATE, LIST_STATE[val]) + rs = RadioSetting("codes.state_now", "Current State", rx) + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf1, _dtmf.dtmf1_cnt) + rs = RadioSetting("dtmf_tab.dtmf1", "DTMF1 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf1", "dtmf1_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf2, _dtmf.dtmf2_cnt) + rs = RadioSetting("dtmf_tab.dtmf2", "DTMF2 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf2", "dtmf2_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf3, _dtmf.dtmf3_cnt) + rs = RadioSetting("dtmf_tab.dtmf3", "DTMF3 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf3", "dtmf3_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf4, _dtmf.dtmf4_cnt) + rs = RadioSetting("dtmf_tab.dtmf4", "DTMF4 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf4", "dtmf4_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf5, _dtmf.dtmf5_cnt) + rs = RadioSetting("dtmf_tab.dtmf5", "DTMF5 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf5", "dtmf5_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf6, _dtmf.dtmf6_cnt) + rs = RadioSetting("dtmf_tab.dtmf6", "DTMF6 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf6", "dtmf6_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf7, _dtmf.dtmf7_cnt) + rs = RadioSetting("dtmf_tab.dtmf7", "DTMF7 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf7", "dtmf7_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf8, _dtmf.dtmf8_cnt) + rs = RadioSetting("dtmf_tab.dtmf8", "DTMF8 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf8", "dtmf8_cnt") + codes.append(rs) + + return group # END get_settings() + + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # Testing the file data size + if len(filedata) == MEM_SIZE + 8: + match_size = True + + # Testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +class LT725UVUpper(LT725UV): + VARIANT = "Upper" + _vfo = "upper" + + +class LT725UVLower(LT725UV): + VARIANT = "Lower" + _vfo = "lower" + + +class Zastone(chirp_common.Alias): + """Declare BJ-218 alias for Zastone BJ-218.""" + VENDOR = "Zastone" + MODEL = "BJ-218" + + +class Hesenate(chirp_common.Alias): + """Declare BJ-218 alias for Hesenate BJ-218.""" + VENDOR = "Hesenate" + MODEL = "BJ-218" + + +@directory.register +class Baojie218(LT725UV): + """Baojie BJ-218""" + VENDOR = "Baojie" + MODEL = "BJ-218" + ALIASES = [Zastone, Hesenate, ] diff --git a/chirp/drivers/mursv1.py b/chirp/drivers/mursv1.py new file mode 100644 index 0000000..31fac7e --- /dev/null +++ b/chirp/drivers/mursv1.py @@ -0,0 +1,874 @@ +# Copyright 2018: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp.drivers import baofeng_common +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, \ + InvalidValueError +from textwrap import dedent + +##### MAGICS ######################################################### + +# BTECH MURS-V1 magic string +MSTRING_MURSV1 = "\x50\x5F\x20\x15\x12\x15\x4D" + +##### ID strings ##################################################### + +# BTECH MURS-V1 +MURSV1_fp1 = "USM2402" + +DTMF_CHARS = "0123456789 *#ABCD" + +LIST_AB = ["A", "B"] +LIST_ALMOD = ["Off", "Site", "Tone", "Code"] +LIST_BANDWIDTH = ["Wide", "Narrow"] +LIST_COLOR = ["Off", "Blue", "Orange", "Purple"] +LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"] +LIST_MODE = ["Channel", "Name", "Frequency"] +LIST_OFF1TO9 = ["Off"] + list("123456789") +LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"] +LIST_OFFAB = ["Off"] + LIST_AB +LIST_RESUME = ["TO", "CO", "SE"] +LIST_PONMSG = ["Full", "Message"] +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_SCODE = ["%s" % x for x in range(1, 16)] +LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)] +LIST_RTONE = ["1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SHIFTD = ["Off", "+", "-"] +LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)] +LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)] +LIST_TXPOWER = ["High", "Low"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_WORKMODE = ["Frequency", "Channel"] + +MURS_FREQS = [151.820, 151.880, 151.940, 154.570, 154.600] * 3 +FM_MODE = [3, 4, 8, 9, 13, 14] + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x1EF0:0x1EF7] + + if rid in cls._fileid: + return True + + return False + + +@directory.register +class MURSV1(baofeng_common.BaofengCommonHT): + """BTech MURS-V1""" + VENDOR = "BTECH" + MODEL = "MURS-V1" + + _fileid = [MURSV1_fp1, ] + + _magic = [MSTRING_MURSV1, ] + _magic_response_length = 8 + _fw_ver_start = 0x1EF0 + _recv_block_size = 0x40 + _mem_size = 0x2000 + _ack_block = True + + _ranges = [(0x0000, 0x0DF0), + (0x0E00, 0x1800), + (0x1EE0, 0x1EF0), + (0x1F60, 0x1F70), + (0x1F80, 0x1F90), + (0x1FC0, 0x1FD0)] + _send_block_size = 0x10 + + MODES = ["NFM", "FM"] + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "!@#$%^&*()+-=[]:\";'<>?,./" + LENGTH_NAME = 7 + SKIP_VALUES = ["", "S"] + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00), + chirp_common.PowerLevel("Low", watts=.50)] + VALID_BANDS = [(151820000, 154600250)] + PTTID_LIST = LIST_PTTID + SCODE_LIST = LIST_SCODE + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = False + rf.has_name = True + rf.has_offset = False + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_name_length = self.LENGTH_NAME + rf.valid_duplexes = [] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = self.SKIP_VALUES + rf.valid_dtcs_codes = self.DTCS_CODES + rf.memory_bounds = (1, 15) + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_bands = self.VALID_BANDS + + return rf + + + MEM_FORMAT = """ + #seekto 0x0010; + struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1; + u8 unknown2:7, + lowpower:1; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; + } memory[15]; + + #seekto 0x0B00; + struct { + u8 code[5]; + u8 unused[11]; + } pttid[15]; + + #seekto 0x0CAA; + struct { + u8 code[5]; + u8 unused1:6, + aniid:2; + u8 unknown[2]; + u8 dtmfon; + u8 dtmfoff; + } ani; + + #seekto 0x0E20; + struct { + u8 unused01:4, + squelch:4; + u8 unused02; + u8 unused03; + u8 unused04:5, + save:3; + u8 unused05:4, + vox:4; + u8 unused06; + u8 unused07:4, + abr:4; + u8 unused08:7, + tdr:1; + u8 unused09:7, + beep:1; + u8 unused10:2, + timeout:6; + u8 unused11[4]; + u8 unused12:6, + voice:2; + u8 unused13; + u8 unused14:6, + dtmfst:2; + u8 unused15; + u8 unused16:6, + screv:2; + u8 unused17:6, + pttid:2; + u8 unused18:2, + pttlt:6; + u8 unused19:6, + mdfa:2; + u8 unused20:6, + mdfb:2; + u8 unused21; + u8 unused22:7, + sync:1; + u8 unused23[4]; + u8 unused24:6, + wtled:2; + u8 unused25:6, + rxled:2; + u8 unused26:6, + txled:2; + u8 unused27:6, + almod:2; + u8 unused28:7, + dbptt:1; + u8 unused29:6, + tdrab:2; + u8 unused30:7, + ste:1; + u8 unused31:4, + rpste:4; + u8 unused32:4, + rptrl:4; + u8 unused33:7, + ponmsg:1; + u8 unused34:7, + roger:1; + u8 unused35:6, + rtone:2; + u8 unused36; + u8 unused37:6, + rogerrx:2; + u8 unused38; + u8 displayab:1, + unknown1:2, + fmradio:1, + alarm:1, + unknown2:1, + reset:1, + menu:1; + u8 unused39; + u8 workmode; + u8 keylock; + u8 cht; + } settings; + + #seekto 0x0E76; + struct { + u8 unused1:1, + mrcha:7; + u8 unused2:1, + mrchb:7; + } wmchannel; + + #seekto 0x0F4E; + u16 fm_presets; + + #seekto 0x1010; + struct { + char name[7]; + u8 unknown1[9]; + } names[15]; + + #seekto 0x1ED0; + struct { + char line1[7]; + char line2[7]; + } sixpoweron_msg; + + #seekto 0x1EE0; + struct { + char line1[7]; + char line2[7]; + } poweron_msg; + + #seekto 0x1EF0; + struct { + char line1[7]; + char line2[7]; + } firmware_msg; + + struct squelch { + u8 sql0; + u8 sql1; + u8 sql2; + u8 sql3; + u8 sql4; + u8 sql5; + u8 sql6; + u8 sql7; + u8 sql8; + u8 sql9; + }; + + #seekto 0x1F60; + struct { + struct squelch vhf; + } squelch; + + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The BTech MURS-V1 driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def _get_mem(self, number): + return self._memobj.memory[number - 1] + + def _get_nam(self, number): + return self._memobj.names[number - 1] + + def get_memory(self, number): + _mem = self._get_mem(number) + _nam = self._get_nam(number) + + mem = chirp_common.Memory() + mem.number = number + + mem.freq = int(_mem.rxfreq) * 10 + + for char in _nam.name: + if str(char) == "\xFF": + char = " " # The OEM software may have 0xFF mid-name + mem.name += str(char) + mem.name = mem.name.rstrip() + + dtcs_pol = ["N", "N"] + + if _mem.txtone in [0, 0xFFFF]: + txmode = "" + elif _mem.txtone >= 0x0258: + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + elif _mem.txtone <= 0x0258: + txmode = "DTCS" + if _mem.txtone > 0x69: + index = _mem.txtone - 0x6A + dtcs_pol[0] = "R" + else: + index = _mem.txtone - 1 + mem.dtcs = self.DTCS_CODES[index] + else: + LOG.warn("Bug: txtone is %04x" % _mem.txtone) + + if _mem.rxtone in [0, 0xFFFF]: + rxmode = "" + elif _mem.rxtone >= 0x0258: + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + elif _mem.rxtone <= 0x0258: + rxmode = "DTCS" + if _mem.rxtone >= 0x6A: + index = _mem.rxtone - 0x6A + dtcs_pol[1] = "R" + else: + index = _mem.rxtone - 1 + mem.rx_dtcs = self.DTCS_CODES[index] + else: + LOG.warn("Bug: rxtone is %04x" % _mem.rxtone) + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + if not _mem.scan: + mem.skip = "S" + + levels = self.POWER_LEVELS + try: + mem.power = levels[_mem.lowpower] + except IndexError: + LOG.error("Radio reported invalid power level %s (in %s)" % + (_mem.power, levels)) + mem.power = levels[0] + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(self.PTTID_LIST, + self.PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("scode", "S-CODE", + RadioSettingValueList(self.SCODE_LIST, + self.SCODE_LIST[_mem.scode])) + mem.extra.append(rs) + + return mem + + def _set_mem(self, number): + return self._memobj.memory[number - 1] + + def _set_nam(self, number): + return self._memobj.names[number - 1] + + def validate_memory(self, mem): + msgs = baofeng_common.BaofengCommonHT.validate_memory(self, mem) + + if mem.freq != int(MURS_FREQS[mem.number - 1] * 1000000): + msgs.append(chirp_common.ValidationError( + 'Memory location cannot change frequency')) + + if mem.mode == "FM" and (mem.number - 1) not in FM_MODE: + msgs.append(chirp_common.ValidationError( + 'Memory location only supports NFM')) + + return msgs + + def set_memory(self, mem): + _mem = self._set_mem(mem.number) + _nam = self._set_nam(mem.number) + + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _nam.name[i] = mem.name[i] + except IndexError: + _nam.name[i] = "\xFF" + + rxmode = txmode = "" + if mem.tmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + _mem.rxtone = 0 + elif mem.tmode == "TSQL": + _mem.txtone = int(mem.ctone * 10) + _mem.rxtone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + rxmode = txmode = "DTCS" + _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + _mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1 + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + elif txmode == "DTCS": + _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + else: + _mem.txtone = 0 + if rxmode == "Tone": + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCS": + _mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1 + else: + _mem.rxtone = 0 + else: + _mem.rxtone = 0 + _mem.txtone = 0 + + if txmode == "DTCS" and mem.dtcs_polarity[0] == "R": + _mem.txtone += 0x69 + if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R": + _mem.rxtone += 0x69 + + _mem.scan = mem.skip != "S" + _mem.wide = mem.mode == "FM" + + if mem.power: + _mem.lowpower = self.POWER_LEVELS.index(mem.power) + else: + _mem.lowpower = 0 + + # extra settings + if len(mem.extra) > 0: + # there are setting, parse + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + else: + # there are no extra settings, load defaults + _mem.bcl = 0 + _mem.pttid = 0 + _mem.scode = 0 + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + fm_preset = RadioSettingGroup("fm_preset", "FM Preset") + dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings") + service = RadioSettingGroup("service", "Service Settings") + top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe, + service) + + # Basic settings + if _mem.settings.squelch > 0x09: + val = 0x00 + else: + val = _mem.settings.squelch + rs = RadioSetting("settings.squelch", "Squelch", + RadioSettingValueList( + LIST_OFF1TO9, LIST_OFF1TO9[val])) + basic.append(rs) + + if _mem.settings.save > 0x04: + val = 0x00 + else: + val = _mem.settings.save + rs = RadioSetting("settings.save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, LIST_SAVE[val])) + basic.append(rs) + + if _mem.settings.vox > 0x0A: + val = 0x00 + else: + val = _mem.settings.vox + rs = RadioSetting("settings.vox", "Vox", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + if _mem.settings.abr > 0x0A: + val = 0x00 + else: + val = _mem.settings.abr + rs = RadioSetting("settings.abr", "Backlight Timeout", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + rs = RadioSetting("settings.tdr", "Dual Watch", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(rs) + + rs = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(rs) + + if _mem.settings.timeout > 0x27: + val = 0x03 + else: + val = _mem.settings.timeout + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + LIST_TIMEOUT, LIST_TIMEOUT[val])) + basic.append(rs) + + if _mem.settings.voice > 0x02: + val = 0x01 + else: + val = _mem.settings.voice + rs = RadioSetting("settings.voice", "Voice Prompt", + RadioSettingValueList( + LIST_VOICE, LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("settings.dtmfst", "DTMF Sidetone", + RadioSettingValueList(LIST_DTMFST, LIST_DTMFST[ + _mem.settings.dtmfst])) + basic.append(rs) + + if _mem.settings.screv > 0x02: + val = 0x01 + else: + val = _mem.settings.screv + rs = RadioSetting("settings.screv", "Scan Resume", + RadioSettingValueList( + LIST_RESUME, LIST_RESUME[val])) + basic.append(rs) + + rs = RadioSetting("settings.pttid", "When to send PTT ID", + RadioSettingValueList(LIST_PTTID, LIST_PTTID[ + _mem.settings.pttid])) + basic.append(rs) + + if _mem.settings.pttlt > 0x1E: + val = 0x05 + else: + val = _mem.settings.pttlt + rs = RadioSetting("pttlt", "PTT ID Delay", + RadioSettingValueInteger(0, 50, val)) + basic.append(rs) + + rs = RadioSetting("settings.mdfa", "Display Mode (A)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("settings.mdfb", "Display Mode (B)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("settings.sync", "Sync A & B", + RadioSettingValueBoolean(_mem.settings.sync)) + basic.append(rs) + + rs = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.wtled])) + basic.append(rs) + + rs = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.rxled])) + basic.append(rs) + + rs = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.txled])) + basic.append(rs) + + val = _mem.settings.almod + rs = RadioSetting("settings.almod", "Alarm Mode", + RadioSettingValueList( + LIST_ALMOD, LIST_ALMOD[val])) + basic.append(rs) + + rs = RadioSetting("settings.dbptt", "Double PTT", + RadioSettingValueBoolean(_mem.settings.dbptt)) + basic.append(rs) + + rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)", + RadioSettingValueBoolean(_mem.settings.ste)) + basic.append(rs) + + rs = RadioSetting("settings.ponmsg", "Power-On Message", + RadioSettingValueList(LIST_PONMSG, LIST_PONMSG[ + _mem.settings.ponmsg])) + basic.append(rs) + + rs = RadioSetting("settings.roger", "Roger Beep", + RadioSettingValueBoolean(_mem.settings.roger)) + basic.append(rs) + + rs = RadioSetting("settings.rtone", "Tone Burst Frequency", + RadioSettingValueList(LIST_RTONE, LIST_RTONE[ + _mem.settings.rtone])) + basic.append(rs) + + rs = RadioSetting("settings.rogerrx", "Roger Beep (RX)", + RadioSettingValueList( + LIST_OFFAB, LIST_OFFAB[ + _mem.settings.rogerrx])) + basic.append(rs) + + # Advanced settings + rs = RadioSetting("settings.reset", "RESET Menu", + RadioSettingValueBoolean(_mem.settings.reset)) + advanced.append(rs) + + rs = RadioSetting("settings.menu", "All Menus", + RadioSettingValueBoolean(_mem.settings.menu)) + advanced.append(rs) + + rs = RadioSetting("settings.fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_mem.settings.fmradio)) + advanced.append(rs) + + rs = RadioSetting("settings.alarm", "Alarm Sound", + RadioSettingValueBoolean(_mem.settings.alarm)) + advanced.append(rs) + + # Other settings + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = _mem.firmware_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val) + other.append(rs) + + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val) + other.append(rs) + + _msg = _mem.sixpoweron_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val) + other.append(rs) + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val) + other.append(rs) + + _msg = _mem.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + # Work mode settings + rs = RadioSetting("settings.displayab", "Display", + RadioSettingValueList( + LIST_AB, LIST_AB[_mem.settings.displayab])) + work.append(rs) + + rs = RadioSetting("settings.keylock", "Keypad Lock", + RadioSettingValueBoolean(_mem.settings.keylock)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(1, 15, + _mem.wmchannel.mrcha)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(1, 15, + _mem.wmchannel.mrchb)) + work.append(rs) + + # broadcast FM settings + _fm_presets = self._memobj.fm_presets + if _fm_presets <= 108.0 * 10 - 650: + preset = _fm_presets / 10.0 + 65 + elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10: + preset = _fm_presets / 10.0 + else: + preset = 76.0 + rs = RadioSetting("fm_presets", "FM Preset(MHz)", + RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)) + fm_preset.append(rs) + + # DTMF settings + def apply_code(setting, obj, length): + code = [] + for j in range(0, length): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + pttid = RadioSetting("pttid/%i.code" % i, + "Signal Code %i" % (i + 1), val) + pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5) + dtmfe.append(pttid) + + if _mem.ani.dtmfon > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + if _mem.ani.dtmfoff > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.code", "ANI Code", val) + rs.set_apply_callback(apply_code, self._memobj.ani, 5) + dtmfe.append(rs) + + rs = RadioSetting("ani.aniid", "When to send ANI ID", + RadioSettingValueList(LIST_PTTID, + LIST_PTTID[_mem.ani.aniid])) + dtmfe.append(rs) + + # Service settings + for index in range(0, 10): + key = "squelch.vhf.sql%i" % (index) + _obj = self._memobj.squelch.vhf + val = RadioSettingValueInteger(0, 123, + getattr(_obj, "sql%i" % (index))) + if index == 0: + val.set_mutable(False) + name = "Squelch %i" % (index) + rs = RadioSetting(key, name, val) + service.append(rs) + + return top + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == 0x2008: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/puxing.py b/chirp/drivers/puxing.py new file mode 100644 index 0000000..fb10d6b --- /dev/null +++ b/chirp/drivers/puxing.py @@ -0,0 +1,524 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Puxing radios management module""" + +import time +import os +import logging + +from chirp import util, chirp_common, bitwise, errors, directory +from chirp.drivers.wouxun import wipe_memory, do_download, do_upload + +LOG = logging.getLogger(__name__) + + +def _puxing_prep(radio): + radio.pipe.write("\x02PROGRA") + ack = radio.pipe.read(1) + if ack != "\x06": + raise Exception("Radio did not ACK first command") + + radio.pipe.write("M\x02") + ident = radio.pipe.read(8) + if len(ident) != 8: + LOG.debug(util.hexprint(ident)) + raise Exception("Radio did not send identification") + + radio.pipe.write("\x06") + if radio.pipe.read(1) != "\x06": + raise Exception("Radio did not ACK ident") + + +def puxing_prep(radio): + """Do the Puxing PX-777 identification dance""" + for _i in range(0, 10): + try: + return _puxing_prep(radio) + except Exception, e: + time.sleep(1) + + raise e + + +def puxing_download(radio): + """Talk to a Puxing PX-777 and do a download""" + try: + puxing_prep(radio) + return do_download(radio, 0x0000, 0x0C60, 0x0008) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + +def puxing_upload(radio): + """Talk to a Puxing PX-777 and do an upload""" + try: + puxing_prep(radio) + return do_upload(radio, 0x0000, 0x0C40, 0x0008) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Low", watts=1.00)] + +PUXING_CHARSET = list("0123456789") + \ + [chr(x + ord("A")) for x in range(0, 26)] + \ + list("- ") + +PUXING_MEM_FORMAT = """ +#seekto 0x0000; +struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 _3_unknown_1; + u8 _2_unknown_1:2, + power_high:1, + iswide:1, + skip:1, + bclo:2, + _2_unknown_2:1; + u8 _4_unknown1:7, + pttid:1; + u8 unknown; +} memory[128]; + +#seekto 0x080A; +struct { + u8 limits; + u8 model; +} model[1]; + +#seekto 0x0850; +struct { + u8 name[6]; + u8 pad[2]; +} names[128]; +""" + +# Limits +# 67- 72: 0xEE +# 136-174: 0xEF +# 240-260: 0xF0 +# 350-390: 0xF1 +# 400-430: 0xF2 +# 430-450: 0xF3 +# 450-470: 0xF4 +# 470-490: 0xF5 +# 400-470: 0xF6 +# 460-520: 0xF7 + +PUXING_MODELS = { + 328: 0x38, + 338: 0x39, + 777: 0x3A, +} + +PUXING_777_BANDS = [ + (67000000, 72000000), + (136000000, 174000000), + (240000000, 260000000), + (350000000, 390000000), + (400000000, 430000000), + (430000000, 450000000), + (450000000, 470000000), + (470000000, 490000000), + (400000000, 470000000), + (460000000, 520000000), +] + + +@directory.register +class Puxing777Radio(chirp_common.CloneModeRadio): + """Puxing PX-777""" + VENDOR = "Puxing" + MODEL = "PX-777" + + def sync_in(self): + self._mmap = puxing_download(self) + self.process_mmap() + + def sync_out(self): + puxing_upload(self) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = ''.join(set(PUXING_CHARSET)) + rf.valid_name_length = 6 + rf.valid_tuning_steps = [2.5, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, + 50.0, 100.0] + rf.has_ctone = False + rf.has_tuning_step = False + rf.has_bank = False + rf.memory_bounds = (1, 128) + + if not hasattr(self, "_memobj") or self._memobj is None: + rf.valid_bands = [PUXING_777_BANDS[1]] + elif self._memobj.model.model == PUXING_MODELS[777]: + limit_idx = self._memobj.model.limits - 0xEE + try: + rf.valid_bands = [PUXING_777_BANDS[limit_idx]] + except IndexError: + LOG.error("Invalid band index %i (0x%02x)" % + (limit_idx, self._memobj.model.limits)) + rf.valid_bands = [PUXING_777_BANDS[1]] + elif self._memobj.model.model == PUXING_MODELS[328]: + # There are PX-777 that says to be model 328 ... + # for them we only know this freq limits till now + if self._memobj.model.limits == 0xEE: + rf.valid_bands = [PUXING_777_BANDS[1]] + else: + raise Exception("Unsupported band limits 0x%02x for PX-777" % + (self._memobj.model.limits) + " submodel 328" + " - PLEASE REPORT THIS ERROR TO DEVELOPERS!!") + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(PUXING_MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + "\r\n" + \ + repr(self._memobj.names[number - 1]) + + @classmethod + def match_model(cls, filedata, filename): + # There are PX-777 that says to be model 328 ... + return (len(filedata) == 3168 and + (ord(filedata[0x080B]) == PUXING_MODELS[777] or + (ord(filedata[0x080B]) == PUXING_MODELS[328] and + ord(filedata[0x080A]) == 0xEE))) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + _nam = self._memobj.names[number - 1] + + def _is_empty(): + for i in range(0, 4): + if _mem.rx_freq[i].get_raw() != "\xFF": + return False + return True + + def _is_no_tone(field): + return field.get_raw() in ["\x00\x00", "\xFF\xFF"] + + def _get_dtcs(value): + # Upper nibble 0x80 -> DCS, 0xC0 -> Inv. DCS + if value > 12000: + return "R", value - 12000 + elif value > 8000: + return "N", value - 8000 + else: + raise Exception("Unable to convert DCS value") + + def _do_dtcs(mem, txfield, rxfield): + if int(txfield) < 8000 or int(rxfield) < 8000: + raise Exception("Split tone not supported") + + if txfield[0].get_raw() == "\xFF": + tp, tx = "N", None + else: + tp, tx = _get_dtcs(int(txfield)) + + if rxfield[0].get_raw() == "\xFF": + rp, rx = "N", None + else: + rp, rx = _get_dtcs(int(rxfield)) + + if not rx: + rx = tx + if not tx: + tx = rx + + if tx != rx: + raise Exception("Different RX and TX DCS codes not supported") + + mem.dtcs = tx + mem.dtcs_polarity = "%s%s" % (tp, rp) + + mem = chirp_common.Memory() + mem.number = number + + if _is_empty(): + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + mem.offset = (int(_mem.tx_freq) * 10) - mem.freq + if mem.offset < 0: + mem.duplex = "-" + elif mem.offset: + mem.duplex = "+" + mem.offset = abs(mem.offset) + if not _mem.skip: + mem.skip = "S" + if not _mem.iswide: + mem.mode = "NFM" + + if _is_no_tone(_mem.tx_tone): + pass # No tone + elif int(_mem.tx_tone) > 8000 or \ + (not _is_no_tone(_mem.rx_tone) and int(_mem.rx_tone) > 8000): + mem.tmode = "DTCS" + _do_dtcs(mem, _mem.tx_tone, _mem.rx_tone) + else: + mem.rtone = int(_mem.tx_tone) / 10.0 + mem.tmode = _is_no_tone(_mem.rx_tone) and "Tone" or "TSQL" + + mem.power = POWER_LEVELS[not _mem.power_high] + + for i in _nam.name: + if i == 0xFF: + break + mem.name += PUXING_CHARSET[i] + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + _nam = self._memobj.names[mem.number - 1] + + if mem.empty: + wipe_memory(_mem, "\xFF") + return + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "+": + _mem.tx_freq = (mem.freq / 10) + (mem.offset / 10) + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq / 10) - (mem.offset / 10) + else: + _mem.tx_freq = (mem.freq / 10) + _mem.skip = mem.skip != "S" + _mem.iswide = mem.mode != "NFM" + + _mem.rx_tone[0].set_raw("\xFF") + _mem.rx_tone[1].set_raw("\xFF") + _mem.tx_tone[0].set_raw("\xFF") + _mem.tx_tone[1].set_raw("\xFF") + + if mem.tmode == "DTCS": + _mem.tx_tone = int("%x" % int("%i" % (mem.dtcs), 16)) + _mem.rx_tone = int("%x" % int("%i" % (mem.dtcs), 16)) + + # Argh. Set the high order two bits to signal DCS or Inv. DCS + txm = mem.dtcs_polarity[0] == "N" and 0x80 or 0xC0 + rxm = mem.dtcs_polarity[1] == "N" and 0x80 or 0xC0 + _mem.tx_tone[1].set_raw(chr(ord(_mem.tx_tone[1].get_raw()) | txm)) + _mem.rx_tone[1].set_raw(chr(ord(_mem.rx_tone[1].get_raw()) | rxm)) + + elif mem.tmode: + _mem.tx_tone = int(mem.rtone * 10) + if mem.tmode == "TSQL": + _mem.rx_tone = int(_mem.tx_tone) + + if mem.power: + _mem.power_high = not POWER_LEVELS.index(mem.power) + else: + _mem.power_high = True + + # Default to disabling the busy channel lockout + # 00 == Close + # 01 == Carrier + # 10 == QT/DQT + _mem.bclo = 0 + + _nam.name = [0xFF] * 6 + for i in range(0, len(mem.name)): + try: + _nam.name[i] = PUXING_CHARSET.index(mem.name[i]) + except IndexError: + raise Exception("Character `%s' not supported") + + +def puxing_2r_prep(radio): + """Do the Puxing 2R identification dance""" + radio.pipe.timeout = 0.2 + radio.pipe.write("PROGRAM\x02") + ack = radio.pipe.read(1) + if ack != "\x06": + raise Exception("Radio is not responding") + + radio.pipe.write(ack) + ident = radio.pipe.read(16) + LOG.info("Radio ident: %s (%i)" % (repr(ident), len(ident))) + + +def puxing_2r_download(radio): + """Talk to a Puxing 2R and do a download""" + try: + puxing_2r_prep(radio) + return do_download(radio, 0x0000, 0x0FE0, 0x0010) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + +def puxing_2r_upload(radio): + """Talk to a Puxing 2R and do an upload""" + try: + puxing_2r_prep(radio) + return do_upload(radio, 0x0000, 0x0FE0, 0x0010) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + +PUXING_2R_MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd freq[4]; + lbcd offset[4]; + u8 rx_tone; + u8 tx_tone; + u8 duplex:2, + txdtcsinv:1, + rxdtcsinv:1, + simplex:1, + unknown2:1, + iswide:1, + ishigh:1; + u8 name[5]; +} memory[128]; +""" + +PX2R_DUPLEX = ["", "+", "-", ""] +PX2R_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.0), + chirp_common.PowerLevel("High", watts=2.0)] +PX2R_CHARSET = "0123456789- ABCDEFGHIJKLMNOPQRSTUVWXYZ +" + + +@directory.register +class Puxing2RRadio(chirp_common.CloneModeRadio): + """Puxing PX-2R""" + VENDOR = "Puxing" + MODEL = "PX-2R" + _memsize = 0x0FE0 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = PX2R_POWER_LEVELS + rf.valid_bands = [(400000000, 500000000)] + rf.valid_characters = PX2R_CHARSET + rf.valid_name_length = 5 + rf.valid_duplexes = ["", "+", "-"] + rf.valid_skips = [] + rf.has_ctone = False + rf.has_tuning_step = False + rf.has_bank = False + rf.memory_bounds = (1, 128) + rf.can_odd_split = False + return rf + + @classmethod + def match_model(cls, filedata, filename): + return (len(filedata) == cls._memsize) and \ + filedata[-16:] != "IcomCloneFormat3" + + def sync_in(self): + self._mmap = puxing_2r_download(self) + self.process_mmap() + + def sync_out(self): + puxing_2r_upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(PUXING_2R_MEM_FORMAT, self._mmap) + + def get_memory(self, number): + _mem = self._memobj.memory[number-1] + + mem = chirp_common.Memory() + mem.number = number + if _mem.get_raw()[0:4] == "\xff\xff\xff\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 10 + mem.offset = int(_mem.offset) * 10 + mem.mode = _mem.iswide and "FM" or "NFM" + mem.duplex = PX2R_DUPLEX[_mem.duplex] + mem.power = PX2R_POWER_LEVELS[_mem.ishigh] + + if _mem.tx_tone >= 0x33: + mem.dtcs = chirp_common.DTCS_CODES[_mem.tx_tone - 0x33] + mem.tmode = "DTCS" + mem.dtcs_polarity = \ + (_mem.txdtcsinv and "R" or "N") + \ + (_mem.rxdtcsinv and "R" or "N") + elif _mem.tx_tone: + mem.rtone = chirp_common.TONES[_mem.tx_tone - 1] + mem.tmode = _mem.rx_tone and "TSQL" or "Tone" + + count = 0 + for i in _mem.name: + if i == 0xFF: + break + try: + mem.name += PX2R_CHARSET[i] + except Exception: + LOG.error("Unknown name char %i: 0x%02x (mem %i)" % + (count, i, number)) + mem.name += " " + count += 1 + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + + if mem.empty: + _mem.set_raw("\xff" * 16) + return + + _mem.freq = mem.freq / 10 + _mem.offset = mem.offset / 10 + _mem.iswide = mem.mode == "FM" + _mem.duplex = PX2R_DUPLEX.index(mem.duplex) + _mem.ishigh = mem.power == PX2R_POWER_LEVELS[1] + + if mem.tmode == "DTCS": + _mem.tx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33 + _mem.rx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33 + _mem.txdtcsinv = mem.dtcs_polarity[0] == "R" + _mem.rxdtcsinv = mem.dtcs_polarity[1] == "R" + elif mem.tmode in ["Tone", "TSQL"]: + _mem.tx_tone = chirp_common.TONES.index(mem.rtone) + 1 + _mem.rx_tone = mem.tmode == "TSQL" and int(_mem.tx_tone) or 0 + else: + _mem.tx_tone = 0 + _mem.rx_tone = 0 + + for i in range(0, 5): + try: + _mem.name[i] = PX2R_CHARSET.index(mem.name[i]) + except IndexError: + _mem.name[i] = 0xFF + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) diff --git a/chirp/drivers/puxing_px888k.py b/chirp/drivers/puxing_px888k.py new file mode 100644 index 0000000..78f49a6 --- /dev/null +++ b/chirp/drivers/puxing_px888k.py @@ -0,0 +1,1878 @@ +# Copyright 2016 Leo Barring +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, directory, memmap, \ + bitwise, settings, errors +from struct import pack +import logging + +LOG = logging.getLogger(__name__) + +SUPPORT_NONSPLIT_DUPLEX_ONLY = False +SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS = True +UNAMBIGUOUS_CROSS_MODES_ONLY = True + +# With this setting enabled, some CHIRP settings are stored in +# thought-to-be junk/padding data of the channel memories. +# Enabling this feature while using CHIRP with an actual radio +# and any effects thereof is entirely the responsibility of the user. +ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES = False + +MEM_FORMAT = """ +// data fields are generally written 0xff if they are unset +struct { + +#seekto 0x0000; + struct { + struct { + // 0-3 + bbcd rx_freq[4]; + + // 4-7 + bbcd tx_freq[4]; + + // 8-9 A-B + struct { + u8 digital:1, + invert:1, + high:6; + u8 low; + } tone[2]; + // tx_squelch on 0, rx_squelch on 1 + + // C + // the duplex sign is not used for memories, + // but is kept for interface consistency + u8 duplex_sign:2, + compander:1, + txpower:1, + modulation_width:1, + txrx_reverse:1, + bcl:2; + + // D + u8 scrambler_type:3, + use_scrambler:1, + opt_signal:2, + ptt_id_edge:2; + + // E-F + // u8 _unknown_000E[2]; + %s + } data[128]; + struct { + // 0-5, alt 8-D + char entry[6]; + + // 6-8, alt E-F + char _unknown_0806[2]; + } names[128]; + +#seekto 0x0c20; + bit present[128]; + +#seekto 0x0c30; + bit priority[128]; + } channel_memory; + +#seekto 0x0c00; + struct { + // 0-3 + bbcd rx_freq[4]; + + // 4-7 + bbcd tx_freq[4]; // actually offset, but kept for name consistency + + // 8 + struct { + u8 digital:1, + invert:1, + high:6; + u8 low; + } tone[2]; // tx_squelch on 0, rx_squelch on 1 + + // C + u8 duplex_sign:2, + compander:1, + txpower:1, + modulation_width:1, + txrx_reverse:1, + bcl:2; + + // D + u8 scrambler_type:3, + use_scrambler:1, + opt_signal:2, + ptt_id_edge:2; + + // E-F + // u8 _unknown_0C0E[2]; + %s + + } vfo_data[2]; + +#seekto 0xc40; + struct { + // 0-5 + char model_string[6]; // typically PX888D, unknown if rw or ro + + // 6-7 + u8 _unknown_0C46[2]; + + // 8-9 + struct { + bbcd lower_freq[2]; + bbcd upper_freq[2]; + } band_limits[2]; + } model_information; + +#seekto 0x0c50; + char radio_information_string[16]; + +#seekto 0x0c60; + struct { + // 0 + u8 ptt_cancel_sq:1, + dis_ptt_id:1, + workmode_b:2, + use_roger_beep:1, + msk_reverse:1, + workmode_a:2; + + // 1 + u8 backlight_color:2, + backlight_mode:2, + dual_single_watch:1, + auto_keylock:1, + scan_mode:2; + + // 2 + u8 rx_stun:1, + tx_stun:1, + boot_message_mode:2, + battery_save:1, + key_beep:1, + voice_announce:2; + + // 3 + bbcd squelch_level; + + // 4 + bbcd tx_timeout; + + // 5 + u8 allow_keypad:1, + relay_without_disable_tail:1 + _unknown_0C65:1, + call_channel_active:1, + vox_gain:4; + + // 6 + bbcd vox_delay; + + // 7 + bbcd vfo_step; + + // 8 + bbcd ptt_id_type; + + // 9 + u8 keypad_lock:1, + _unknown_0C69_1:1, + side_button_hold_mode:2, + dtmf_sidetone:1, + _unknown_0C69_2:1, + side_button_click_mode:2; + + // A + u8 roger_beep:4, + main_watch:1, + _unknown_0C6A:3; + + // B + u8 channel_a; + + // C + u8 channel_b; + + // D + u8 priority_channel; + + // E + u8 wait_time; + + // F + u8 _unknown_0C6F; + + // 0-7 on next block + u8 _unknown_0C70[8]; + + // 8-D on next block + char boot_message[6]; + } opt_settings; + +#seekto 0x0c80; + struct { + // these fields are used for all ptt id forms (msk/dtmf/5t) + // (only one can be active and stored at a time) + // and different constraints are applied depending + // on the ptt id type + u8 entry[7]; + u8 length; + } ptt_id_data[2]; + // 0 is BOT, 1 is EOT + +#seekto 0x0c90; + struct { + // 0 + u8 _unknown_0C90; + + // 1 + u8 _unknown_0C91_1:3, + channel_stepping:1, + unknown_0C91_2:1 + receive_range:2 + unknown_0C91_3:1; + + // 2-3 + u8 _unknown_0C92[2]; + + // 4-7 + u8 vfo_freq[4]; + + // 8-F and two more blocks + struct { + u8 entry[4]; + } memory[10]; + } fm_radio; + +#seekto 0x0cc0; + struct { + char id_code[4]; + struct { + char entry[4]; + } phone_book[9]; + } msk_settings; + +#seekto 0x0cf0; + struct { + // 0-3 + bbcd rx_freq[4]; + + // 4-7 + bbcd tx_freq[4]; + + // 8 + struct { + u8 digital:1, + invert:1, + high:6; + u8 low; + } tone[2]; + // tx_squelch on 0, rx_squelch on 1 + + + // C + // the duplex sign is not used for the CALL, + // channel but is kept for interface consistency + u8 duplex_sign:2, + compander:1, + txpower:1, + modulation_width:1, + txrx_reverse:1 + bcl:2; + + // D + u8 scrambler_type:3, + use_scrambler:1, + opt_signal:2, + ptt_id_edge:2; + + // E-F + // u8 _unknown_0CFE[2]; + %s + } call_channel; + +#seekto 0x0d00; + struct { + + // DTMF codes are stored as hex half-bytes, + // 0-9 A-D are mapped straight + // DTMF '*' is HEX E, DTMF '#' is HEX F + + // 0x0d00 + struct { + u8 digit_length; // 0x05 to 0x14 corresponding to 50-200ms + u8 inter_digit_pause; // same + u8 first_digit_length; // same + u8 first_digit_delay; // 0x02 to 0x14 corresponding to 100-1000ms + } timing; + +#seekto 0x0d30; + u8 _unknown_0D30[2]; // 0-1 + u8 group_code; // 2 + u8 reset_time; // 3 + u8 alert_transpond; // 4 + u8 id_code[4]; // 5-8 + u8 _unknown_0D39[4]; // 9-C + u8 id_code_length; // D + u8 _unknown_0d3e[2]; // E-F + +// 0x0d40 + u8 tx_stun_code[4]; + u8 _unknown_0D44[4]; + u8 tx_stun_code_length; + u8 cancel_tx_stun_code_length; + u8 cancel_tx_stun_code[4]; + u8 _unknown_0D4E[2]; + +// 0x0d50 + u8 rxtx_stun_code[4]; + u8 _unknown_0D54[4]; + u8 rxtx_stun_code_length; + u8 cancel_rxtx_stun_code_length; + u8 cancel_rxtx_stun_code[4]; + u8 _unknown_0D4E[2]; + +// 0x0d60 + struct { + u8 entry[5]; + u8 _unknown_0D65[3]; + u8 length; + u8 _unknown_0D69[7]; + } phone_book[9]; + } dtmf_settings; + +#seekto 0x0e00; + struct { + u8 delay; + u8 _unknown_0E01[5]; + u8 alert_transpond; + u8 reset_time; + u8 tone_standard; + u8 id_code[3]; + +#seekto 0x0e20; + struct { + u8 period; + u8 group_code:4, + repeat_code:4; + } tone_settings[4]; + // the order is ZVEI1 ZVEI2 CCIR1 CCITT + +#seekto 0x0e40; + // 5-Tone tone standard frequency table + // unknown use, changing the values does not seem to have + // any effect on the produced sound, but the values are not + // overwritten either. + il16 tone_frequency_table[16]; + +// 0xe60 + u8 tx_stun_code[5]; + u8 _unknown_0E65[3]; + u8 tx_stun_code_length; + u8 cancel_tx_stun_code_length; + u8 cancel_tx_stun_code[5]; + u8 _unknown_0E6F; + +// 0xe70 + u8 rxtx_stun_code[5]; + u8 _unknown_0E75[3]; + u8 rxtx_stun_code_length; + u8 cancel_rxtx_stun_code_length; + u8 cancel_rxtx_stun_code[5]; + u8 _unknown_0E7F; + +// 0xe80 + struct { + u8 entry[3]; + } phone_book[9]; + } five_tone_settings; +} mem;""" + +# various magic numbers and strings, apart from the memory format +if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES: + LOG.warn("ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES" + "AND/OR DANGEROUS FEATURES ENABLED") + MEM_FORMAT = MEM_FORMAT % ( + "u8 _unknown_000E_1: 6,\n" + " experimental_duplex_mode_indicator: 1,\n" + " experimental_cross_mode_indicator: 1;\n" + "u8 _unknown_000F;", + "u8 _unknown_0C0E_1: 6,\n" + " experimental_duplex_mode_indicator: 1,\n" + " experimental_cross_mode_indicator: 1;\n" + "u8 _unknown_0C0F;", + "u8 _unknown_0CFE_1: 6,\n" + " experimental_duplex_mode_indicator: 1,\n" + " experimental_cross_mode_indicator: 1;\n" + "u8 _unknown_0CFF;") + # we don't need these settings anymore, because it's exactly what the + # experimental features are about + SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS = False + SUPPORT_NONSPLIT_DUPLEX_ONLY = False + UNAMBIGUOUS_CROSS_MODES_ONLY = False + +else: + MEM_FORMAT = MEM_FORMAT % ( + "u8 _unknown_000E[2];", + "u8 _unknown_0C0E[2];", + "u8 _unknown_0CFE[2];") + +FILE_MAGIC = [0xc40, 0xc50, + '\x50\x58\x38\x38\x38\x44\x00\xff' + '\x13\x40\x17\x60\x40\x00\x48\x00'] +HANDSHAKE_OUT = b'XONLINE' +HANDSHAKE_IN = [b'PX888D\x00\xff'] + +LOWER_READ_BOUND = 0 +UPPER_READ_BOUND = 0x1000 +LOWER_WRITE_BOUND = 0 +UPPER_WRITE_BOUND = 0x0fc0 +BLOCKSIZE = 64 + +OFF_INT = ["Off"] + [str(x+1) for x in range(100)] +OFF_ON = ["Off", "On"] +INACTIVE_ACTIVE = ["Inactive", "Active"] +NO_YES = ["No", "Yes"] +YES_NO = ["Yes", "No"] + +BANDS = [(134000000, 176000000), # VHF + (400000000, 480000000)] # UHF + +SPECIAL_CHANNELS = {'VFO-A': -2, 'VFO-B': -1, 'CALL': 0} +SPECIAL_NUMBERS = {-2: 'VFO-A', -1: 'VFO-B', 0: 'CALL'} + +DUPLEX_MODES = ['', '+', '-', 'split'] +if SUPPORT_NONSPLIT_DUPLEX_ONLY: + DUPLEX_MODES = ['', '+', '-'] + +TONE_MODES = ["", "Tone", "TSQL", "DTCS", "Cross"] + +CROSS_MODES = ["Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS", + "Tone->"] +if UNAMBIGUOUS_CROSS_MODES_ONLY: + CROSS_MODES = ["Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + +MODES = ["NFM", "FM"] + +# Only the 'High' power level is quantified in the manual for +# the radio (4W for VHF, 5W for UHF), a web search turned +# up numbers for the 'Low' power level (0.5W for VHF and +# 0.7W for UHF), but they are not official to my knowledge +# and should be taken with a grain of salt or two. +# Numbers used in code is the averages +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.6), + chirp_common.PowerLevel("High", watts=4.5)] + +SKIP_MODES = ["", "S"] +BCL_MODES = ["Off", "Carrier", "QT/DQT"] +SCRAMBLER_MODES = OFF_INT[0:9] +PTT_ID_EDGES = ["Off", "BOT", "EOT", "Both"] +OPTSIGN_MODES = ["None", "DTMF", "5-Tone", "MSK"] + +VFO_STRIDE = ['5kHz', '6.25kHz', '10kHz', '12.5kHz', '25kHz'] +AB = ['A', 'B'] +WATCH_MODES = ['Single watch', 'Dual watch'] +AB_MODES = ['VFO', 'Memory index', 'Memory name', 'Memory frequency'] +SCAN_MODES = ["Time", "Carrier", "Seek"] +WAIT_TIMES = [("0.3s", 6), ("0.5s", 10)] +\ + [("%ds" % t, t*20) for t in range(1, 13)] + +BUTTON_MODES = ["Send call list data", + "Emergency alarm", + "Send 1750Hz signal", + "Open squelch"] +BOOT_MESSAGE_TYPES = ["Off", "Battery voltage", "Custom message"] +TALKBACK = ['Off', 'Chinese', 'English'] +BACKLIGHT_COLORS = zip(["Blue", "Orange", "Purple"], range(1, 4)) +VOX_GAIN = OFF_INT[0:10] +VOX_DELAYS = ['1s', '2s', '3s', '4s'] +TRANSMIT_ALARMS = ['Off', '30s', '60s', '90s', '120s', + '150s', '180s', '210s', '240s', '270s'] + +DATA_MODES = ['MSK', 'DTMF', '5-Tone'] + +ASCIIPART = ''.join([chr(x) for x in range(0x20, 0x7f)]) +DTMF = "0123456789ABCD*#" +HEXADECIMAL = "0123456789ABCDEF" + +ROGER_BEEP = OFF_INT[0:11] +BACKLIGHT_MODES = ["Off", "Auto", "On"] + +TONE_RESET_TIME = ['Off'] + ['%ds' % x for x in range(1, 256)] +DTMF_TONE_RESET_TIME = TONE_RESET_TIME[0:16] + +DTMF_GROUPS = zip(["Off", "A", "B", "C", "D", "*", "#"], [255]+range(10, 16)) +FIVE_TONE_STANDARDS = ['ZVEI1', 'ZVEI2', 'CCIR1', 'CCITT'] + +# should mimic the defaults in the memedit MemoryEditor somewhat +# 0 1 2 3 4 5 6 7 +SANE_MEMORY_DEFAULT = b"\x14\x61\x00\x00\x14\x61\x00\x00" + \ + b"\xff\xff\xff\xff\xc8\x00\xff\xff" +# 8 9 A B C D E F + + +# these two option sets are listed differently like this in the stock software, +# so I'm keeping them separate for now if they are in fact identical +# in behaviour, that should probably be amended +DTMF_ALERT_TRANSPOND = zip(['Off', 'Call alert', + 'Transpond-alert', + 'Transpond-ID code'], + [255]+range(1, 4)) +FIVE_TONE_ALERT_TRANSPOND = zip(['Off', 'Alert tone', + 'Transpond', 'Transpond-ID code'], + [255]+range(1, 4)) + +BFM_BANDS = ['87.5-108MHz', '76.0-91.0MHz', '76.0-108.0MHz', '65.0-76.0MHz'] +BFM_STRIDE = ['100kHz', '50kHz'] + + +def piperead(pipe, amount): + """read some data, catch exceptions, validate length of data read""" + try: + d = pipe.read(amount) + except Exception as e: + raise errors.RadioError( + "Tried to read %d bytes, but got an exception: %s" % + (amount, repr(e))) + if d is None: + raise errors.RadioError( + "Tried to read %d bytes, but read operation returned ." % + (amount)) + if d is None or len(d) != amount: + raise errors.RadioError( + "Tried to read %d bytes, but got %d bytes instead." % + (amount, len(d))) + return d + + +def pipewrite(pipe, data): + """write some data, catch exceptions, validate length of data written""" + try: + n = pipe.write(data) + except Exception as e: + raise errors.RadioError( + "Tried to write %d bytes, but got an exception: %s." % + (len(data), repr(e))) + if n is None: + raise errors.RadioError( + "Tried to write %d bytes, but operation returned ." % + (len(data))) + if n != len(data): + raise errors.RadioError( + "Tried to write %d bytes, but wrote %d bytes instead." % + (len(data), n)) + + +def attempt_initial_handshake(pipe): + """try to do the initial handshake""" + pipewrite(pipe, HANDSHAKE_OUT) + x = piperead(pipe, len(HANDSHAKE_IN[0])) + if x in HANDSHAKE_IN: + return True + LOG.debug("Handshake failed: received: %s expected one of: %s" % + (repr(x), repr(HANDSHAKE_IN))) + return False + + +def initial_handshake(pipe, tries): + """do an initial handshake attempt up to tries times""" + x = False + for i in range(tries): + x = attempt_initial_handshake(pipe) + if x: + break + if not x: + raise errors.RadioError("Initial handshake failed all ten tries.") + + +def mk_writecommand(addr): + """makes a write command from an address specification""" + return pack('>cHc', b'W', addr, b'@') + + +def mk_readcommand(addr): + """makes a read command from an address specification""" + return pack('>cHc', b'R', addr, b'@') + + +def expect_ack(pipe): + x = piperead(pipe, 1) + if x != b'\x06': + LOG.debug( + "Did not get ACK. received: %s, expected: '\\x06'" % + repr(x)) + raise errors.RadioError("Did not get ACK when expected.") + + +def end_communications(pipe): + """tell the radio that we are done""" + pipewrite(pipe, b'E') + expect_ack(pipe) + + +def read_block(pipe, addr): + """read and return a chunk of data at specified address""" + r = mk_readcommand(addr) + w = mk_writecommand(addr) + pipewrite(pipe, r) + x = piperead(pipe, len(w)) + if x != w: + raise errors.RadioError("Received data not following protocol.") + block = piperead(pipe, BLOCKSIZE) + return block + + +def write_block(pipe, addr, block): + """write a chunk of data at specified address""" + w = mk_writecommand(addr) + pipewrite(pipe, w) + pipewrite(pipe, block) + expect_ack(pipe) + + +def show_progress(radio, blockaddr, upper, msg): + """relay read/write information to the user through the gui""" + if radio.status_fn: + status = chirp_common.Status() + status.cur = blockaddr + status.max = upper + status.msg = msg + radio.status_fn(status) + + +def do_download(radio): + """download from the radio to the memory map""" + initial_handshake(radio.pipe, 10) + memory = memmap.MemoryMap(b'\xff'*0x1000) + for blockaddr in range(LOWER_READ_BOUND, UPPER_READ_BOUND, BLOCKSIZE): + LOG.debug("Reading block "+str(blockaddr)) + block = read_block(radio.pipe, blockaddr) + memory.set(blockaddr, block) + show_progress(radio, blockaddr, UPPER_READ_BOUND, + "Reading radio memory... %04x" % blockaddr) + end_communications(radio.pipe) + return memory + + +def do_upload(radio): + """upload from the memory map to the radio""" + memory = radio.get_mmap() + initial_handshake(radio.pipe, 10) + for blockaddr in range(LOWER_WRITE_BOUND, UPPER_WRITE_BOUND, BLOCKSIZE): + LOG.debug("Writing block "+str(blockaddr)) + block = memory[blockaddr:blockaddr+BLOCKSIZE] + write_block(radio.pipe, blockaddr, block) + show_progress(radio, blockaddr, UPPER_WRITE_BOUND, + "Writing radio memory... % 04x" % blockaddr) + end_communications(radio.pipe) + + +def parse_tone(t): + """ + parse the tone (ctss, dtcs) part of the mmap + into more easily handled data types + """ + # [ mode, value, polarity ] + if int(t.high) == 0x3f and int(t.low) == 0xff: + return [None, None, None] + elif bool(t.digital): + t = ['DTCS', + (int(t.high) & 0x0f)*100 + + ((int(t.low) & 0xf0) >> 4)*10 + + (int(t.low) & 0x0f), + ['N', 'R'][bool(t.invert)]] + if t[1] not in chirp_common.DTCS_CODES: + return [None, None, None] + else: + t = ['Tone', + ((int(t.high) & 0xf0) >> 4)*100 + + (int(t.high) & 0x0f)*10 + + ((int(t.low) & 0xf0) >> 4) + + (int(t.low) & 0x0f)/10.0, + None] + if t[1] not in chirp_common.TONES: + return [None, None, None] + return t + + +def unparse_tone(t): + """parse tone data back into the format used by the radio""" + # [ mode, value, polarity ] + if t[0] == 'Tone': + tint = int(t[1]*10) + t0, tint = tint % 10, tint // 10 + t1, tint = tint % 10, tint // 10 + t2, tint = tint % 10, tint // 10 + high = (tint << 4) | t2 + low = (t1 << 4) | t0 + digital = False + invert = False + return digital, invert, high, low + elif t[0] == 'DTCS': + tint = int(t[1]) + t0, tint = tint % 10, tint // 10 + t1, tint = tint % 10, tint // 10 + high = tint + low = (t1 << 4) | t0 + digital = True + invert = t[2] == 'R' + return digital, invert, high, low + return None + + +def decode_halfbytes(data, mapping, length): + """ + construct a string from a datatype + where each half-byte maps to a character + """ + s = '' + for i in range(length): + if i & 1 == 0: + s += mapping[(int(data[i >> 1]) & 0xf0) >> 4] + else: + s += mapping[int(data[i >> 1]) & 0x0f] + return s + + +def encode_halfbytes(data, datapad, mapping, fillvalue, fieldlen): + """encode data from a string where each character maps to a half-byte""" + if len(data) & 1: + # pad to an even length + data += datapad + o = [fillvalue] * fieldlen + for i in range(0, len(data), 2): + v = (mapping.index(data[i]) << 4) | mapping.index(data[i+1]) + o[i >> 1] = v + return bytearray(o) + + +def decode_ffstring(data): + """decode a string delimited by 0xff""" + s = '' + for b in data: + if int(b) == 0xff: + break + s += chr(int(b)) + return s + + +def encode_ffstring(data, fieldlen): + """right-pad to specified length with 0xff bytes""" + extra = fieldlen-len(data) + if extra > 0: + data += '\xff'*extra + return bytearray(data) + + +def decode_dtmf(data, length): + """decode a field containing dtmf data into a string""" + if length == 0xff: + return '' + return decode_halfbytes(data, DTMF, length) + + +def encode_dtmf(data, length, fieldlen): + """encode a string containing dtmf characters into a data field""" + return encode_halfbytes(data, '0', DTMF, b'\xff', fieldlen) + + +def decode_5tone(data): + """decode a field containing 5-tone data into a string""" + if (int(data[2]) & 0x0f) != 0: + return '' + return decode_halfbytes(data, HEXADECIMAL, 5) + + +def encode_5tone(data, fieldlen): + """encode a string containing 5-tone characters into a data field""" + return encode_halfbytes(data, '0', HEXADECIMAL, b'\xff', fieldlen) + + +def decode_freq(data): + """decode frequency data for the broadcast fm radio memories""" + data_out = '' + if data[0] != 0xff: + data_out = chirp_common.format_freq( + int(decode_halfbytes(data, "0123456789", len(data)))*100000) + return data_out + + +def encode_freq(data, fieldlen): + """encode frequency data for the broadcast fm radio memories""" + data_out = bytearray('\xff')*fieldlen + if data != '': + data_out = encode_halfbytes((('%%0%di' % (fieldlen << 1)) % + int(chirp_common.parse_freq(data)/10)), + '', '0123456789', '', fieldlen) + return data_out + + +def sbyn(s, n): + """setting by name""" + return filter(lambda x: x.get_name() == n, s)[0] + + +# These helper classes provide a direct link between the value +# of the widget shown in the ui, and the setting in the memory +# map of the radio, lessening the need to write large chunks +# of code, first for populating the ui from the memory map, +# then secondly for parsing the values back. +# By supplying the memory map entry to the setting instance, +# it is possible to automatically 1) initialize the value of +# the setting, as well as 2) automatically update the memory +# value when the user changes it in the ui, without adding +# any code outside the class. +class MappedIntegerSettingValue(settings.RadioSettingValueInteger): + """" + Integer setting, with the possibility to add translation + functions between memory map <-> integer setting + """ + def __init__(self, val_mem, minval, maxval, step=1, + int_from_mem=lambda x: int(x), + mem_from_int=lambda x: x, + autowrite=True): + """ + val_mem - memory map entry for the value + minval - the minimum value allowed + maxval - maximum value allowed + step - value stepping + int_from_mem - function to convert memory entry to integer + mem_from_int - function to convert integer to memory entry + autowrite - automatically write the memory map entry + when the value is changed + """ + self._val_mem = val_mem + self._int_from_mem = int_from_mem + self._mem_from_int = mem_from_int + self._autowrite = autowrite + settings.RadioSettingValueInteger.__init__( + self, + minval, maxval, self._int_from_mem(val_mem), step) + + def set_value(self, x): + settings.RadioSettingValueInteger.set_value(self, x) + if self._autowrite: + self.write_mem() + + def write_mem(self): + if self.get_mutable() and self._mem_from_int is not None: + self._val_mem.set_value(self._mem_from_int( + settings.RadioSettingValueInteger.get_value(self))) + + +class MappedListSettingValue(settings.RadioSettingValueMap): + """Mapped list setting""" + def __init__(self, val_mem, options, autowrite=True): + """ + val_mem - memory map entry for the value + options - either a list of strings options to present, + mapped to integers 0...n + in the memory map entry, or a list of tuples + ("option description", memory map value) + int_from_mem - function to convert memory entry to integer + mem_from_int - function to convert integer to memory entry + autowrite - automatically write the memory map entry when + the value is changed + """ + self._val_mem = val_mem + self._autowrite = autowrite + if not isinstance(options[0], tuple): + options = zip(options, range(len(options))) + settings.RadioSettingValueMap.__init__( + self, + options, mem_val=int(val_mem)) + + def set_value(self, value): + settings.RadioSettingValueMap.set_value(self, value) + if self._autowrite: + self.write_mem() + + def write_mem(self): + if self.get_mutable(): + self._val_mem.set_value( + settings.RadioSettingValueMap.get_mem_val(self)) + + +class MappedCodedStringSettingValue(settings.RadioSettingValueString): + """ + generic base class for a number of mapped presented-as-strings + values which may need conversion between mem and string, + and may store a length value in a separate mem field + """ + def __init__(self, val_mem, len_mem, min_length, max_length, + charset=ASCIIPART, padchar=' ', autowrite=True, + str_from_mem=lambda mve, lve: str(mve[0:int(lve)]), + mem_val_from_str=lambda s, fl: s[0:fl], + mem_len_from_int=lambda l: l): + """ + val_mem - memory map entry for the value + len_mem - memory map entry for the length (or None) + min_length - length that the string will be right-padded to + max_length - maximum length of the string, set as maxlength + for the RadioSettingValueString + charset - the allowed charset + padchar - the character that will be used to pad short + strings, if not in the charset, charset[0] is used + autowrite - automatically call write_mem when the ui value + change + str_from_mem - function to convert from memory entry to string + value, form: + func(value_entry, length_entry or none) -> string + mem_val_from_str - function to convert from string value to + memory-fitting value, form: + func(string, value_entry_length) -> + value to store in value entry + mem_len_from_int - function to convert from string length to + memory-fitting value, form: + func(stringlength) -> value to store in length entry + """ + self._min_length = min_length + self._val_mem = val_mem + self._len_mem = len_mem + self._padchar = padchar + if padchar not in charset: + self._padchar = charset[0] + self._autowrite = autowrite + self._str_from_mem = str_from_mem + self._mem_val_from_str = mem_val_from_str + self._mem_len_from_int = mem_len_from_int + settings.RadioSettingValueString.__init__( + self, + 0, max_length, + self._str_from_mem(self._val_mem, self._len_mem), + charset=charset, autopad=False) + + def set_value(self, value): + """ + Set the value of the string, pad if below minimum length, + unless it's '' to provide a distinction between + uninitialized/reset data and needs-to-be-padded data + """ + while len(value) < self._min_length and len(value) != 0: + value += self._padchar + settings.RadioSettingValueString.set_value(self, value) + if self._autowrite: + self.write_mem() + + def write_mem(self): + """update the memory""" + if not self.get_mutable() or self._mem_val_from_str is None: + return + v = self.get_value() + l = len(v) + self._val_mem.set_value(self._mem_val_from_str(v, len(self._val_mem))) + if self._len_mem is not None and self._mem_len_from_int is not None: + self._len_mem.set_value(self._mem_len_from_int(l)) + + +class MappedFFStringSettingValue(MappedCodedStringSettingValue): + """ + Mapped string setting, tailored for the puxing px888k, + which uses 0xff terminated strings. + """ + def __init__(self, val_mem, min_length, max_length, + charset=ASCIIPART, padchar=' ', autowrite=True): + MappedCodedStringSettingValue.__init__( + self, + val_mem, None, min_length, max_length, + charset=charset, padchar=padchar, autowrite=autowrite, + str_from_mem=lambda mve, lve: decode_ffstring(mve), + mem_val_from_str=lambda s, fl: encode_ffstring(s, fl), + mem_len_from_int=None) + + +class MappedDTMFStringSettingValue(MappedCodedStringSettingValue): + """ + Mapped string setting, tailored for the puxing px888k + field pairs (value and length) storing DTMF codes + """ + def __init__(self, val_mem, len_mem, min_length, max_length, + autowrite=True): + MappedCodedStringSettingValue.__init__( + self, + val_mem, len_mem, min_length, max_length, + charset=DTMF, padchar='0', autowrite=autowrite, + str_from_mem=lambda mve, lve: decode_dtmf(mve, lve), + mem_val_from_str=lambda s, fl: encode_dtmf(s, len(s), fl)) + + +class MappedFiveToneStringSettingValue(MappedCodedStringSettingValue): + """ + Mapped string setting, tailored for the puxing px888k + fields storing 5-Tone codes + """ + def __init__(self, val_mem, autowrite=True): + MappedCodedStringSettingValue.__init__( + self, + val_mem, None, 0, 5, charset=HEXADECIMAL, padchar='0', + autowrite=autowrite, + str_from_mem=lambda mve, lve: decode_5tone(mve), + mem_val_from_str=lambda s, fl: encode_5tone(s, fl), + mem_len_from_int=None) + + +class MappedFreqStringSettingValue(MappedCodedStringSettingValue): + """ + Mapped string setting, tailored for the puxing px888k + fields for the broadcast FM radio frequencies + """ + def __init__(self, val_mem, autowrite=True): + MappedCodedStringSettingValue.__init__( + self, + val_mem, None, 0, 128, charset=ASCIIPART, padchar=' ', + autowrite=autowrite, + str_from_mem=lambda mve, lve: decode_freq(mve), + mem_val_from_str=lambda s, fl: encode_freq(s, fl)) + + +# These functions lessen the amount of boilerplate on the form +# x = RadioSetting("AAA", "BBB", SomeKindOfRadioSettingValue( ... )) +# to +# x = some_kind_of_setting("AAA", "BBB", ... ) +def integer_setting(k, n, *args, **kwargs): + return settings.RadioSetting( + k, n, + MappedIntegerSettingValue(*args, **kwargs)) + + +def list_setting(k, n, *args, **kwargs): + return settings.RadioSetting( + k, n, + MappedListSettingValue(*args, **kwargs)) + + +def ff_string_setting(k, n, *args, **kwargs): + return settings.RadioSetting( + k, n, + MappedFFStringSettingValue(*args, **kwargs)) + + +def dtmf_string_setting(k, n, *args, **kwargs): + return settings.RadioSetting( + k, n, + MappedDTMFStringSettingValue(*args, **kwargs)) + + +def five_tone_string_setting(k, n, *args, **kwargs): + return settings.RadioSetting( + k, n, + MappedFiveToneStringSettingValue(*args, **kwargs)) + + +def frequency_setting(k, n, *args, **kwargs): + return settings.RadioSetting( + k, n, + MappedFreqStringSettingValue(*args, **kwargs)) + + +@directory.register +class Puxing_PX888K_Radio(chirp_common.CloneModeRadio): + """Puxing PX-888K""" + VENDOR = "Puxing" + MODEL = "PX-888K" + BAUD_RATE = 9600 + + @classmethod + def match_model(cls, filedata, filename): + if len(filedata) == UPPER_READ_BOUND: + if filedata[FILE_MAGIC[0]:FILE_MAGIC[1]] == FILE_MAGIC[2]: + return True + else: + LOG.debug("The data at 0x0c40 does not match the PX-888K") + else: + LOG.debug("The file size does not match.") + return False + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank_index = False + rf.has_dtcs = True + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_mode = True + rf.has_offset = True + rf.has_name = True + rf.has_bank = False + rf.has_bank_names = False + rf.has_tuning_step = False + rf.has_cross = True + rf.has_infinite_number = False + rf.has_nostep_tuning = False + rf.has_comment = False + rf.has_settings = True + if SUPPORT_NONSPLIT_DUPLEX_ONLY: + rf.can_odd_split = False + else: + rf.can_odd_split = True + + rf.valid_modes = MODES + rf.valid_tmodes = TONE_MODES + rf.valid_duplexes = DUPLEX_MODES + rf.valid_bands = BANDS + rf.valid_skips = SKIP_MODES + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = ASCIIPART + rf.valid_name_length = 6 + rf.valid_cross_modes = CROSS_MODES + rf.memory_bounds = (1, 128) + rf.valid_special_chans = SPECIAL_CHANNELS.keys() + rf.valid_tuning_steps = [5.0, 6.25, 10.0, 12.5, 25.0] + return rf + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_out(self): + do_upload(self) + + def _set_sane_defaults(self, data): + # thanks thayward! + data.set_raw(SANE_MEMORY_DEFAULT) + + def _uninitialize(self, data, n): + if isinstance(data, bitwise.arrayDataElement): + data.set_value(b"\xff"*n) + else: + data.set_raw(b"\xff"*n) + + def _get_memory_structs(self, number): + """ + fetch the correct data structs no matter + if its regular or special channels, + no matter if they're referred by name or channel index + """ + index = 2501 + i = -42 + designator = 'INVALID' + isregular = False + iscall = False + isvfo = False + _data = None + _name = None + _present = None + _priority = None + if number in SPECIAL_NUMBERS.keys(): + index = number + # speical by index + designator = SPECIAL_NUMBERS[number] + elif number in SPECIAL_CHANNELS.keys(): + # special by name + index = SPECIAL_CHANNELS[number] + designator = number + elif number > 0: + # regular by number + index = number + designator = number + + if index < 0: + isvfo = True + _data = self._memobj.mem.vfo_data[index+2] + elif index == 0: + iscall = True + _data = self._memobj.mem.call_channel + elif index > 0: + isregular = True + i = number - 1 + _data = self._memobj.mem.channel_memory.data[i] + _name = self._memobj.mem.channel_memory.names[i].entry + _present = self._memobj.mem.channel_memory.present[ + (i & 0x78) | (7-(i & 0x07))] + _priority = self._memobj.mem.channel_memory.priority[ + (i & 0x78) | (7-(i & 0x07))] + + if _data == bytearray(0xff)*16: + self._set_sane_defaults(_data) + + return (index, designator, + _data, _name, _present, _priority, + isregular, isvfo, iscall) + + def get_raw_memory(self, number): + x = self._get_memory_structs(number) + return repr(x[2]) + + def get_memory(self, number): + mem = chirp_common.Memory() + (index, designator, + _data, _name, _present, _priority, + isregular, isvfo, iscall) = self._get_memory_structs(number) + + mem.number = index + mem.extd_number = designator + + # handle empty channels + if isregular: + if bool(_present): + mem.empty = False + mem.name = str(decode_ffstring(_name)) + mem.skip = SKIP_MODES[1-int(_priority)] + else: + mem.empty = True + mem.name = '' + return mem + else: + mem.empty = False + mem.name = '' + + # get frequency data + mem.freq = int(_data.rx_freq)*10 + mem.offset = int(_data.tx_freq)*10 + + # interpret frequency data + # only the vfo channels support duplex, + # memory channels operate in split mode all the time + if isvfo: + mem.duplex = DUPLEX_MODES[int(_data.duplex_sign)] + if mem.duplex == '-': + mem.offset = mem.freq - mem.offset + elif mem.duplex == '': + mem.offset = 0 + elif mem.duplex == '+': + mem.offset = mem.offset - mem.freq + else: + if mem.freq == mem.offset: + mem.duplex = '' + mem.offset = 0 + elif SUPPORT_NONSPLIT_DUPLEX_ONLY or \ + SUPPORT_SPLIT_BUT_DEFAULT_TO_NONSPLIT_ALWAYS: + if mem.freq > mem.offset: + mem.offset = mem.freq - mem.offset + mem.duplex = '-' + elif mem.freq < mem.offset: + mem.offset = mem.offset - mem.freq + mem.duplex = '+' + else: + mem.duplex = 'split' + + # get tone data + txtone = parse_tone(_data.tone[0]) + rxtone = parse_tone(_data.tone[1]) + chirp_common.split_tone_decode(mem, txtone, rxtone) + +###################################################################### + if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES: + # override certain settings based on flags + # that we have set in junk areas of the memory + # or basically, we BELIEVE this to be junk memory, + # hence why it's experimental and dangerous + if bool(_data.experimental_cross_mode_indicator) is False: + if mem.tmode == 'Tone': + mem.cross_mode = 'Tone->' + elif mem.tmode == 'TSQL': + mem.cross_mode = 'Tone->Tone' + elif mem.tmode == 'DTCS': + mem.cross_mode = 'DTCS->DTCS' + mem.tmode = 'Cross' +###################################################################### + + # transmit mode and power level + mem.mode = MODES[bool(_data.modulation_width)] + mem.power = POWER_LEVELS[_data.txpower] + + # extra channel settings + mem.extra = settings.RadioSettingGroup( + "extra", + "extra", + list_setting("Busy channel lockout", + "BCL", + _data.bcl, + BCL_MODES), + list_setting("Swap transmit and receive frequencies", + "Tx Rx freq swap", + _data.txrx_reverse, + OFF_ON), + list_setting("Use compander", + "Use compander", + _data.compander, + OFF_ON), + list_setting("Use scrambler", "Use scrambler", + _data.use_scrambler, + NO_YES), + list_setting("Scrambler selection", + "Voice Scrambler", + _data.scrambler_type, + SCRAMBLER_MODES), + list_setting("Send ID code before and/or after transmitting", + "PTT ID", + _data.ptt_id_edge, + PTT_ID_EDGES), + list_setting("Optional signal before/after transmission, " + + "this setting overrides the PTT ID setting.", + "Opt Signal", + _data.opt_signal, + OPTSIGN_MODES)) + +###################################################################### + if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES: + # override certain settings based on flags + # that we have set in junk areas of the memory + # or basically, we BELIEVE this to be junk memory, + # hence why it's experimental and dangerous + if bool(_data.experimental_duplex_mode_indicator) is False: + # if this flag is set, this means that we in the gui + # have set the duplex mode to something + # the channel does not really support, + # such as split modes for vfo channels, + # and non-split modes for the memory channels + mem.duplex = DUPLEX_MODES[int(_data.duplex_sign)] + mem.freq = int(_data.rx_freq)*10 + mem.offset = int(_data.tx_freq)*10 + if isvfo: + # we want split, so we have to reconstruct it + # from -/0/+ modes + if mem.duplex == '-': + mem.offset = mem.freq - mem.offset + elif mem.duplex == '': + mem.offset = mem.freq + elif mem.duplex == '+': + mem.offset = mem.freq + mem.offset + mem.duplex = 'split' + else: + # we want -/0/+, so we have to reconstruct it + # from split modes + if mem.freq > mem.offset: + mem.offset = mem.freq - mem.offset + mem.duplex = '-' + elif mem.freq < mem.offset: + mem.offset = mem.offset - mem.freq + mem.duplex = '+' + else: + mem.offset = 0 + mem.duplex = '' +###################################################################### + + return mem + + def set_memory(self, mem): + (index, designator, + _data, _name, _present, _priority, + isregular, isvfo, iscall) = self._get_memory_structs(mem.number) + mem.number = index + mem.extd_number = designator + + # handle empty channels + if mem.empty: + if isregular: + _present.set_value(False) + _priority.set_value(False) + self._uninitialize(_data, 16) + self._uninitialize(_name, 6) + else: + raise errors.InvalidValueError( + "Can't remove CALL and/or VFO channels!") + return + + # handle regular channel stuff like name and present+priority flags + if isregular: + if not bool(_present): + self._set_sane_defaults(_data) + _name.set_value( + encode_ffstring(self.filter_name(mem.name), len(_name))) + _present.set_value(True) + _priority.set_value(1-SKIP_MODES.index(mem.skip)) + + # frequency data + rxf = int(mem.freq/10) + txf = int(mem.offset/10) + + _data.rx_freq.set_value(rxf) + + if isvfo: + # fake split modes on write, for channels + # that do not support it, which are some + # (the two vfo channels) + if mem.duplex == 'split': + for band in BANDS: + rb = mem.freq in range(band[0], band[1]) + tb = mem.offset in range(band[0], band[1]) + if rb != tb: + raise errors.InvalidValueError( + "VFO frequencies should be on the same band") + if rxf < txf: + _data.duplex_sign.set_value(1) + _data.tx_freq.set_value(txf - rxf) + elif rxf > txf: + _data.duplex_sign.set_value(2) + _data.tx_freq.set_value(rxf-txf) + else: + _data.duplex_sign.set_value(0) + _data.tx_freq.set_value(0) + else: + _data.duplex_sign.set_value(DUPLEX_MODES.index(mem.duplex)) + _data.tx_freq.set_value(txf) + +###################################################################### + if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES: + if mem.duplex == 'split': + _data.experimental_duplex_mode_indicator.set_value(0) + else: + _data.experimental_duplex_mode_indicator.set_value(1) +###################################################################### + + else: + # fake duplex modes on write, for channels + # that do not support it, which are most + # (all the memory channels) + if mem.duplex == '' or mem.duplex is None: + _data.tx_freq.set_value(rxf) + elif mem.duplex == '+': + _data.tx_freq.set_value(rxf + txf) + elif mem.duplex == '-': + _data.tx_freq.set_value(rxf - txf) + else: + _data.tx_freq.set_value(txf) + +###################################################################### + if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES: + if mem.duplex != 'split': + _data.experimental_duplex_mode_indicator.set_value(0) + else: + _data.experimental_duplex_mode_indicator.set_value(1) +###################################################################### + + # tone data + tonedata = chirp_common.split_tone_encode(mem) + for i in range(2): + dihl = unparse_tone(tonedata[i]) + if dihl is not None: + _data.tone[i].digital.set_value(dihl[0]) + _data.tone[i].invert.set_value(dihl[1]) + _data.tone[i].high.set_value(dihl[2]) + _data.tone[i].low.set_value(dihl[3]) + else: + _data.tone[i].digital.set_value(1) + _data.tone[i].invert.set_value(1) + _data.tone[i].high.set_value(0x3f) + _data.tone[i].low.set_value(0xff) + +###################################################################### + if ENABLE_DANGEROUS_EXPERIMENTAL_FEATURES: + if mem.tmode == 'Cross' and mem.cross_mode in ['Tone->', + 'Tone->Tone', + 'DTCS->DTCS']: + _data.experimental_cross_mode_indicator.set_value(0) + else: + _data.experimental_cross_mode_indicator.set_value(1) +###################################################################### + + # transmit mode and power level + _data.modulation_width.set_value(MODES.index(mem.mode)) + if str(mem.power) == 'High': + _data.txpower.set_value(1) + else: + _data.txpower = 0 + + def get_settings(self): + _model = self._memobj.mem.model_information + _settings = self._memobj.mem.opt_settings + _ptt_id_data = self._memobj.mem.ptt_id_data + _msk_settings = self._memobj.mem.msk_settings + _dtmf_settings = self._memobj.mem.dtmf_settings + _5tone_settings = self._memobj.mem.five_tone_settings + _broadcast = self._memobj.mem.fm_radio + + # for safety reasons we are showing these as read-only + model_unit_settings = [ + integer_setting("vhflo", "VHF lower bound", + _model.band_limits[0].lower_freq, + 134, 176, + int_from_mem=lambda x:int(int(x)/10), + mem_from_int=None), + integer_setting("vhfhi", "VHF upper bound", + _model.band_limits[0].upper_freq, + 134, 176, + int_from_mem=lambda x:int(int(x)/10), + mem_from_int=None), + integer_setting("uhflo", "UHF lower bound", + _model.band_limits[1].lower_freq, + 400, 480, + int_from_mem=lambda x:int(int(x)/10), + mem_from_int=None), + integer_setting("uhfhi", "UHF upper bound", + _model.band_limits[1].upper_freq, + 400, 480, + int_from_mem=lambda x:int(int(x)/10), + mem_from_int=None), + ff_string_setting("model", "Model string", + _model.model_string, + 0, 6) + + ] + for s in model_unit_settings: + s.value.set_mutable(False) + model_unit_settings.append(ff_string_setting( + "info", "Unit Information", + self._memobj.mem.radio_information_string, + 0, 16)) + + # tx/rx related settings + radio_channel_settings = [ + list_setting("vfostep", "VFO step size", + _settings.vfo_step, + VFO_STRIDE), + list_setting("abwatch", "Main watch", + _settings.main_watch, + AB), + list_setting("watchmade", "Watch mode", + _settings.main_watch, + WATCH_MODES), + list_setting("amode", "A mode", + _settings.workmode_a, + AB_MODES), + list_setting("bmode", "B mode", + _settings.workmode_b, + AB_MODES), + integer_setting("achan", "A channel index", + _settings.channel_a, + 1, 128, + int_from_mem=lambda i:i+1, + mem_from_int=lambda i:i-1), + integer_setting("bchan", "B channel index", + _settings.channel_b, + 1, 128, + int_from_mem=lambda i:i+1, + mem_from_int=lambda i:i-1), + integer_setting("pchan", "Priority channel index", + _settings.priority_channel, + 1, 128, int_from_mem=lambda i:i+1, + mem_from_int=lambda i:i-1), + list_setting("cactive", "Call channel active?", + _settings.call_channel_active, + NO_YES), + list_setting("scanm", "Scan mode", + _settings.scan_mode, + SCAN_MODES), + list_setting("swait", "Wait time", + _settings.wait_time, + WAIT_TIMES), + # it is unclear what this option below does, + # possibly squelch tail elimination? + list_setting("tail", "Relay without disable tail (?)", + _settings.relay_without_disable_tail, + NO_YES), + list_setting("batsav", "Battery saving mode", + _settings.battery_save, + OFF_ON), + ] + + # user interface related settings + interface_settings = [ + list_setting("sidehold", "Side button hold action", + _settings.side_button_hold_mode, + BUTTON_MODES), + list_setting("sideclick", "Side button click action", + _settings.side_button_click_mode, + BUTTON_MODES), + list_setting("bootmt", "Boot message type", + _settings.boot_message_mode, + BOOT_MESSAGE_TYPES), + ff_string_setting("bootm", "Boot message", + _settings.boot_message, + 0, 6), + list_setting("beep", "Key beep", + _settings.key_beep, + OFF_ON), + list_setting("talkback", "Menu talkback", + _settings.voice_announce, + TALKBACK), + list_setting("sidetone", "DTMF sidetone", + _settings.dtmf_sidetone, + OFF_ON), + list_setting("roger", "Roger beep", + _settings.use_roger_beep, + ROGER_BEEP), + list_setting("backlm", "Backlight mode", + _settings.backlight_mode, + BACKLIGHT_MODES), + list_setting("backlc", "Backlight color", + _settings.backlight_color, + BACKLIGHT_COLORS), + integer_setting("squelch", "Squelch level", + _settings.squelch_level, + 0, 9), + list_setting("voxg", "Vox gain", + _settings.vox_gain, + VOX_GAIN), + list_setting("voxd", "Vox delay", + _settings.vox_delay, + VOX_DELAYS), + list_setting("txal", "Trinsmit time alarm", + _settings.tx_timeout, + TRANSMIT_ALARMS), + ] + + # settings related to tone/data sending and interpretation + data_general_settings = [ + list_setting("disptt", "Display PTT ID", + _settings.dis_ptt_id, + NO_YES), + list_setting("pttidt", "PTT ID signal type", + _settings.ptt_id_type, + DATA_MODES) + ] + + data_msk_settings = [ + ff_string_setting("bot", "MSK PTT ID (BOT)", + _ptt_id_data[0].entry, + 0, 6, autowrite=False), + ff_string_setting("eot", "MSK PTT ID (EOT)", + _ptt_id_data[1].entry, + 0, 6, autowrite=False), + ff_string_setting("id", "MSK ID code", + _msk_settings.id_code, + 0, 4, charset=HEXADECIMAL), + list_setting("mskr", "MSK reverse", + _settings.msk_reverse, + NO_YES) + ] + + data_dtmf_settings = [ + dtmf_string_setting("bot", "DTMF PTT ID (BOT)", + _ptt_id_data[0].entry, + _ptt_id_data[0].length, + 0, 8, autowrite=False), + dtmf_string_setting("eot", "DTMF PTT ID (EOT)", + _ptt_id_data[1].entry, + _ptt_id_data[1].length, + 0, 8, autowrite=False), + dtmf_string_setting("id", "DTMF ID code", + _dtmf_settings.id_code, + _dtmf_settings.id_code_length, + 3, 8), + + integer_setting("time", "Digit time (ms)", + _dtmf_settings.timing.digit_length, + 50, 200, step=10, + int_from_mem=lambda x:x*10, + mem_from_int=lambda x:int(x/10)), + integer_setting("pause", "Inter digit time (ms)", + _dtmf_settings.timing.digit_length, + 50, 200, step=10, + int_from_mem=lambda x:x*10, + mem_from_int=lambda x:int(x/10)), + integer_setting("time1", "First digit time (ms)", + _dtmf_settings.timing.digit_length, + 50, 200, step=10, + int_from_mem=lambda x:x*10, + mem_from_int=lambda x:int(x/10)), + integer_setting("pause1", "First digit delay (ms)", + _dtmf_settings.timing.digit_length, + 100, 1000, step=50, + int_from_mem=lambda x:x*50, + mem_from_int=lambda x:int(x/50)), + + list_setting("arst", "Auto reset time", + _dtmf_settings.reset_time, + DTMF_TONE_RESET_TIME), + list_setting("grp", "Group code", + _dtmf_settings.group_code, + DTMF_GROUPS), + dtmf_string_setting("stunt", "TX Stun code", + _dtmf_settings.tx_stun_code, + _dtmf_settings.tx_stun_code_length, + 3, 8), + dtmf_string_setting("cstunt", "TX Stun cancel code", + _dtmf_settings.cancel_tx_stun_code, + _dtmf_settings.cancel_tx_stun_code_length, + 3, 8), + dtmf_string_setting("stunrt", "RX/TX Stun code", + _dtmf_settings.rxtx_stun_code, + _dtmf_settings.rxtx_stun_code_length, + 3, 8), + dtmf_string_setting("cstunrt", "RX/TX Stun cancel code", + _dtmf_settings.cancel_rxtx_stun_code, + _dtmf_settings.cancel_rxtx_stun_code_length, + 3, 8), + list_setting("altr", "Alert/Transpond", + _dtmf_settings.alert_transpond, + DTMF_ALERT_TRANSPOND), + ] + + data_5tone_settings = [ + five_tone_string_setting("bot", + "5-Tone PTT ID (BOT)", + _ptt_id_data[0].entry, + autowrite=False), + five_tone_string_setting("eot", + "5-Tone PTT ID (EOT)", + _ptt_id_data[1].entry, + autowrite=False), + five_tone_string_setting("id", + "5-tone ID code", + _5tone_settings.id_code), + list_setting("arst", "Auto reset time", + _5tone_settings.reset_time, + TONE_RESET_TIME), + five_tone_string_setting("stunt", + "TX Stun code", + _5tone_settings.tx_stun_code), + five_tone_string_setting("cstunt", + "TX Stun cancel code", + _5tone_settings.cancel_tx_stun_code), + five_tone_string_setting("stunrt", + "RX/TX Stun code", + _5tone_settings.rxtx_stun_code), + five_tone_string_setting("cstunrt", + "RX/TX Stun cancel code", + _5tone_settings.cancel_rxtx_stun_code), + list_setting("altr", "Alert/Transpond", + _5tone_settings.alert_transpond, + FIVE_TONE_ALERT_TRANSPOND), + list_setting("std", "5-Tone standard", + _5tone_settings.tone_standard, + FIVE_TONE_STANDARDS), + ] + for i in range(4): + s = ['z1', 'z2', 'c1', 'ct'][i] + l = FIVE_TONE_STANDARDS[i] + data_5tone_settings.append( + settings.RadioSettingGroup( + s, '%s settings' % l, + integer_setting("%speriod" % s, "%s Period (ms)" % l, + _5tone_settings.tone_settings[i].period, + 20, 255), + list_setting("%sgrp" % s, "%s Group code" % l, + _5tone_settings.tone_settings[i].group_code, + HEXADECIMAL), + list_setting("%srpt" % s, "%s Repeat code" % l, + _5tone_settings.tone_settings[i].repeat_code, + HEXADECIMAL))) + + data_msk_call_list = [] + data_dtmf_call_list = [] + data_5tone_call_list = [] + for i in range(9): + j = i+1 + data_msk_call_list.append( + ff_string_setting("ce%d" % i, + "MSK call entry %d" % j, + _msk_settings.phone_book[i].entry, + 0, 4, + charset=HEXADECIMAL)) + + data_dtmf_call_list.append( + dtmf_string_setting("ce%d" % i, + "DTMF call entry %d" % j, + _dtmf_settings.phone_book[i].entry, + _dtmf_settings.phone_book[i].length, + 0, 10)) + + data_5tone_call_list.append( + five_tone_string_setting("ce%d" % i, + "5-Tone call entry %d" % j, + _5tone_settings.phone_book[i].entry)), + + data_settings = data_general_settings + data_settings.extend([ + settings.RadioSettingGroup("MSK_s", "MSK settings", + *data_msk_settings), + settings.RadioSettingGroup("MSK_c", "MSK call list", + *data_msk_call_list), + settings.RadioSettingGroup("DTMF_s", "DTMF settings", + *data_dtmf_settings), + settings.RadioSettingGroup("DTMF_c", "DTMF call list", + *data_dtmf_call_list), + settings.RadioSettingGroup("5-Tone_s", "5-tone settings", + *data_5tone_settings), + settings.RadioSettingGroup("5-Tone_c", "5-tone call list", + *data_5tone_call_list) + ]) + + # settings related to the various ways the radio can be locked down + locking_settings = [ + list_setting("autolock", "Automatic timed keypad lock", + _settings.auto_keylock, + OFF_ON), + list_setting("lockon", "Current status of keypad lock", + _settings.keypad_lock, + INACTIVE_ACTIVE), + list_setting("nokeypad", "Disable keypad", + _settings.allow_keypad, + YES_NO), + list_setting("rxstun", "Disable receiver (rx stun)", + _settings.rx_stun, + NO_YES), + list_setting("txstun", "Disable transmitter (tx stun)", + _settings.tx_stun, + NO_YES), + ] + + # broadcast fm radio settings + broadcast_settings = [ + list_setting("band", "Frequency interval", + _broadcast.receive_range, + BFM_BANDS), + list_setting("stride", "VFO step", + _broadcast.channel_stepping, + BFM_STRIDE), + frequency_setting("vfo", "VFO frequency (MHz)", + _broadcast.vfo_freq) + ] + + for i in range(10): + broadcast_settings.append( + frequency_setting("bcd%d" % i, "Memory %d frequency" % i, + _broadcast.memory[i].entry)) + + return settings.RadioSettings( + settings.RadioSettingGroup("model", "Model/Unit information", + *model_unit_settings), + settings.RadioSettingGroup("radio", "Radio/Channel settings", + *radio_channel_settings), + settings.RadioSettingGroup("interface", "Interface", + *interface_settings), + settings.RadioSettingGroup("data", "Data", + *data_settings), + settings.RadioSettingGroup("locking", "Locking", + *locking_settings), + settings.RadioSettingGroup("broadcast", + "Broadcast FM radio settings", + *broadcast_settings) + ) + + def set_settings(self, s, parent=''): + # The helper classes take care of all settings except these below, + # since it is a single instance of settings having interdependencies, + # i.e., which value that gets written to the memory depends on + # the value of another setting. The value of the ptt id type setting + # decides which of the msk/dtmf/5-tone ptt id strings are + # actually written to memory. + ds = sbyn(s, 'data') + idts = sbyn(ds, 'pttidt').value + idtv = idts.get_value() + cs = sbyn(ds, idtv+'_s') + tss = [sbyn(cs, e).value for e in ['bot', 'eot']] + + for ts in tss: + ts.write_mem() diff --git a/chirp/drivers/radioddity_r2.py b/chirp/drivers/radioddity_r2.py new file mode 100644 index 0000000..ca2a13e --- /dev/null +++ b/chirp/drivers/radioddity_r2.py @@ -0,0 +1,622 @@ +# Copyright August 2018 Klaus Ruebsam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings, \ + RadioSettingValueString + +LOG = logging.getLogger(__name__) + +# memory map +# 0000 copy of channel 16: 0100 - 010F +# 0010 Channel 1 +# 0020 Channel 2 +# 0030 Channel 3 +# 0040 Channel 4 +# 0050 Channel 5 +# 0060 Channel 6 +# 0070 Channel 7 +# 0080 Channel 8 +# 0090 Channel 9 +# 00A0 Channel 10 +# 00B0 Channel 11 +# 00C0 Channel 12 +# 00D0 Channel 13 +# 00E0 Channel 14 +# 00F0 Channel 15 +# 0100 Channel 16 +# 03C0 various settings + +# the last three bytes of every channel are identical +# to the first three bytes of the next channel in row. +# However it will automatically be filled by the radio itself + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 unknown1:1, + compand:1, + scramb:1, + scanadd:1, + power:1, + mode:1, + unknown2:1, + bclo:1; + u8 unknown3[3]; +} memory[16]; + +#seekto 0x03C0; +struct { + u8 unknown3c08:1, + scanmode:1, + unknown3c06:1, + unknown3c05:1, + voice:2, + save:1, + beep:1; + u8 squelch; + u8 unknown3c2; + u8 timeout; + u8 voxgain; + u8 specialcode; + u8 unknown3c6; + u8 voxdelay; +} settings; + +""" + +CMD_ACK = "\x06" +CMD_STX = "\x02" +CMD_ENQ = "\x05" + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50), + chirp_common.PowerLevel("High", watts=3.00)] +TIMEOUT_LIST = ["Off"] + ["%s seconds" % x for x in range(30, 330, 30)] +SCANMODE_LIST = ["Carrier", "Timer"] +VOICE_LIST = ["Off", "Chinese", "English"] +VOX_LIST = ["Off"] + ["%s" % x for x in range(1, 9)] +VOXDELAY_LIST = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"] +MODE_LIST = ["WFM", "NFM"] + +TONES = chirp_common.TONES +DTCS_CODES = chirp_common.DTCS_CODES + +SETTING_LISTS = { + "tot": TIMEOUT_LIST, + "scanmode": SCANMODE_LIST, + "voice": VOICE_LIST, + "vox": VOX_LIST, + "voxdelay": VOXDELAY_LIST, + "mode": MODE_LIST, + } + +VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" + + +def _r2_enter_programming_mode(radio): + serial = radio.pipe + + magic = "TYOGRAM" + exito = False + serial.write(CMD_STX) + for i in range(0, 5): + for j in range(0, len(magic)): + serial.write(magic[j]) + ack = serial.read(1) + if ack == CMD_ACK: + exito = True + break + + # check if we had EXITO + if exito is False: + msg = "The radio did not accept program mode after five tries.\n" + msg += "Check your interface cable and power cycle your radio." + raise errors.RadioError(msg) + + try: + serial.write(CMD_STX) + ident = serial.read(8) + except: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + # No idea yet what the next 7 bytes stand for + # as long as they start with ACK we are fine + if not ident.startswith(CMD_ACK): + _r2_exit_programming_mode(radio) + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + # the next 6 bytes represent the 6 digit password + # they are somehow coded where '1' becomes x01 and 'a' becomes x25 + try: + serial.write(CMD_ENQ) + ack = serial.read(6) + except: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + # we will only read if no password is set + if ack != "\xFF\xFF\xFF\xFF\xFF\xFF": + _r2_exit_programming_mode(radio) + raise errors.RadioError("Radio is password protected") + try: + serial.write(CMD_ACK) + ack = serial.read(6) + + except: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio 2") + + if ack != CMD_ACK: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode 2") + + +def _r2_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write(CMD_ACK) + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _r2_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + for j in range(0, len(cmd)): + serial.write(cmd[j]) + + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + _r2_exit_programming_mode(radio) + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + _r2_exit_programming_mode(radio) + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _r2_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing block %04x..." % (block_addr)) + LOG.debug(util.hexprint(cmd + data)) + + try: + for j in range(0, len(cmd)): + serial.write(cmd[j]) + for j in range(0, len(data)): + serial.write(data[j]) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + _r2_exit_programming_mode(radio) + raise errors.RadioError("Failed to send block " + "%04x to radio" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _r2_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio._block_size): + status.cur = addr + radio._block_size + radio.status_fn(status) + + block = _r2_read_block(radio, addr, radio._block_size) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + data += radio.MODEL.ljust(8) + + _r2_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _r2_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr, block_size in radio._ranges: + for addr in range(start_addr, end_addr, block_size): + status.cur = addr + block_size + radio.status_fn(status) + _r2_write_block(radio, addr, block_size) + + _r2_exit_programming_mode(radio) + + +@directory.register +class RadioddityR2Radio(chirp_common.CloneModeRadio): + """Radioddity R2""" + VENDOR = "Radioddity" + MODEL = "R2" + BAUD_RATE = 9600 + + # definitions on how to read StartAddr EndAddr BlockZize + _ranges = [ + (0x0000, 0x01F8, 0x08), + (0x01F8, 0x03F0, 0x08) + ] + _memsize = 0x03F0 + # never read more than 8 bytes at once + _block_size = 0x08 + # frequency range is 400-470MHz + _range = [400000000, 470000000] + # maximum 16 channels + _upper = 16 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.has_name = False + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.can_odd_split = False + # FIXME: Is this right? The get_memory() has no checking for + # deleted memories, but set_memory() used to reference a missing + # variable likely copied from another driver + rf.can_delete = False + rf.valid_modes = MODE_LIST + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = [ + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "Tone->Tone", + "->DTCS", + "DTCS->", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = [] + rf.valid_bands = [self._range] + rf.memory_bounds = (1, self._upper) + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0] + return rf + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + # to set the vars on the class to the correct ones + + def sync_in(self): + """Download from radio""" + try: + data = do_download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = data + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + do_upload(self) + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_memory(self, number): + bitpos = (1 << ((number - 1) % 8)) + bytepos = ((number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + + mem.number = number + + mem.freq = int(_mem.rx_freq) * 10 + + txfreq = int(_mem.tx_freq) * 10 + if txfreq == mem.freq: + mem.duplex = "" + elif txfreq == 0: + mem.duplex = "off" + mem.offset = 0 + # 166666665*10 is the equivalent for FF FF FF FF + # stored in the TX field + elif txfreq == 1666666650: + mem.duplex = "off" + mem.offset = 0 + elif txfreq < mem.freq: + mem.duplex = "-" + mem.offset = mem.freq - txfreq + elif txfreq > mem.freq: + mem.duplex = "+" + mem.offset = txfreq - mem.freq + + # get bandwith FM or NFM + mem.mode = MODE_LIST[_mem.mode] + + # tone data + txtone = self.decode_tone(_mem.tx_tone) + rxtone = self.decode_tone(_mem.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.power = POWER_LEVELS[_mem.power] + + # add extra channel settings to the OTHER tab of the properties + # extra settings are unfortunately inverted + mem.extra = RadioSettingGroup("extra", "Extra") + + scanadd = RadioSetting("scanadd", "Scan Add", + RadioSettingValueBoolean( + not bool(_mem.scanadd))) + scanadd.set_doc("Add channel for scanning") + mem.extra.append(scanadd) + + bclo = RadioSetting("bclo", "Busy Lockout", + RadioSettingValueBoolean(not bool(_mem.bclo))) + bclo.set_doc("Busy Lockout") + mem.extra.append(bclo) + + scramb = RadioSetting("scramb", "Scramble", + RadioSettingValueBoolean(not bool(_mem.scramb))) + scramb.set_doc("Scramble Audio Signal") + mem.extra.append(scramb) + + compand = RadioSetting("compand", "Compander", + RadioSettingValueBoolean( + not bool(_mem.compand))) + compand.set_doc("Compress Audio for TX") + mem.extra.append(compand) + + return mem + + def set_memory(self, mem): + bitpos = (1 << ((mem.number - 1) % 8)) + bytepos = ((mem.number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number - 1] + + LOG.warning('This driver may be broken for deleted memories') + if mem.empty: + return + + _mem.rx_freq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + # power, default power is low + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 # low + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.tx_tone, txmode, txtone, txpol) + self.encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol) + + _mem.mode = MODE_LIST.index(mem.mode) + + # extra settings are unfortunately inverted + for setting in mem.extra: + LOG.debug("@set_mem:", setting.get_name(), setting.value) + setattr(_mem, setting.get_name(), not setting.value) + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("settings.squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + TIMEOUT_LIST, TIMEOUT_LIST[_settings.timeout])) + + basic.append(rs) + + rs = RadioSetting("settings.scanmode", "Scan Mode", + RadioSettingValueList( + SCANMODE_LIST, + SCANMODE_LIST[_settings.scanmode])) + basic.append(rs) + + rs = RadioSetting("settings.voice", "Voice Prompts", + RadioSettingValueList( + VOICE_LIST, VOICE_LIST[_settings.voice])) + basic.append(rs) + + rs = RadioSetting("settings.voxgain", "VOX Level", + RadioSettingValueList( + VOX_LIST, VOX_LIST[_settings.voxgain])) + basic.append(rs) + + rs = RadioSetting("settings.voxdelay", "VOX Delay Time", + RadioSettingValueList( + VOXDELAY_LIST, + VOXDELAY_LIST[_settings.voxdelay])) + basic.append(rs) + + rs = RadioSetting("settings.save", "Battery Save", + RadioSettingValueBoolean(_settings.save)) + basic.append(rs) + + rs = RadioSetting("settings.beep", "Beep Tone", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + def _filter(name): + filtered = "" + for char in str(name): + if char in VALID_CHARS: + filtered += char + else: + filtered += " " + return filtered + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + # This radio has always been post-metadata, so never do + # old-school detection + return False diff --git a/chirp/drivers/radtel_t18.py b/chirp/drivers/radtel_t18.py new file mode 100644 index 0000000..b5eea5c --- /dev/null +++ b/chirp/drivers/radtel_t18.py @@ -0,0 +1,501 @@ +# Copyright 2017 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import unittest +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown1:1, + compander:1, + scramble:1, + skip:1, + highpower:1, + narrow:1, + unknown2:1, + bcl:1; + u8 unknown3[3]; +} memory[16]; +#seekto 0x03C0; +struct { + u8 unknown1:1, + scanmode:1, + unknown2:2, + voiceprompt:2, + batterysaver:1, + beep:1; + u8 squelchlevel; + u8 unused2; + u8 timeouttimer; + u8 voxlevel; + u8 unknown3; + u8 unused; + u8 voxdelay; +} settings; +""" + +CMD_ACK = "\x06" +BLOCK_SIZE = 0x08 + +VOICE_LIST = ["Off", "Chinese", "English"] +TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds", + "120 seconds", "150 seconds", "180 seconds", + "210 seconds", "240 seconds", "270 seconds", + "300 seconds"] +SCANMODE_LIST = ["Carrier", "Time"] +VOXLEVEL_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9"] +VOXDELAY_LIST = ["0.5 seconds", "1.0 seconds", "1.5 seconds", + "2.0 seconds", "2.5 seconds", "3.0 seconds"] + +SETTING_LISTS = { + "voice": VOICE_LIST, + "timeouttimer": TIMEOUTTIMER_LIST, + "scanmode": SCANMODE_LIST, + "voxlevel": VOXLEVEL_LIST, + "voxdelay": VOXDELAY_LIST +} + + +def _t18_enter_programming_mode(radio): + serial = radio.pipe + + try: + serial.write("\x02") + time.sleep(0.1) + serial.write("1ROGRAM") + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if not ack: + raise errors.RadioError("No response from radio") + elif ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + try: + serial.write("\x02") + ident = serial.read(8) + except: + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith("SMP558"): + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + try: + serial.write("\x05") + response = serial.read(6) + except: + raise errors.RadioError("Error communicating with radio") + + if not response == ("\xFF" * 6): + LOG.debug(util.hexprint(response)) + raise errors.RadioError("Radio returned unexpected response") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + +def _t18_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("b") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _t18_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, BLOCK_SIZE) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + BLOCK_SIZE) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _t18_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, BLOCK_SIZE) + data = radio.get_mmap()[block_addr:block_addr + 8] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _t18_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + + block = _t18_read_block(radio, addr, BLOCK_SIZE) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _t18_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _t18_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + _t18_write_block(radio, addr, BLOCK_SIZE) + + _t18_exit_programming_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + if len(data) == cls._memsize: + rid = data[0x03D0:0x03D8] + return "P558" in rid + else: + return False + + +@directory.register +class T18Radio(chirp_common.CloneModeRadio): + """radtel T18""" + VENDOR = "Radtel" + MODEL = "T18" + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x03F0), + ] + _memsize = 0x03F0 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.can_odd_split = True + rf.has_rx_dtcs = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.has_tuning_step = False + rf.has_bank = False + rf.has_name = False + rf.memory_bounds = (1, 16) + rf.valid_bands = [(400000000, 470000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + do_upload(self) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _decode_tone(self, val): + val = int(val) + if val == 16665: + return '', None, None + elif val >= 12000: + return 'DTCS', val - 12000, 'R' + elif val >= 8000: + return 'DTCS', val - 8000, 'N' + else: + return 'Tone', val / 10.0, None + + def _encode_tone(self, memval, mode, value, pol): + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if _mem.txfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = not _mem.narrow and "FM" or "NFM" + + mem.skip = _mem.skip and "S" or "" + + txtone = self._decode_tone(_mem.txtone) + rxtone = self._decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.extra = RadioSettingGroup("Extra", "extra") + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueBoolean(not _mem.bcl)) + mem.extra.append(rs) + rs = RadioSetting("scramble", "Scramble", + RadioSettingValueBoolean(not _mem.scramble)) + mem.extra.append(rs) + rs = RadioSetting("compander", "Compander", + RadioSettingValueBoolean(not _mem.compander)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + txtone, rxtone = chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.txtone, *txtone) + self._encode_tone(_mem.rxtone, *rxtone) + + _mem.narrow = 'N' in mem.mode + _mem.skip = mem.skip == "S" + + for setting in mem.extra: + # NOTE: Only three settings right now, all are inverted + setattr(_mem, setting.get_name(), not int(setting.value)) + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("squelchlevel", "Squelch level", + RadioSettingValueInteger( + 0, 9, _settings.squelchlevel)) + basic.append(rs) + + rs = RadioSetting("timeouttimer", "Timeout timer", + RadioSettingValueList( + TIMEOUTTIMER_LIST, + TIMEOUTTIMER_LIST[ + _settings.timeouttimer])) + basic.append(rs) + + rs = RadioSetting("scanmode", "Scan mode", + RadioSettingValueList( + SCANMODE_LIST, + SCANMODE_LIST[_settings.scanmode])) + basic.append(rs) + + rs = RadioSetting("voiceprompt", "Voice prompt", + RadioSettingValueList( + VOICE_LIST, + VOICE_LIST[_settings.voiceprompt])) + basic.append(rs) + + rs = RadioSetting("voxlevel", "Vox level", + RadioSettingValueList( + VOXLEVEL_LIST, + VOXLEVEL_LIST[_settings.voxlevel])) + basic.append(rs) + + rs = RadioSetting("voxdelay", "VOX delay", + RadioSettingValueList( + VOXDELAY_LIST, + VOXDELAY_LIST[_settings.voxdelay])) + basic.append(rs) + + rs = RadioSetting("batterysaver", "Battery saver", + RadioSettingValueBoolean(_settings.batterysaver)) + basic.append(rs) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == cls._memsize: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/repeaterbook.py b/chirp/drivers/repeaterbook.py new file mode 100644 index 0000000..3b68660 --- /dev/null +++ b/chirp/drivers/repeaterbook.py @@ -0,0 +1,36 @@ +# Copyright 2016 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import six + +from chirp import chirp_common +from chirp.drivers import generic_csv + + +class RBRadio(generic_csv.CSVRadio, chirp_common.NetworkSourceRadio): + VENDOR = "RepeaterBook" + MODEL = "" + + def _clean_comment(self, headers, line, mem): + "Converts iso-8859-1 encoded comments to unicode for pyGTK." + if six.PY2: + mem.comment = six.text_type(mem.comment, 'iso-8859-1') + return mem + + def _clean_name(self, headers, line, mem): + "Converts iso-8859-1 encoded names to unicode for pyGTK." + if six.PY2: + mem.name = six.text_type(mem.name, 'iso-8859-1') + return mem diff --git a/chirp/drivers/retevis_rt1.py b/chirp/drivers/retevis_rt1.py new file mode 100644 index 0000000..96baa13 --- /dev/null +++ b/chirp/drivers/retevis_rt1.py @@ -0,0 +1,748 @@ +# Copyright 2016 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + InvalidValueError, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 bcl:1, // Busy Lock + epilogue:1, // Epilogue (STE) + scramble:1, // Scramble + compander:1, // Compander + skip:1, // Scan Add + wide:1, // Bandwidth + unknown1:1, + highpower:1; // Power Level + u8 unknown2[3]; +} memory[16]; + +#seekto 0x0120; +struct { + u8 hivoltnotx:1, // TX Inhibit when voltage too high + lovoltnotx:1, // TX Inhibit when voltage too low + unknown1:1, + alarm:1, // Incept Alarm + scan:1, // Scan + tone:1, // Tone + voice:2; // Voice + u8 unknown2:1, + ssave:3, // Super Battery Save + unknown3:1, + save:3; // Battery Save + u8 squelch; // Squelch + u8 tot; // Time Out Timer + u8 voxi:1, // VOX Inhibit on Receive + voxd:2, // VOX Delay + voxc:1, // VOX Control + voxg:4; // VOX Gain + u8 unknown4:4, + scanspeed:4; // Scan Speed + u8 unknown5:3, + scandelay:5; // Scan Delay + u8 unknown6:3, + prioritych:5; // Priority Channel + u8 k1shortp; // Key 1 Short Press + u8 k2shortp; // Key 2 Short Press + u8 k1longp; // Key 1 Long Press + u8 k2longp; // Key 2 Long Press + u8 lpt; // Long Press Time +} settings; + +#seekto 0x0170; +struct { + char fp[8]; +} fingerprint; +""" + +CMD_ACK = "\x06" + +RT1_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("High", watts=9.00)] + +RT1_DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +LIST_LPT = ["0.5", "1.0", "1.5", "2.0", "2.5"] +LIST_SHORT_PRESS = ["Off", "Monitor On/Off", "Power High/Low", "Alarm", "Volt"] +LIST_LONG_PRESS = ["Off", "Monitor On/Off", "Monitor(momentary)", + "Power High/Low", "Alarm", "Volt", "TX 1750 Hz"] +LIST_VOXDELAY = ["0.5", "1.0", "2.0", "3.0"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_TIMEOUTTIMER = ["Off"] + ["%s" % x for x in range(30, 330, 30)] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SSAVE = ["Off"] + ["%s" % x for x in range(1, 7)] +LIST_PRIORITYCH = ["Off"] + ["%s" % x for x in range(1, 17)] +LIST_SCANSPEED = ["%s" % x for x in range(100, 550, 50)] +LIST_SCANDELAY = ["%s" % x for x in range(3, 31)] + +SETTING_LISTS = { + "lpt": LIST_LPT, + "k1shortp": LIST_SHORT_PRESS, + "k1longp": LIST_LONG_PRESS, + "k2shortp": LIST_SHORT_PRESS, + "k2longp": LIST_LONG_PRESS, + "voxd": LIST_VOXDELAY, + "voice": LIST_VOICE, + "tot": LIST_TIMEOUTTIMER, + "save": LIST_SAVE, + "ssave": LIST_SSAVE, + "prioritych": LIST_PRIORITYCH, + "scanspeed": LIST_SCANSPEED, + "scandelay": LIST_SCANDELAY, + } + +# Retevis RT1 fingerprints +RT1_VHF_fp = "PXT8K" + "\xF0\x00\x00" # RT1 VHF model +RT1_UHF_fp = "PXT8K" + "\xF3\x00\x00" # RT1 UHF model + +MODELS = [RT1_VHF_fp, RT1_UHF_fp] + + +def _model_from_data(data): + return data[0x0170:0x0178] + + +def _model_from_image(radio): + return _model_from_data(radio.get_mmap()) + + +def _get_radio_model(radio): + block = _rt1_read_block(radio, 0x0170, 0x10) + version = block[0:8] + return version + + +def _rt1_enter_programming_mode(radio): + serial = radio.pipe + + magic = ["PROGRAMa", "PROGRAMb"] + for i in range(0, 2): + + try: + LOG.debug("sending " + magic[i]) + serial.write(magic[i]) + ack = serial.read(1) + except: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if not ack: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("No response from radio") + elif ack != CMD_ACK: + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ack)) + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + try: + LOG.debug("sending " + util.hexprint("\x02")) + serial.write("\x02") + ident = serial.read(16) + except: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith("PXT8K"): + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ident)) + _rt1_exit_programming_mode(radio) + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + LOG.debug("sending " + util.hexprint("MXT8KCUMHS1X7BN/")) + serial.write("MXT8KCUMHS1X7BN/") + ack = serial.read(1) + except: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != "\xB2": + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ack)) + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + try: + LOG.debug("sending " + util.hexprint(CMD_ACK)) + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ack)) + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + # DEBUG + LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL)) + + +def _rt1_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _rt1_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + _rt1_exit_programming_mode(radio) + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + _rt1_exit_programming_mode(radio) + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _rt1_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + _rt1_exit_programming_mode(radio) + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _rt1_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio._block_size): + status.cur = addr + radio._block_size + radio.status_fn(status) + + block = _rt1_read_block(radio, addr, radio._block_size) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _rt1_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _rt1_enter_programming_mode(radio) + + image_model = _model_from_image(radio) + LOG.info("Image Version is %s" % repr(image_model)) + + radio_model = _get_radio_model(radio) + LOG.info("Radio Version is %s" % repr(radio_model)) + + bands = ["VHF", "UHF"] + image_band = radio_band = "unknown" + for i in range(0,2): + if image_model == MODELS[i]: + image_band = bands[i] + if radio_model == MODELS[i]: + radio_band = bands[i] + + if image_model != radio_model: + _rt1_exit_programming_mode(radio) + msg = ("The upload was stopped because the band supported by " + "the image (%s) does not match the band supported by " + "the radio (%s).") + raise errors.RadioError(msg % (image_band, radio_band)) + + status.cur = 0 + status.max = 0x0190 + + for start_addr, end_addr, block_size in radio._ranges: + for addr in range(start_addr, end_addr, block_size): + status.cur = addr + block_size + radio.status_fn(status) + _rt1_write_block(radio, addr, block_size) + + _rt1_exit_programming_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x0170:0x0176] + + return rid.startswith("PXT8K") + + +@directory.register +class RT1Radio(chirp_common.CloneModeRadio): + """Retevis RT1""" + VENDOR = "Retevis" + MODEL = "RT1" + BAUD_RATE = 2400 + + _ranges = [ + (0x0000, 0x0190, 0x10), + ] + _memsize = 0x0400 + _block_size = 0x10 + _vhf_range = (134000000, 175000000) + _uhf_range = (400000000, 521000000) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = False + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = RT1_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.memory_bounds = (1, 16) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + if self._mmap is None: + rf.valid_bands = [self._vhf_range, self._uhf_range] + elif self._my_band() == RT1_VHF_fp: + rf.valid_bands = [self._vhf_range] + elif self._my_band() == RT1_UHF_fp: + rf.valid_bands = [self._uhf_range] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + do_upload(self) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def _my_band(self): + model_tag = _model_from_image(self) + return model_tag + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = _mem.wide and "FM" or "NFM" + + rxtone = txtone = None + txtone = self.decode_tone(_mem.txtone) + rxtone = self.decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.power = RT1_POWER_LEVELS[_mem.highpower] + + if _mem.skip: + mem.skip = "S" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(not _mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("epilogue", "Epilogue(STE)", + RadioSettingValueBoolean(not _mem.epilogue)) + mem.extra.append(rs) + + rs = RadioSetting("compander", "Compander", + RadioSettingValueBoolean(not _mem.compander)) + mem.extra.append(rs) + + rs = RadioSetting("scramble", "Scramble", + RadioSettingValueBoolean(not _mem.scramble)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.wide = mem.mode == "FM" + + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.txtone, txmode, txtone, txpol) + self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol) + + _mem.highpower = mem.power == RT1_POWER_LEVELS[1] + + _mem.skip = mem.skip == "S" + + for setting in mem.extra: + setattr(_mem, setting.get_name(), not int(setting.value)) + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("lpt", "Long Press Time[s]", + RadioSettingValueList( + LIST_LPT, + LIST_LPT[_settings.lpt])) + basic.append(rs) + + if _settings.k1shortp > 4: + val = 1 + else: + val = _settings.k1shortp + rs = RadioSetting("k1shortp", "Key 1 Short Press", + RadioSettingValueList( + LIST_SHORT_PRESS, + LIST_SHORT_PRESS[val])) + basic.append(rs) + + if _settings.k1longp > 6: + val = 3 + else: + val = _settings.k1longp + rs = RadioSetting("k1longp", "Key 1 Long Press", + RadioSettingValueList( + LIST_LONG_PRESS, + LIST_LONG_PRESS[val])) + basic.append(rs) + + if _settings.k2shortp > 4: + val = 4 + else: + val = _settings.k2shortp + rs = RadioSetting("k2shortp", "Key 2 Short Press", + RadioSettingValueList( + LIST_SHORT_PRESS, + LIST_SHORT_PRESS[val])) + basic.append(rs) + + if _settings.k2longp > 6: + val = 4 + else: + val = _settings.k2longp + rs = RadioSetting("k2longp", "Key 2 Long Press", + RadioSettingValueList( + LIST_LONG_PRESS, + LIST_LONG_PRESS[val])) + basic.append(rs) + + rs = RadioSetting("voxc", "VOX Control", + RadioSettingValueBoolean(not _settings.voxc)) + basic.append(rs) + + if _settings.voxg > 8: + val = 4 + else: + val = _settings.voxg + 1 + rs = RadioSetting("voxg", "VOX Gain", + RadioSettingValueInteger(1, 9, val)) + basic.append(rs) + + rs = RadioSetting("voxd", "VOX Delay Time", + RadioSettingValueList( + LIST_VOXDELAY, + LIST_VOXDELAY[_settings.voxd])) + basic.append(rs) + + rs = RadioSetting("voxi", "VOX Inhibit on Receive", + RadioSettingValueBoolean(not _settings.voxi)) + basic.append(rs) + + if _settings.squelch > 8: + val = 4 + else: + val = _settings.squelch + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, val)) + basic.append(rs) + + if _settings.voice == 3: + val = 1 + else: + val = _settings.voice + rs = RadioSetting("voice", "Voice Prompts", + RadioSettingValueList( + LIST_VOICE, + LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("tone", "Tone", + RadioSettingValueBoolean(_settings.tone)) + basic.append(rs) + + rs = RadioSetting("lovoltnotx", "TX Inhibit (when battery < 6 volts)", + RadioSettingValueBoolean(_settings.lovoltnotx)) + basic.append(rs) + + rs = RadioSetting("hivoltnotx", "TX Inhibit (when battery > 9 volts)", + RadioSettingValueBoolean(_settings.hivoltnotx)) + basic.append(rs) + + if _settings.tot > 10: + val = 6 + else: + val = _settings.tot + rs = RadioSetting("tot", "Time-out Timer[s]", + RadioSettingValueList( + LIST_TIMEOUTTIMER, + LIST_TIMEOUTTIMER[val])) + basic.append(rs) + + if _settings.save < 3: + val = 0 + else: + val = _settings.save - 3 + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, + LIST_SAVE[val])) + basic.append(rs) + + rs = RadioSetting("ssave", "Super Battery Saver[s]", + RadioSettingValueList( + LIST_SSAVE, + LIST_SSAVE[_settings.ssave])) + basic.append(rs) + + rs = RadioSetting("alarm", "Incept Alarm", + RadioSettingValueBoolean(_settings.alarm)) + basic.append(rs) + + rs = RadioSetting("scan", "Scan Function", + RadioSettingValueBoolean(_settings.scan)) + basic.append(rs) + + if _settings.prioritych > 15: + val = 0 + else: + val = _settings.prioritych + 1 + rs = RadioSetting("prioritych", "Priority Channel", + RadioSettingValueList( + LIST_PRIORITYCH, + LIST_PRIORITYCH[val])) + basic.append(rs) + + if _settings.scanspeed > 8: + val = 4 + else: + val = _settings.scanspeed + rs = RadioSetting("scanspeed", "Scan Speed[ms]", + RadioSettingValueList( + LIST_SCANSPEED, + LIST_SCANSPEED[val])) + basic.append(rs) + + if _settings.scandelay > 27: + val = 12 + else: + val = _settings.scandelay + rs = RadioSetting("scandelay", "Scan Droupout Delay Time[s]", + RadioSettingValueList( + LIST_SCANDELAY, + LIST_SCANDELAY[val])) + basic.append(rs) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if setting == "voxc": + setattr(obj, setting, not int(element.value)) + elif setting == "voxg": + setattr(obj, setting, int(element.value) - 1) + elif setting == "voxi": + setattr(obj, setting, not int(element.value)) + elif setting == "voice": + if int(element.value) == 2: + setattr(obj, setting, int(element.value) + 1) + else: + setattr(obj, setting, int(element.value)) + elif setting == "save": + setattr(obj, setting, int(element.value) + 3) + elif setting == "prioritych": + if int(element.value) == 0: + setattr(obj, setting, int(element.value) + 31) + else: + setattr(obj, setting, int(element.value) - 1) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) in [0x0400, ]: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/retevis_rt21.py b/chirp/drivers/retevis_rt21.py new file mode 100644 index 0000000..856b33e --- /dev/null +++ b/chirp/drivers/retevis_rt21.py @@ -0,0 +1,584 @@ +# Copyright 2016 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 unknown1:3, + bcl:2, // Busy Lock + unknown2:3; + u8 unknown3:2, + highpower:1, // Power Level + wide:1, // Bandwidth + unknown4:4; + u8 scramble_type:4, + unknown5:4; + u8 unknown6:4, + scramble_type2:4; +} memory[16]; + +#seekto 0x011D; +struct { + u8 unused:4, + pf1:4; // Programmable Function Key 1 +} keys; + +#seekto 0x012C; +struct { + u8 use_scramble; // Scramble Enable + u8 unknown1[2]; + u8 voice; // Voice Annunciation + u8 tot; // Time-out Timer + u8 totalert; // Time-out Timer Pre-alert + u8 unknown2[2]; + u8 squelch; // Squelch Level + u8 save; // Battery Saver + u8 unknown3[3]; + u8 use_vox; // VOX Enable + u8 vox; // VOX Gain +} settings; + +#seekto 0x017E; +u8 skipflags[2]; // SCAN_ADD +""" + +CMD_ACK = "\x06" +BLOCK_SIZE = 0x10 + +RT21_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), + chirp_common.PowerLevel("High", watts=2.50)] + + +RT21_DTCS = sorted(chirp_common.DTCS_CODES + + [17, 50, 55, 135, 217, 254, 305, 645, 765]) + +BCL_LIST = ["Off", "Carrier", "QT/DQT"] +SCRAMBLE_LIST = ["Scramble 1", "Scramble 2", "Scramble 3", "Scramble 4", + "Scramble 5", "Scramble 6", "Scramble 7", "Scramble 8"] +TIMEOUTTIMER_LIST = ["%s seconds" % x for x in range(15, 615, 15)] +TOTALERT_LIST = ["Off"] + ["%s seconds" % x for x in range(1, 11)] +VOICE_LIST = ["Off", "Chinese", "English"] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)] +PF1_CHOICES = ["None", "Monitor", "Scan", "Scramble", "Alarm"] +PF1_VALUES = [0x0F, 0x04, 0x06, 0x08, 0x0C] + +SETTING_LISTS = { + "bcl": BCL_LIST, + "scramble": SCRAMBLE_LIST, + "tot": TIMEOUTTIMER_LIST, + "totalert": TOTALERT_LIST, + "voice": VOICE_LIST, + "vox": VOX_LIST, + } + + +def _rt21_enter_programming_mode(radio): + serial = radio.pipe + + try: + serial.write("PRMZUNE") + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if not ack: + raise errors.RadioError("No response from radio") + elif ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + try: + serial.write("\x02") + ident = serial.read(8) + except: + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith("P3207"): + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + +def _rt21_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _rt21_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, BLOCK_SIZE) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + BLOCK_SIZE) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _rt21_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, BLOCK_SIZE) + data = radio.get_mmap()[block_addr:block_addr + BLOCK_SIZE] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _rt21_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + + block = _rt21_read_block(radio, addr, BLOCK_SIZE) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _rt21_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _rt21_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + _rt21_write_block(radio, addr, BLOCK_SIZE) + + _rt21_exit_programming_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x01B8:0x01BE] + + return rid.startswith("P3207") + + +@directory.register +class RT21Radio(chirp_common.CloneModeRadio): + """RETEVIS RT21""" + VENDOR = "Retevis" + MODEL = "RT21" + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x0400), + ] + _memsize = 0x0400 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = False + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = RT21_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.memory_bounds = (1, 16) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + rf.valid_bands = [(400000000, 480000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + do_upload(self) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2000: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2000: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + if mem.tmode == "DTCS": + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.tx_tone, rxmode, _mem.rx_tone)) + + def get_memory(self, number): + bitpos = (1 << ((number - 1) % 8)) + bytepos = ((number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[number - 1] + _skp = self._memobj.skipflags[bytepos] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if _mem.get_raw() == ("\xFF" * 16): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 13 + "\x30\x8F\xF8") + + if int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = _mem.wide and "FM" or "NFM" + + self._get_tone(_mem, mem) + + mem.power = RT21_POWER_LEVELS[_mem.highpower] + + mem.skip = "" if (_skp & bitpos) else "S" + LOG.debug("mem.skip %s" % mem.skip) + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueList( + BCL_LIST, BCL_LIST[_mem.bcl])) + mem.extra.append(rs) + + rs = RadioSetting("scramble_type", "Scramble Type", + RadioSettingValueList(SCRAMBLE_LIST, + SCRAMBLE_LIST[_mem.scramble_type - 8])) + mem.extra.append(rs) + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + tx_tone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = int(mem.ctone * 10) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.tx_tone, rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + bitpos = (1 << ((mem.number - 1) % 8)) + bytepos = ((mem.number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[mem.number - 1] + _skp = self._memobj.skipflags[bytepos] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.set_raw("\x00" * 13 + "\x00\x8F\xF8") + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.wide = mem.mode == "FM" + + self._set_tone(mem, _mem) + + _mem.highpower = mem.power == RT21_POWER_LEVELS[1] + + if mem.skip != "S": + _skp |= bitpos + else: + _skp &= ~bitpos + LOG.debug("_skp %s" % _skp) + + for setting in mem.extra: + if setting.get_name() == "scramble_type": + setattr(_mem, setting.get_name(), int(setting.value) + 8) + setattr(_mem, "scramble_type2", int(setting.value) + 8) + else: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _keys = self._memobj.keys + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("tot", "Time-out timer", + RadioSettingValueList( + TIMEOUTTIMER_LIST, + TIMEOUTTIMER_LIST[_settings.tot - 1])) + basic.append(rs) + + rs = RadioSetting("totalert", "TOT Pre-alert", + RadioSettingValueList( + TOTALERT_LIST, + TOTALERT_LIST[_settings.totalert])) + basic.append(rs) + + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("voice", "Voice Annumciation", + RadioSettingValueList( + VOICE_LIST, VOICE_LIST[_settings.voice])) + basic.append(rs) + + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueBoolean(_settings.save)) + basic.append(rs) + + rs = RadioSetting("use_scramble", "Scramble", + RadioSettingValueBoolean(_settings.use_scramble)) + basic.append(rs) + + rs = RadioSetting("use_vox", "VOX", + RadioSettingValueBoolean(_settings.use_vox)) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Gain", + RadioSettingValueList( + VOX_LIST, VOX_LIST[_settings.vox])) + basic.append(rs) + + def apply_pf1_listvalue(setting, obj): + LOG.debug("Setting value: "+ str(setting.value) + " from list") + val = str(setting.value) + index = PF1_CHOICES.index(val) + val = PF1_VALUES[index] + obj.set_value(val) + + if _keys.pf1 in PF1_VALUES: + idx = PF1_VALUES.index(_keys.pf1) + else: + idx = LIST_DTMF_SPECIAL_VALUES.index(0x04) + rs = RadioSetting("keys.pf1", "PF1 Key Function", + RadioSettingValueList(PF1_CHOICES, + PF1_CHOICES[idx])) + rs.set_apply_callback(apply_pf1_listvalue, _keys.pf1) + basic.append(rs) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "tot": + setattr(obj, setting, int(element.value) + 1) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) in [0x0400, ]: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + diff --git a/chirp/drivers/retevis_rt22.py b/chirp/drivers/retevis_rt22.py new file mode 100644 index 0000000..9edb9c4 --- /dev/null +++ b/chirp/drivers/retevis_rt22.py @@ -0,0 +1,650 @@ +# Copyright 2016 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings, \ + RadioSettingValueString + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 unknown1; + u8 unknown3:2, + highpower:1, // Power Level + wide:1, // Bandwidth + unknown4:4; + u8 unknown5[2]; +} memory[16]; + +#seekto 0x012F; +struct { + u8 voice; // Voice Annunciation + u8 tot; // Time-out Timer + u8 unknown1[3]; + u8 squelch; // Squelch Level + u8 save; // Battery Saver + u8 beep; // Beep + u8 unknown2[2]; + u8 vox; // VOX + u8 voxgain; // VOX Gain + u8 voxdelay; // VOX Delay + u8 unknown3[2]; + u8 pf2key; // PF2 Key +} settings; + +#seekto 0x017E; +u8 skipflags[2]; // SCAN_ADD + +#seekto 0x0300; +struct { + char line1[32]; + char line2[32]; +} embedded_msg; +""" + +CMD_ACK = "\x06" + +RT22_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=2.00), + chirp_common.PowerLevel("High", watts=5.00)] + +RT22_DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +PF2KEY_LIST = ["Scan", "Local Alarm", "Remote Alarm"] +TIMEOUTTIMER_LIST = [""] + ["%s seconds" % x for x in range(15, 615, 15)] +VOICE_LIST = ["Off", "Chinese", "English"] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)] +VOXDELAY_LIST = ["0.5", "1.0", "1.5", "2.0", "2.5", "3.0"] + +SETTING_LISTS = { + "pf2key": PF2KEY_LIST, + "tot": TIMEOUTTIMER_LIST, + "voice": VOICE_LIST, + "vox": VOX_LIST, + "voxdelay": VOXDELAY_LIST, + } + +VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" + + +def _rt22_enter_programming_mode(radio): + serial = radio.pipe + + magic = "PROGRGS" + exito = False + for i in range(0, 5): + for j in range(0, len(magic)): + time.sleep(0.005) + serial.write(magic[j]) + ack = serial.read(1) + + try: + if ack == CMD_ACK: + exito = True + break + except: + LOG.debug("Attempt #%s, failed, trying again" % i) + pass + + # check if we had EXITO + if exito is False: + msg = "The radio did not accept program mode after five tries.\n" + msg += "Check you interface cable and power cycle your radio." + raise errors.RadioError(msg) + + try: + serial.write("\x02") + ident = serial.read(8) + except: + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + # check if ident is OK + itis = False + for fp in radio._fileid: + if fp in ident: + # got it! + itis = True + + break + + if itis is False: + LOG.debug("Incorrect model ID, got this:\n\n" + util.hexprint(ident)) + raise errors.RadioError("Radio identification failed.") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + try: + serial.write("\x07") + ack = serial.read(1) + except: + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != "\x4E": + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + +def _rt22_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _rt22_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + for j in range(0, len(cmd)): + time.sleep(0.005) + serial.write(cmd[j]) + + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + _rt22_exit_programming_mode(radio) + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + _rt22_exit_programming_mode(radio) + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _rt22_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + for j in range(0, len(cmd)): + time.sleep(0.005) + serial.write(cmd[j]) + for j in range(0, len(data)): + time.sleep(0.005) + serial.write(data[j]) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + _rt22_exit_programming_mode(radio) + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _rt22_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio._block_size): + status.cur = addr + radio._block_size + radio.status_fn(status) + + block = _rt22_read_block(radio, addr, radio._block_size) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + data += radio.MODEL.ljust(8) + + _rt22_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _rt22_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr, block_size in radio._ranges: + for addr in range(start_addr, end_addr, block_size): + status.cur = addr + block_size + radio.status_fn(status) + _rt22_write_block(radio, addr, block_size) + + _rt22_exit_programming_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + if len(data) == 0x0408: + rid = data[0x0400:0x0408] + return rid.startswith(cls.MODEL) + else: + return False + + +@directory.register +class RT22Radio(chirp_common.CloneModeRadio): + """Retevis RT22""" + VENDOR = "Retevis" + MODEL = "RT22" + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x0180, 0x10), + (0x01B8, 0x01F8, 0x10), + (0x01F8, 0x0200, 0x08), + (0x0200, 0x0340, 0x10), + ] + _memsize = 0x0400 + _block_size = 0x40 + _fileid = ["P32073", "P3" + "\x00\x00\x00" + "3"] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = False + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = RT22_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.memory_bounds = (1, 16) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + rf.valid_bands = [(400000000, 520000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + """Download from radio""" + try: + data = do_download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = data + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + do_upload(self) + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + if mem.tmode == "DTCS": + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.tx_tone, rxmode, _mem.rx_tone)) + + def get_memory(self, number): + bitpos = (1 << ((number - 1) % 8)) + bytepos = ((number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[number - 1] + _skp = self._memobj.skipflags[bytepos] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = _mem.wide and "FM" or "NFM" + + self._get_tone(_mem, mem) + + mem.power = RT22_POWER_LEVELS[_mem.highpower] + + mem.skip = "" if (_skp & bitpos) else "S" + LOG.debug("mem.skip %s" % mem.skip) + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + tx_tone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = int(mem.ctone * 10) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.tx_tone, rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + bitpos = (1 << ((mem.number - 1) % 8)) + bytepos = ((mem.number - 1) / 8) + LOG.debug("bitpos %s" % bitpos) + LOG.debug("bytepos %s" % bytepos) + + _mem = self._memobj.memory[mem.number - 1] + _skp = self._memobj.skipflags[bytepos] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.wide = mem.mode == "FM" + + self._set_tone(mem, _mem) + + _mem.highpower = mem.power == RT22_POWER_LEVELS[1] + + if mem.skip != "S": + _skp |= bitpos + else: + _skp &= ~bitpos + LOG.debug("_skp %s" % _skp) + + def get_settings(self): + _settings = self._memobj.settings + _message = self._memobj.embedded_msg + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("tot", "Time-out timer", + RadioSettingValueList( + TIMEOUTTIMER_LIST, + TIMEOUTTIMER_LIST[_settings.tot])) + basic.append(rs) + + rs = RadioSetting("voice", "Voice Prompts", + RadioSettingValueList( + VOICE_LIST, VOICE_LIST[_settings.voice])) + basic.append(rs) + + rs = RadioSetting("pf2key", "PF2 Key", + RadioSettingValueList( + PF2KEY_LIST, PF2KEY_LIST[_settings.pf2key])) + basic.append(rs) + + rs = RadioSetting("vox", "Vox", + RadioSettingValueBoolean(_settings.vox)) + basic.append(rs) + + rs = RadioSetting("voxgain", "VOX Level", + RadioSettingValueList( + VOX_LIST, VOX_LIST[_settings.voxgain])) + basic.append(rs) + + rs = RadioSetting("voxdelay", "VOX Delay Time", + RadioSettingValueList( + VOXDELAY_LIST, + VOXDELAY_LIST[_settings.voxdelay])) + basic.append(rs) + + rs = RadioSetting("save", "Battery Save", + RadioSettingValueBoolean(_settings.save)) + basic.append(rs) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + def _filter(name): + filtered = "" + for char in str(name): + if char in VALID_CHARS: + filtered += char + else: + filtered += " " + return filtered + + rs = RadioSetting("embedded_msg.line1", "Embedded Message 1", + RadioSettingValueString(0, 32, _filter( + _message.line1))) + basic.append(rs) + + rs = RadioSetting("embedded_msg.line2", "Embedded Message 2", + RadioSettingValueString(0, 32, _filter( + _message.line2))) + basic.append(rs) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) in [0x0408, ]: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + +@directory.register +class KDC1(RT22Radio): + """WLN KD-C1""" + VENDOR = "WLN" + MODEL = "KD-C1" + +@directory.register +class ZTX6(RT22Radio): + """Zastone ZT-X6""" + VENDOR = "Zastone" + MODEL = "ZT-X6" + +@directory.register +class LT316(RT22Radio): + """Luiton LT-316""" + VENDOR = "LUITON" + MODEL = "LT-316" + +@directory.register +class TDM8(RT22Radio): + VENDOR = "TID" + MODEL = "TD-M8" diff --git a/chirp/drivers/retevis_rt23.py b/chirp/drivers/retevis_rt23.py new file mode 100644 index 0000000..d424131 --- /dev/null +++ b/chirp/drivers/retevis_rt23.py @@ -0,0 +1,868 @@ +# Copyright 2017 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import os +import struct +import re +import logging + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct memory { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown1; + u8 pttid:2, // PTT-ID + unknown2:1, + signaling:1, // Signaling(ANI) + unknown3:1, + bcl:1, // Busy Channel Lockout + unknown4:2; + u8 unknown5:3, + highpower:1, // Power Level + isnarrow:1, // Bandwidth + scan:1, // Scan Add + unknown6:2; + u8 unknown7; +}; + +#seekto 0x0010; +struct memory channels[128]; + +#seekto 0x0810; +struct memory vfo_a; +struct memory vfo_b; + +#seekto 0x0830; +struct { + u8 unknown_0830_1:4, + color:2, // Background Color + dst:1, // DTMF Side Tone + txsel:1; // Priority TX Channel Select + u8 scans:2, // Scan Mode + unknown_0831:1, + autolk:1, // Auto Key Lock + save:1, // Battery Save + beep:1, // Key Beep + voice:2; // Voice Prompt + u8 vfomr_fm:1, // FM Radio Display Mode + led:2, // Background Light + unknown_0832_2:1, + dw:1, // FM Radio Dual Watch + name:1, // Display Names + vfomr_a:2; // Display Mode A + u8 opnset:2, // Power On Message + unknown_0833_1:3, + dwait:1, // Dual Standby + vfomr_b:2; // Display Mode B + u8 mrcha; // mr a ch num + u8 mrchb; // mr b ch num + u8 fmch; // fm radio ch num + u8 unknown_0837_1:1, + ste:1, // Squelch Tail Eliminate + roger:1, // Roger Beep + unknown_0837_2:1, + vox:4; // VOX + u8 step:4, // Step + unknown_0838_1:4; + u8 squelch; // Squelch + u8 tot; // Time Out Timer + u8 rptmod:1, // Repeater Mode + volmod:2, // Volume Mode + rptptt:1, // Repeater PTT Switch + rptspk:1, // Repeater Speaker + relay:3; // Cross Band Repeater Enable + u8 unknown_083C:4, // 0x083C + rptrl:4; // Repeater TX Delay + u8 pf1:4, // Function Key 1 + pf2:4; // Function Key 2 + u8 vot; // VOX Delay Time +} settings; + +#seekto 0x0848; +struct { + char line1[7]; +} poweron_msg; + +struct limit { + bbcd lower[2]; + bbcd upper[2]; +}; + +#seekto 0x0850; +struct { + struct limit vhf; + struct limit uhf; +} limits; + +#seekto 0x08D0; +struct { + char name[7]; + u8 unknown2[1]; +} names[128]; + +#seekto 0x0D20; +u8 usedflags[16]; +u8 scanflags[16]; + +#seekto 0x0FA0; +struct { + u8 unknown_0FA0_1:4, + dispab:1, // select a/b + unknown_0FA0_2:3; +} settings2; +""" + +CMD_ACK = "\x06" +BLOCK_SIZE = 0x10 + +RT23_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), + chirp_common.PowerLevel("High", watts=2.50)] + + +RT23_DTCS = sorted(chirp_common.DTCS_CODES + + [17, 50, 55, 135, 217, 254, 305, 645, 765]) + +RT23_CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + \ + ":;<=>?@ !\"#$%&'()*+,-./" + +LIST_COLOR = ["Blue", "Orange", "Purple"] +LIST_LED = ["Off", "On", "Auto"] +LIST_OPNSET = ["Full", "Voltage", "Message"] +LIST_PFKEY = [ + "Radio", + "Sub-channel Sent", + "Scan", + "Alarm", + "DTMF", + "Squelch Off Momentarily", + "Battery Power Indicator", + "Tone 1750", + "Tone 2100", + "Tone 1000", + "Tone 1450"] +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_RPTMOD = ["Single", "Double"] +LIST_RPTRL = ["0.5S", "1.0S", "1.5S", "2.0S", "2.5S", "3.0S", "3.5S", "4.0S", + "4.5S"] +LIST_SCANS = ["Time Operated", "Carrier Operated", "Search"] +LIST_SIGNALING = ["No", "DTMF"] +LIST_TOT = ["OFF"] + ["%s seconds" % x for x in range(30, 300, 30)] +LIST_TXSEL = ["Edit", "Busy"] +_STEP_LIST = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.] +LIST_STEP = ["{0:.2f}K".format(x) for x in _STEP_LIST] +LIST_VFOMR = ["VFO", "MR(Frequency)", "MR(Channel #/Name)"] +LIST_VFOMRFM = ["VFO", "Channel"] +LIST_VOICE = ["Off", "Chinese", "English"] +LIST_VOLMOD = ["Off", "Sub", "Main"] +LIST_VOT = ["0.5S", "1.0S", "1.5S", "2.0S", "3.0S"] +LIST_VOX = ["OFF"] + ["%s" % x for x in range(1, 6)] + + +def _rt23_enter_programming_mode(radio): + serial = radio.pipe + + magic = "PROIUAM" + exito = False + for i in range(0, 5): + for j in range(0, len(magic)): + time.sleep(0.005) + serial.write(magic[j]) + ack = serial.read(1) + + try: + if ack == CMD_ACK: + exito = True + break + except: + LOG.debug("Attempt #%s, failed, trying again" % i) + pass + + # check if we had EXITO + if exito is False: + msg = "The radio did not accept program mode after five tries.\n" + msg += "Check you interface cable and power cycle your radio." + raise errors.RadioError(msg) + + try: + serial.write("\x02") + ident = serial.read(8) + except: + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith("P31183"): + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + serial.write(CMD_ACK) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + +def _rt23_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _rt23_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, BLOCK_SIZE) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + BLOCK_SIZE + 1) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + chunk = response[4:] + + cs = 0 + for byte in chunk[:-1]: + cs += ord(byte) + if ord(chunk[-1]) != (cs & 0xFF): + raise Exception("Block failed checksum!") + + block_data = chunk[:-1] + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + return block_data + + +def _rt23_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, BLOCK_SIZE) + data = radio.get_mmap()[block_addr:block_addr + BLOCK_SIZE] + cs = 0 + for byte in data: + cs += ord(byte) + data += chr(cs & 0xFF) + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _rt23_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + + block = _rt23_read_block(radio, addr, BLOCK_SIZE) + if addr == 0 and block.startswith("\xFF" * 6): + block = "P31183" + "\xFF" * 10 + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _rt23_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _rt23_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, BLOCK_SIZE): + status.cur = addr + BLOCK_SIZE + radio.status_fn(status) + _rt23_write_block(radio, addr, BLOCK_SIZE) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + if len(data) == 0x1000: + rid = data[0x0000:0x0006] + return rid == "P31183" + else: + return False + + +def _split(rf, f1, f2): + """Returns False if the two freqs are in the same band (no split) + or True otherwise""" + + # determine if the two freqs are in the same band + for low, high in rf.valid_bands: + if f1 >= low and f1 <= high and \ + f2 >= low and f2 <= high: + # if the two freqs are on the same Band this is not a split + return False + + # if you get here is because the freq pairs are split + return True + + +@directory.register +class RT23Radio(chirp_common.CloneModeRadio): + """RETEVIS RT23""" + VENDOR = "Retevis" + MODEL = "RT23" + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x0EC0), + ] + _memsize = 0x1000 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.valid_name_length = 7 + rf.valid_characters = RT23_CHARSET + rf.has_name = True + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = RT23_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["FM", "NFM"] # 25 KHz, 12.5 KHz. + rf.memory_bounds = (1, 128) + rf.valid_tuning_steps = _STEP_LIST + rf.valid_bands = [ + (136000000, 174000000), + (400000000, 480000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + """Download from radio""" + try: + data = do_download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = data + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + do_upload(self) + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_memory(self, number): + mem = chirp_common.Memory() + _mem = self._memobj.channels[number-1] + _nam = self._memobj.names[number - 1] + mem.number = number + bitpos = (1 << ((number - 1) % 8)) + bytepos = ((number - 1) / 8) + _scn = self._memobj.scanflags[bytepos] + _usd = self._memobj.usedflags[bytepos] + isused = bitpos & int(_usd) + isscan = bitpos & int(_scn) + + if not isused: + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.empty = True + return mem + + if _mem.get_raw() == ("\xFF" * 16): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 16) + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[4] == "\xFF": + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX freq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset != 0: + if _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10): + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + elif offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + for char in _nam.name: + if str(char) == "\xFF": + char = " " + mem.name += str(char) + mem.name = mem.name.rstrip() + + mem.mode = _mem.isnarrow and "NFM" or "FM" + + rxtone = txtone = None + txtone = self.decode_tone(_mem.txtone) + rxtone = self.decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.power = RT23_POWER_LEVELS[_mem.highpower] + + if not isscan: + mem.skip = "S" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList( + LIST_PTTID, LIST_PTTID[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("signaling", "Optional Signaling", + RadioSettingValueList(LIST_SIGNALING, + LIST_SIGNALING[_mem.signaling])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number)) + _mem = self._memobj.channels[mem.number - 1] + _nam = self._memobj.names[mem.number - 1] + bitpos = (1 << ((mem.number - 1) % 8)) + bytepos = ((mem.number - 1) / 8) + _scn = self._memobj.scanflags[bytepos] + _usd = self._memobj.usedflags[bytepos] + + if mem.empty: + _mem.set_raw("\xFF" * 16) + _nam.name = ("\xFF" * 7) + _usd &= ~bitpos + _scn &= ~bitpos + return + else: + _usd |= bitpos + + if _mem.get_raw() == ("\xFF" * 16): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 16) + _scn |= bitpos + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _nam.name[i] = mem.name[i] + except IndexError: + _nam.name[i] = "\xFF" + + _mem.scan = mem.skip != "S" + if mem.skip == "S": + _scn &= ~bitpos + else: + _scn |= bitpos + _mem.isnarrow = mem.mode == "NFM" + + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.txtone, txmode, txtone, txpol) + self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol) + + _mem.highpower = mem.power == RT23_POWER_LEVELS[1] + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + workmode = RadioSettingGroup("workmode", "Workmode Settings") + fmradio = RadioSettingGroup("fmradio", "FM Radio Settings") + top = RadioSettings(basic, advanced, other, workmode, fmradio) + + save = RadioSetting("save", "Battery Saver", + RadioSettingValueBoolean(_settings.save)) + basic.append(save) + + vox = RadioSetting("vox", "VOX Gain", + RadioSettingValueList( + LIST_VOX, LIST_VOX[_settings.vox])) + basic.append(vox) + + squelch = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger( + 0, 9, _settings.squelch)) + basic.append(squelch) + + relay = RadioSetting("relay", "Repeater", + RadioSettingValueBoolean(_settings.relay)) + basic.append(relay) + + tot = RadioSetting("tot", "Time-out timer", RadioSettingValueList( + LIST_TOT, LIST_TOT[_settings.tot])) + basic.append(tot) + + beep = RadioSetting("beep", "Key Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(beep) + + color = RadioSetting("color", "Background Color", RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_settings.color - 1])) + basic.append(color) + + vot = RadioSetting("vot", "VOX Delay Time", RadioSettingValueList( + LIST_VOT, LIST_VOT[_settings.vot])) + basic.append(vot) + + dwait = RadioSetting("dwait", "Dual Standby", + RadioSettingValueBoolean(_settings.dwait)) + basic.append(dwait) + + led = RadioSetting("led", "Background Light", RadioSettingValueList( + LIST_LED, LIST_LED[_settings.led])) + basic.append(led) + + voice = RadioSetting("voice", "Voice Prompt", RadioSettingValueList( + LIST_VOICE, LIST_VOICE[_settings.voice])) + basic.append(voice) + + roger = RadioSetting("roger", "Roger Beep", + RadioSettingValueBoolean(_settings.roger)) + basic.append(roger) + + autolk = RadioSetting("autolk", "Auto Key Lock", + RadioSettingValueBoolean(_settings.autolk)) + basic.append(autolk) + + opnset = RadioSetting("opnset", "Open Mode Set", + RadioSettingValueList( + LIST_OPNSET, LIST_OPNSET[_settings.opnset])) + basic.append(opnset) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = self._memobj.poweron_msg + ponmsg = RadioSetting("poweron_msg.line1", "Power-On Message", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + basic.append(ponmsg) + + + scans = RadioSetting("scans", "Scan Mode", RadioSettingValueList( + LIST_SCANS, LIST_SCANS[_settings.scans])) + basic.append(scans) + + dw = RadioSetting("dw", "FM Radio Dual Watch", + RadioSettingValueBoolean(_settings.dw)) + basic.append(dw) + + name = RadioSetting("name", "Display Names", + RadioSettingValueBoolean(_settings.name)) + basic.append(name) + + rptrl = RadioSetting("rptrl", "Repeater TX Delay", + RadioSettingValueList(LIST_RPTRL, LIST_RPTRL[ + _settings.rptrl])) + basic.append(rptrl) + + rptspk = RadioSetting("rptspk", "Repeater Speaker", + RadioSettingValueBoolean(_settings.rptspk)) + basic.append(rptspk) + + rptptt = RadioSetting("rptptt", "Repeater PTT Switch", + RadioSettingValueBoolean(_settings.rptptt)) + basic.append(rptptt) + + rptmod = RadioSetting("rptmod", "Repeater Mode", + RadioSettingValueList( + LIST_RPTMOD, LIST_RPTMOD[_settings.rptmod])) + basic.append(rptmod) + + volmod = RadioSetting("volmod", "Volume Mode", + RadioSettingValueList( + LIST_VOLMOD, LIST_VOLMOD[_settings.volmod])) + basic.append(volmod) + + dst = RadioSetting("dst", "DTMF Side Tone", + RadioSettingValueBoolean(_settings.dst)) + basic.append(dst) + + txsel = RadioSetting("txsel", "Priority TX Channel", + RadioSettingValueList( + LIST_TXSEL, LIST_TXSEL[_settings.txsel])) + basic.append(txsel) + + ste = RadioSetting("ste", "Squelch Tail Eliminate", + RadioSettingValueBoolean(_settings.ste)) + basic.append(ste) + + #advanced + if _settings.pf1 > 0x0A: + val = 0x00 + else: + val = _settings.pf1 + pf1 = RadioSetting("pf1", "PF1 Key", + RadioSettingValueList( + LIST_PFKEY, LIST_PFKEY[val])) + advanced.append(pf1) + + if _settings.pf2 > 0x0A: + val = 0x00 + else: + val = _settings.pf2 + pf2 = RadioSetting("pf2", "PF2 Key", + RadioSettingValueList( + LIST_PFKEY, LIST_PFKEY[val])) + advanced.append(pf2) + + # other + _limit = str(int(_mem.limits.vhf.lower) / 10) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + rs = RadioSetting("limits.vhf.lower", "VHF low", val) + other.append(rs) + + _limit = str(int(_mem.limits.vhf.upper) / 10) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + rs = RadioSetting("limits.vhf.upper", "VHF high", val) + other.append(rs) + + _limit = str(int(_mem.limits.uhf.lower) / 10) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + rs = RadioSetting("limits.uhf.lower", "UHF low", val) + other.append(rs) + + _limit = str(int(_mem.limits.uhf.upper) / 10) + val = RadioSettingValueString(0, 3, _limit) + val.set_mutable(False) + rs = RadioSetting("limits.uhf.upper", "UHF high", val) + other.append(rs) + + #work mode + vfomr_a = RadioSetting("vfomr_a", "Display Mode A", + RadioSettingValueList( + LIST_VFOMR, LIST_VFOMR[_settings.vfomr_a])) + workmode.append(vfomr_a) + + vfomr_b = RadioSetting("vfomr_b", "Display Mode B", + RadioSettingValueList( + LIST_VFOMR, LIST_VFOMR[_settings.vfomr_b])) + workmode.append(vfomr_b) + + mrcha = RadioSetting("mrcha", "Channel # A", + RadioSettingValueInteger( + 1, 128, _settings.mrcha)) + workmode.append(mrcha) + + mrchb = RadioSetting("mrchb", "Channel # B", + RadioSettingValueInteger( + 1, 128, _settings.mrchb)) + workmode.append(mrchb) + + #fm radio + vfomr_fm = RadioSetting("vfomr_fm", "FM Radio Display Mode", + RadioSettingValueList( + LIST_VFOMRFM, LIST_VFOMRFM[ + _settings.vfomr_fm])) + fmradio.append(vfomr_fm) + + fmch = RadioSetting("fmch", "FM Radio Channel #", + RadioSettingValueInteger( + 1, 25, _settings.fmch)) + fmradio.append(fmch) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "color": + setattr(obj, setting, int(element.value) + 1) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) in [0x1000, ]: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/retevis_rt26.py b/chirp/drivers/retevis_rt26.py new file mode 100644 index 0000000..3840575 --- /dev/null +++ b/chirp/drivers/retevis_rt26.py @@ -0,0 +1,919 @@ +# Copyright 2017 Jim Unroe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + InvalidValueError, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown1:1, + pttid:2, // PTT ID + skip:1, // Scan Add + wide:1, // Bandwidth + bcl:1, // Busy Lock + epilogue:1, // Epilogue (STE) + highpower:1; // Power Level + u8 unknown2[3]; +} memory[16]; + +#seekto 0x0120; +struct { + u8 hivoltnotx:1, // TX Inhibit when voltage too high + lovoltnotx:1, // TX Inhibit when voltage too low + unknown1:1, + fmradio:1, // Broadcast FM Radio + unknown2:1, + tone:1, // Tone + voice:2; // Voice + u8 unknown3:1, + save:3, // Battery Save + squelch:4; // Squelch + u8 tot; // Time Out Timer + u8 voxi:1, // VOX Inhibit on Receive + voxd:2, // VOX Delay + vox:1, // VOX + voxg:4; // VOX Gain + u8 unknown4; + u8 unknown5:4, + scanspeed:4; // Scan Speed + u8 unknown6:3, + scandelay:5; // Scan Delay + u8 k1longp:4, // Key 1 Long Press + k1shortp:4; // Key 1 Short Press + u8 k2longp:4, // Key 2 Long Press + k2shortp:4; // Key 2 Short Press + u8 unknown7:4, + ssave:4; // Super Battery Save +} settings; + +#seekto 0x0140; +struct { + u8 unknown1:4, + dtmfspd:4; // DTMF Speed + u8 digdelay:4, // 1st Digit Delay + digtime:4; // 1st Digit Time + u8 stuntype:1, // Stun Type + sidetone:1, // DTMF Sidetone + starhash:2, // * and # Time + decodetone:1, // Decode Tone + txdecode:1, // TX Decode + unknown2:2; + u8 unknown3; + u8 unknown4:4, + groupcode:4; // Group Code + u8 unknown5:1, + resettone:1, // Reset Tone + resettime:6; // Reset Time + u8 codespace:4, // Code Space Time + decodeto:4; // Decode Tome Out + u8 unknown6; + u8 idcode[3]; // ID Code + u8 unknown7[2]; + u8 code1_len; // PTT ID length(begging of TX) + u8 code2_len; // PTT ID length(end of TX) + u8 unknown8; + u8 code3_len; // Stun Code length + u8 code3[5]; // Stun Code + u8 unknown9[10]; + u8 code1[8]; // PTT ID(beggining of TX) + u8 code2[8]; // PTT ID(end of TX) +} dtmf; + +#seekto 0x0170; +struct { + char fp[8]; +} fingerprint; +""" + +CMD_ACK = "\x06" + +NUMERIC_CHARSET = list("0123456789") +DTMF_CHARSET = NUMERIC_CHARSET + list("ABCD*#") + +RT26_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("High", watts=10.00)] + +RT26_DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_SHORT_PRESS = ["Off", "Monitor On/Off", "", "Scan", "Alarm", + "Power High/Low"] +LIST_LONG_PRESS = ["Off", "Monitor On/Off", "Monitor(momentary)", + "Scan", "Alarm", "Power High/Low"] +LIST_VOXDELAY = ["0.5", "1.0", "2.0", "3.0"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_TIMEOUTTIMER = ["Off"] + ["%s" % x for x in range(15, 615, 15)] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SSAVE = ["Off"] + ["%s" % x for x in range(1, 10)] +LIST_SCANSPEED = ["%s" % x for x in range(100, 550, 50)] +LIST_SCANDELAY = ["%s" % x for x in range(3, 31)] +LIST_DIGTIME = ["%s" % x for x in range(0, 1100, 100)] +LIST_DIGDELAY = ["%s" % x for x in range(100, 1100, 100)] +LIST_STARHASH = ["100", "500", "1000", "0"] +LIST_CODESPACE = ["None"] + ["%s" % x for x in range(600, 2100, 100)] +LIST_GROUPCODE = ["Off", "A", "B", "C", "D", "#", "*"] +LIST_RESETTIME = ["Off"] + ["%s" % x for x in range(1, 61)] +LIST_DECODETO = ["%s" % x for x in range(500, 1000, 50)] + \ + ["%s" % x for x in range(1000, 1600, 100)] +LIST_STUNTYPE = ["TX/RX Inhibit", "TX Inhibit"] + +SETTING_LISTS = { + "k1shortp": LIST_SHORT_PRESS, + "k1longp": LIST_LONG_PRESS, + "k2shortp": LIST_SHORT_PRESS, + "k2longp": LIST_LONG_PRESS, + "voxd": LIST_VOXDELAY, + "voice": LIST_VOICE, + "tot": LIST_TIMEOUTTIMER, + "save": LIST_SAVE, + "ssave": LIST_SSAVE, + "scanspeed": LIST_SCANSPEED, + "scandelay": LIST_SCANDELAY, + "digtime": LIST_DIGTIME, + "digdelay": LIST_DIGDELAY, + "starhash" : LIST_STARHASH, + "codespace" : LIST_CODESPACE, + "groupcode" : LIST_GROUPCODE, + "resettime" : LIST_RESETTIME, + "decodeto" : LIST_DECODETO, + "stuntype" : LIST_STUNTYPE, + } + +# Retevis RT26 fingerprints +RT26_UHF_fp = "PDK80" + "\xF3\x00\x00" # RT26 UHF model + +MODELS = [RT26_UHF_fp,] + + +def _model_from_data(data): + return data[0x0170:0x0178] + + +def _model_from_image(radio): + return _model_from_data(radio.get_mmap()) + + +def _get_radio_model(radio): + block = _rt26_read_block(radio, 0x0170, 0x10) + version = block[0:8] + return version + + +def _rt26_enter_programming_mode(radio): + serial = radio.pipe + + magic = ["PROGRAMa", "PROGRAMb"] + for i in range(0, 2): + + try: + LOG.debug("sending " + magic[i]) + serial.write(magic[i]) + ack = serial.read(1) + except: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if not ack: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("No response from radio") + elif ack != CMD_ACK: + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ack)) + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + try: + LOG.debug("sending " + util.hexprint("\x02")) + serial.write("\x02") + ident = serial.read(16) + except: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith("PDK80"): + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ident)) + _rt26_exit_programming_mode(radio) + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + + try: + LOG.debug("sending " + util.hexprint("MDK8ECUMHS1X7BN/")) + serial.write("MXT8KCUMHS1X7BN/") + ack = serial.read(1) + except: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != "\xB2": + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ack)) + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + try: + LOG.debug("sending " + util.hexprint(CMD_ACK)) + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Error communicating with radio") + + if ack != CMD_ACK: + LOG.debug("Incorrect response, got this:\n\n" + util.hexprint(ack)) + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Radio refused to enter programming mode") + + # DEBUG + LOG.info("Positive ident, this is a %s %s" % (radio.VENDOR, radio.MODEL)) + + +def _rt26_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write("E") + except: + raise errors.RadioError("Radio refused to exit programming mode") + + +def _rt26_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = "W" + cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + _rt26_exit_programming_mode(radio) + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + + serial.write(CMD_ACK) + ack = serial.read(1) + except: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + if ack != CMD_ACK: + _rt26_exit_programming_mode(radio) + raise Exception("No ACK reading block %04x." % (block_addr)) + + return block_data + + +def _rt26_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + _rt26_exit_programming_mode(radio) + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + +def do_download(radio): + LOG.debug("download") + _rt26_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio._block_size): + status.cur = addr + radio._block_size + radio.status_fn(status) + + block = _rt26_read_block(radio, addr, radio._block_size) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _rt26_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _rt26_enter_programming_mode(radio) + + status.cur = 0 + status.max = 0x0190 + + for start_addr, end_addr, block_size in radio._ranges: + for addr in range(start_addr, end_addr, block_size): + status.cur = addr + block_size + radio.status_fn(status) + _rt26_write_block(radio, addr, block_size) + + _rt26_exit_programming_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x0170:0x0176] + + return rid.startswith("PDK80") + + +@directory.register +class RT26Radio(chirp_common.CloneModeRadio): + """Retevis RT26""" + VENDOR = "Retevis" + MODEL = "RT26" + BAUD_RATE = 4800 + + _ranges = [ + (0x0000, 0x0190, 0x10), + ] + _memsize = 0x0400 + _block_size = 0x10 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = False + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = RT26_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["NFM", "FM"] # 12.5 KHz, 25 kHz. + rf.memory_bounds = (1, 16) + rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 25.] + rf.valid_bands = [(400000000, 520000000)] + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + do_upload(self) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def _my_band(self): + model_tag = _model_from_image(self) + return model_tag + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + + mem.number = number + mem.freq = int(_mem.rxfreq) * 10 + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + return mem + + if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF": + mem.freq = 0 + mem.empty = True + return mem + + if int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.mode = _mem.wide and "FM" or "NFM" + + rxtone = txtone = None + txtone = self.decode_tone(_mem.txtone) + rxtone = self.decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.power = RT26_POWER_LEVELS[_mem.highpower] + + if _mem.skip: + mem.skip = "S" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(not _mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("epilogue", "Epilogue(STE)", + RadioSettingValueBoolean(_mem.epilogue)) + mem.extra.append(rs) + + val = 3 - _mem.pttid + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList( + LIST_PTTID, LIST_PTTID[val])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _mem.wide = mem.mode == "FM" + + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.txtone, txmode, txtone, txpol) + self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol) + + _mem.highpower = mem.power == RT26_POWER_LEVELS[1] + + _mem.skip = mem.skip == "S" + + for setting in mem.extra: + if setting.get_name() == "bcl": + setattr(_mem, setting.get_name(), not int(setting.value)) + elif setting.get_name() == "pttid": + setattr(_mem, setting.get_name(), 3 - int(setting.value)) + else: + setattr(_mem, setting.get_name(), int(setting.value)) + + def _bbcd2dtmf(self, bcdarr, strlen=16): + # doing bbcd, but with support for ABCD*# + LOG.debug(bcdarr.get_value()) + string = ''.join("%02X" % b for b in bcdarr) + LOG.debug("@_bbcd2dtmf, received: %s" % string) + string = string.replace('E', '#').replace('F', '*') + if strlen <= 16: + string = string[:strlen] + return string + + def _dtmf2bbcd(self, value, strlen): + dtmfstr = value.get_value() + dtmfstr = dtmfstr.replace('#', 'E').replace('*', 'F') + dtmfstr = str.ljust(dtmfstr.strip(), strlen, "F") + bcdarr = list(bytearray.fromhex(dtmfstr)) + LOG.debug("@_dtmf2bbcd, sending: %s" % bcdarr) + return bcdarr + + def _bbcd2num(self, bcdarr, strlen=6): + # doing bbcd + LOG.debug(bcdarr.get_value()) + string = ''.join("%02X" % b for b in bcdarr) + LOG.debug("@_bbcd2num, received: %s" % string) + if strlen <= 6: + string = string[:strlen] + return string + + def _num2bbcd(self, value): + numstr = value.get_value() + numstr = str.ljust(numstr.strip(), 6, "F") + bcdarr = list(bytearray.fromhex(numstr)) + LOG.debug("@_num2bbcd, sending: %s" % bcdarr) + return bcdarr + + def get_settings(self): + _settings = self._memobj.settings + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + top = RadioSettings(basic, dtmf) + + if _settings.k1shortp > 5: + val = 4 + else: + val = _settings.k1shortp + rs = RadioSetting("k1shortp", "Key 1 Short Press", + RadioSettingValueList( + LIST_SHORT_PRESS, + LIST_SHORT_PRESS[val])) + basic.append(rs) + + if _settings.k1longp > 5: + val = 5 + else: + val = _settings.k1longp + rs = RadioSetting("k1longp", "Key 1 Long Press", + RadioSettingValueList( + LIST_LONG_PRESS, + LIST_LONG_PRESS[val])) + basic.append(rs) + + if _settings.k2shortp > 5: + val = 1 + else: + val = _settings.k2shortp + rs = RadioSetting("k2shortp", "Key 2 Short Press", + RadioSettingValueList( + LIST_SHORT_PRESS, + LIST_SHORT_PRESS[val])) + basic.append(rs) + + if _settings.k2longp > 5: + val = 3 + else: + val = _settings.k2longp + rs = RadioSetting("k2longp", "Key 2 Long Press", + RadioSettingValueList( + LIST_LONG_PRESS, + LIST_LONG_PRESS[val])) + basic.append(rs) + + rs = RadioSetting("vox", "VOX", + RadioSettingValueBoolean(not _settings.vox)) + basic.append(rs) + + if _settings.voxg > 8: + val = 4 + else: + val = _settings.voxg + 1 + rs = RadioSetting("voxg", "VOX Gain", + RadioSettingValueInteger(1, 9, val)) + basic.append(rs) + + rs = RadioSetting("voxd", "VOX Delay Time", + RadioSettingValueList( + LIST_VOXDELAY, + LIST_VOXDELAY[_settings.voxd])) + basic.append(rs) + + rs = RadioSetting("voxi", "VOX Inhibit on Receive", + RadioSettingValueBoolean(_settings.voxi)) + basic.append(rs) + + if _settings.squelch > 9: + val = 5 + else: + val = _settings.squelch + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, val)) + basic.append(rs) + + if _settings.voice == 3: + val = 1 + else: + val = _settings.voice + rs = RadioSetting("voice", "Voice Prompts", + RadioSettingValueList( + LIST_VOICE, + LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("tone", "Tone", + RadioSettingValueBoolean(_settings.tone)) + basic.append(rs) + + rs = RadioSetting("lovoltnotx", "TX Inhibit (when battery < 6 volts)", + RadioSettingValueBoolean(_settings.lovoltnotx)) + basic.append(rs) + + rs = RadioSetting("hivoltnotx", "TX Inhibit (when battery > 9 volts)", + RadioSettingValueBoolean(_settings.hivoltnotx)) + basic.append(rs) + + if _settings.tot > 0x28: + val = 6 + else: + val = _settings.tot + rs = RadioSetting("tot", "Time-out Timer[s]", + RadioSettingValueList( + LIST_TIMEOUTTIMER, + LIST_TIMEOUTTIMER[val])) + basic.append(rs) + + if _settings.save < 3: + val = 0 + else: + val = _settings.save - 3 + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, + LIST_SAVE[val])) + basic.append(rs) + + rs = RadioSetting("ssave", "Super Battery Saver[s]", + RadioSettingValueList( + LIST_SSAVE, + LIST_SSAVE[_settings.ssave])) + basic.append(rs) + + rs = RadioSetting("fmradio", "Broadcast FM", + RadioSettingValueBoolean(_settings.fmradio)) + basic.append(rs) + + if _settings.scanspeed > 8: + val = 4 + else: + val = _settings.scanspeed + rs = RadioSetting("scanspeed", "Scan Speed[ms]", + RadioSettingValueList( + LIST_SCANSPEED, + LIST_SCANSPEED[val])) + basic.append(rs) + + if _settings.scandelay > 27: + val = 12 + else: + val = _settings.scandelay + rs = RadioSetting("scandelay", "Scan Droupout Delay Time[s]", + RadioSettingValueList( + LIST_SCANDELAY, + LIST_SCANDELAY[val])) + basic.append(rs) + + if _mem.dtmf.dtmfspd > 11: + val = 2 + else: + val = _mem.dtmf.dtmfspd + 4 + rs = RadioSetting("dtmf.dtmfspd", "DTMF Speed[digit/s]", + RadioSettingValueInteger(4, 15, val)) + dtmf.append(rs) + + if _mem.dtmf.digtime > 10: + val = 0 + else: + val = _mem.dtmf.digtime + rs = RadioSetting("dtmf.digtime", "1st Digit Time[ms]", + RadioSettingValueList( + LIST_DIGTIME, + LIST_DIGTIME[val])) + dtmf.append(rs) + + if _mem.dtmf.digdelay > 9: + val = 0 + else: + val = _mem.dtmf.digdelay + rs = RadioSetting("dtmf.digdelay", "1st Digit Delay[ms]", + RadioSettingValueList( + LIST_DIGDELAY, + LIST_DIGDELAY[val])) + dtmf.append(rs) + + rs = RadioSetting("dtmf.starhash", "* and # Time[ms]", + RadioSettingValueList( + LIST_STARHASH, + LIST_STARHASH[_mem.dtmf.starhash])) + dtmf.append(rs) + + rs = RadioSetting("dtmf.codespace", "Code Space Time[ms]", + RadioSettingValueList( + LIST_CODESPACE, + LIST_CODESPACE[_mem.dtmf.codespace])) + dtmf.append(rs) + + rs = RadioSetting("dtmf.sidetone", "DTMF Sidetone", + RadioSettingValueBoolean(_mem.dtmf.sidetone)) + dtmf.append(rs) + + # setup pttid entries + for i in range(0, 2): + objname = "code" + str(i + 1) + names = ["PTT ID(BOT)", "PTT ID(EOT)"] + strname = str(names[i]) + dtmfsetting = getattr(_mem.dtmf, objname) + dtmflen = getattr(_mem.dtmf, objname + "_len") + dtmfstr = self._bbcd2dtmf(dtmfsetting, dtmflen) + code = RadioSettingValueString(0, 16, dtmfstr) + code.set_charset(DTMF_CHARSET + list(" ")) + rs = RadioSetting("dtmf." + objname, strname, code) + dtmf.append(rs) + + def _filter(name): + filtered = "" + for char in str(name): + if char in NUMERIC_CHARSET: + filtered += char + else: + filtered += " " + return filtered + + # setup id code entry + codesetting = getattr(_mem.dtmf, "idcode") + codestr = self._bbcd2num(codesetting, 6) + code = RadioSettingValueString(0, 6, _filter(codestr)) + code.set_charset(NUMERIC_CHARSET + list(" ")) + rs = RadioSetting("dtmf.idcode", "ID Code", code) + dtmf.append(rs) + + if _mem.dtmf.groupcode > 6: + val = 0 + else: + val = _mem.dtmf.groupcode + rs = RadioSetting("dtmf.groupcode", "Group Code", + RadioSettingValueList( + LIST_GROUPCODE, + LIST_GROUPCODE[val])) + dtmf.append(rs) + + if _mem.dtmf.resettime > 60: + val = 0 + else: + val = _mem.dtmf.resettime + rs = RadioSetting("dtmf.resettime", "Auto Reset Time[s]", + RadioSettingValueList( + LIST_RESETTIME, + LIST_RESETTIME[_mem.dtmf.resettime])) + dtmf.append(rs) + + rs = RadioSetting("dtmf.txdecode", "TX Decode", + RadioSettingValueBoolean(_mem.dtmf.txdecode)) + dtmf.append(rs) + + rs = RadioSetting("dtmf.decodeto", "Decode Time Out[ms]", + RadioSettingValueList( + LIST_DECODETO, + LIST_DECODETO[_mem.dtmf.decodeto])) + dtmf.append(rs) + + rs = RadioSetting("dtmf.decodetone", "Decode Tone", + RadioSettingValueBoolean(_mem.dtmf.decodetone)) + dtmf.append(rs) + + rs = RadioSetting("dtmf.resettone", "Reset Tone", + RadioSettingValueBoolean(_mem.dtmf.resettone)) + dtmf.append(rs) + + rs = RadioSetting("dtmf.stuntype", "Stun Type", + RadioSettingValueList( + LIST_STUNTYPE, + LIST_STUNTYPE[_mem.dtmf.stuntype])) + dtmf.append(rs) + + ## setup stun entry + objname = "code3" + strname = "Stun Code" + dtmfsetting = getattr(_mem.dtmf, objname) + dtmflen = getattr(_mem.dtmf, objname + "_len") + dtmfstr = self._bbcd2dtmf(dtmfsetting, dtmflen) + code = RadioSettingValueString(0, 10, dtmfstr) + code.set_charset(DTMF_CHARSET + list(" ")) + rs = RadioSetting("dtmf." + objname, strname, code) + dtmf.append(rs) + + return top + + def set_settings(self, settings): + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if setting == "vox": + setattr(obj, setting, not int(element.value)) + elif setting == "voxg": + setattr(obj, setting, int(element.value) - 1) + elif setting == "save": + setattr(obj, setting, int(element.value) + 3) + elif setting == "dtmfspd": + setattr(obj, setting, int(element.value) - 4) + elif re.match('code\d', setting): + # set dtmf length field and then get bcd dtmf + if setting == "code3": + strlen = 10 + else: + strlen = 16 + codelen = len(str(element.value).strip()) + setattr(_mem.dtmf, setting + "_len", codelen) + dtmfstr = self._dtmf2bbcd(element.value, strlen) + setattr(_mem.dtmf, setting, dtmfstr) + elif setting == "idcode": + numstr = self._num2bbcd(element.value) + setattr(_mem.dtmf, setting, numstr) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) in [0x0400, ]: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/rfinder.py b/chirp/drivers/rfinder.py new file mode 100644 index 0000000..cedf835 --- /dev/null +++ b/chirp/drivers/rfinder.py @@ -0,0 +1,335 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import urllib +import hashlib +import re +import logging + +from math import pi, cos, acos, sin, atan2 +from chirp import chirp_common, CHIRP_VERSION + +LOG = logging.getLogger(__name__) + +EARTH_RADIUS = 3963.1 + +SCHEMA = [ + "ID", + "TRUSTEE", + "OUTFREQUENCY", + "CITY", + "STATE", + "COUNTRY", + "LATITUDE", + "LONGITUDE", + "CLUB", + "DESCRIPTION", + "NOTES", + "RANGE", + "OFFSETSIGN", + "OFFSETFREQ", + "PL", + "DCS", + "REPEATERTYPE", + "BAND", + "IRLP", + "ECHOLINK", + "DOC_ID", + ] + + +def deg2rad(deg): + """Convert degrees to radians""" + return deg * (pi / 180) + + +def rad2deg(rad): + """Convert radians to degrees""" + return rad / (pi / 180) + + +def dm2deg(degrees, minutes): + """Convert degrees and minutes to decimal degrees""" + return degrees + (minutes / 60.0) + + +def deg2dm(decdeg): + """Convert decimal degrees to degrees and minutes""" + degrees = int(decdeg) + minutes = (decdeg - degrees) * 60.0 + + return degrees, minutes + + +def nmea2deg(nmea, direction="N"): + """Convert NMEA-encoded value to float""" + deg = int(nmea) / 100 + try: + minutes = nmea % (deg * 100) + except ZeroDivisionError: + minutes = int(nmea) + + if direction == "S" or direction == "W": + sign = -1 + else: + sign = 1 + + return dm2deg(deg, minutes) * sign + + +def deg2nmea(deg): + """Convert degrees to a NMEA-encoded value""" + degrees, minutes = deg2dm(deg) + + return (degrees * 100) + minutes + + +def meters2feet(meters): + """Convert meters to feet""" + return meters * 3.2808399 + + +def feet2meters(feet): + """Convert feet to meters""" + return feet * 0.3048 + + +def distance(lat_a, lon_a, lat_b, lon_b): + """Calculate the distance between two points""" + lat_a = deg2rad(lat_a) + lon_a = deg2rad(lon_a) + + lat_b = deg2rad(lat_b) + lon_b = deg2rad(lon_b) + + earth_radius = EARTH_RADIUS + + tmp = (cos(lat_a) * cos(lon_a) * cos(lat_b) * cos(lon_b)) + \ + (cos(lat_a) * sin(lon_a) * cos(lat_b) * sin(lon_b)) + \ + (sin(lat_a) * sin(lat_b)) + + # Correct round-off error (which is just *silly*) + if tmp > 1: + tmp = 1 + elif tmp < -1: + tmp = -1 + + dist = acos(tmp) + + return dist * earth_radius + + +def bearing(lat_a, lon_a, lat_b, lon_b): + """Calculate the bearing between two points""" + lat_me = deg2rad(lat_a) + lat_u = deg2rad(lat_b) + lon_d = deg2rad(lon_b - lon_a) + + posy = sin(lon_d) * cos(lat_u) + posx = cos(lat_me) * sin(lat_u) - \ + sin(lat_me) * cos(lat_u) * cos(lon_d) + + bear = rad2deg(atan2(posy, posx)) + + return (bear + 360) % 360 + + +def fuzzy_to(lat_a, lon_a, lat_b, lon_b): + """Calculate a fuzzy distance to a point""" + bear = bearing(lat_a, lon_a, lat_b, lon_b) + + dirs = ["N", "NNE", "NE", "ENE", "E", + "ESE", "SE", "SSE", "S", + "SSW", "SW", "WSW", "W", + "WNW", "NW", "NNW"] + + delta = 22.5 + angle = 0 + + direction = "?" + for i in dirs: + if bear > angle and bear < (angle + delta): + direction = i + angle += delta + + return direction + + +class RFinderParser: + """Parser for RFinder's data format""" + def __init__(self, lat, lon): + self.__memories = [] + self.__cheat = {} + self.__lat = lat + self.__lon = lon + + def fetch_data(self, user, pw, coords, radius): + """Fetches the data for a set of parameters""" + LOG.debug(user) + LOG.debug(pw) + args = { + "email": urllib.quote_plus(user), + "pass": hashlib.new("md5", pw).hexdigest(), + "lat": "%7.5f" % coords[0], + "lon": "%7.5f" % coords[1], + "radius": "%i" % radius, + "vers": "CH%s" % CHIRP_VERSION, + } + + _url = "https://www.rfinder.net/query.php?%s" % \ + ("&".join(["%s=%s" % (k, v) for k, v in args.items()])) + + LOG.debug("Query URL: %s" % _url) + + f = urllib.urlopen(_url) + data = f.read() + f.close() + + match = re.match("^/#SERVERMSG#/(.*)/#ENDMSG#/", data) + if match: + raise Exception(match.groups()[0]) + + return data + + def _parse_line(self, line): + mem = chirp_common.Memory() + + _vals = line.split("|") + + vals = {} + for i in range(0, len(SCHEMA)): + try: + vals[SCHEMA[i]] = _vals[i] + except IndexError: + LOG.error("No such vals %s" % SCHEMA[i]) + self.__cheat = vals + + mem.name = vals["TRUSTEE"] + mem.freq = chirp_common.parse_freq(vals["OUTFREQUENCY"]) + if vals["OFFSETSIGN"] != "X": + mem.duplex = vals["OFFSETSIGN"] + if vals["OFFSETFREQ"]: + mem.offset = chirp_common.parse_freq(vals["OFFSETFREQ"]) + + if vals["PL"] and float(vals["PL"]) != 0: + mem.rtone = float(vals["PL"]) + mem.tmode = "Tone" + elif vals["DCS"] and vals["DCS"] != "0": + mem.dtcs = int(vals["DCS"]) + mem.tmode = "DTCS" + + if vals["NOTES"]: + mem.comment = vals["NOTES"].strip() + + if vals["LATITUDE"] and vals["LONGITUDE"]: + try: + lat = float(vals["LATITUDE"]) + lon = float(vals["LONGITUDE"]) + dist = distance(self.__lat, self.__lon, lat, lon) + bear = fuzzy_to(self.__lat, self.__lon, lat, lon) + mem.comment = "(%imi %s) %s" % (dist, bear, mem.comment) + except Exception, e: + LOG.error("Failed to calculate distance: %s" % e) + + return mem + + def parse_data(self, data): + """Parse the fetched data""" + number = 1 + for line in data.split("\n"): + if line.startswith("<"): + continue + elif not line.strip(): + continue + try: + mem = self._parse_line(line) + mem.number = number + number += 1 + self.__memories.append(mem) + except Exception, e: + import traceback + LOG.error(traceback.format_exc()) + LOG.error("Error in received data, cannot continue") + LOG.error("rfinder.parse_data: %s", e) + LOG.error(self.__cheat) + LOG.error(line) + + def get_memories(self): + """Return the Memory objects associated with the fetched data""" + return self.__memories + + +class RFinderRadio(chirp_common.NetworkSourceRadio): + """A network source radio that supports the RFinder repeater directory""" + VENDOR = "ITWeRKS" + MODEL = "RFinder" + + def __init__(self, *args, **kwargs): + chirp_common.NetworkSourceRadio.__init__(self, *args, **kwargs) + + self._lat = 0 + self._lon = 0 + self._user = "" + self._pass = "" + self._miles = 25 + + self._rfp = None + + def set_params(self, (lat, lon), miles, email, password): + """Sets the parameters to use for the query""" + self._lat = lat + self._lon = lon + self._miles = miles + self._user = email + self._pass = password + + def do_fetch(self): + self._rfp = RFinderParser(self._lat, self._lon) + + self._rfp.parse_data(self._rfp.fetch_data(self._user, + self._pass, + (self._lat, self._lon), + self._miles)) + + def get_features(self): + if not self._rfp: + self.do_fetch() + + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, len(self._rfp.get_memories())) + rf.has_bank = False + rf.has_ctone = False + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_modes = ["", "FM", "NFM", "AM", "NAM", "DV"] + return rf + + def get_memory(self, number): + if not self._rfp: + self.do_fetch() + + return self._rfp.get_memories()[number-1] + + +def _test(): + rfp = RFinderParser() + data = rfp.fetch_data("KK7DS", "dsmith@danplanet.com", + (45.5, -122.91), 25) + rfp.parse_data(data) + + for mem in rfp.get_memories(): + LOG.debug(mem) + +if __name__ == "__main__": + _test() diff --git a/chirp/drivers/rh5r_v2.py b/chirp/drivers/rh5r_v2.py new file mode 100644 index 0000000..01ffc96 --- /dev/null +++ b/chirp/drivers/rh5r_v2.py @@ -0,0 +1,290 @@ +# Copyright 2017 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Rugged RH5R V2 radio management module""" + +import struct +import logging + +from chirp import chirp_common, bitwise, errors, directory, memmap, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + + +LOG = logging.getLogger(__name__) + + +def _identify(radio): + try: + radio.pipe.write("PGM2015") + ack = radio.pipe.read(2) + if ack != "\x06\x30": + raise errors.RadioError("Radio did not ACK first command: %r" % + ack) + except: + LOG.exception('') + raise errors.RadioError("Unable to communicate with the radio") + + +def _download(radio): + _identify(radio) + data = [] + for i in range(0, 0x2000, 0x40): + msg = struct.pack('>cHb', 'R', i, 0x40) + radio.pipe.write(msg) + block = radio.pipe.read(0x40 + 4) + if len(block) != (0x40 + 4): + raise errors.RadioError("Radio sent a short block (%02x/%02x)" % ( + len(block), 0x44)) + data += block[4:] + + if radio.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = 0x2000 + status.msg = "Cloning from radio" + radio.status_fn(status) + + radio.pipe.write("E") + data += 'PGM2015' + + return memmap.MemoryMap(data) + + +def _upload(radio): + _identify(radio) + for i in range(0, 0x2000, 0x40): + msg = struct.pack('>cHb', 'W', i, 0x40) + msg += radio._mmap[i:(i + 0x40)] + radio.pipe.write(msg) + ack = radio.pipe.read(1) + if ack != '\x06': + raise errors.RadioError('Radio did not ACK block %i (0x%04x)' % ( + i, i)) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = 0x2000 + status.msg = "Cloning from radio" + radio.status_fn(status) + + radio.pipe.write("E") + + +MEM_FORMAT = """ +struct memory { + bbcd rx_freq[4]; + bbcd tx_freq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + + u8 unknown10:5, + highpower:1, + unknown11:2; + u8 unknown20:4, + narrow:1, + unknown21:3; + u8 unknown31:1, + scanadd:1, + unknown32:6; + u8 unknown4; +}; + +struct name { + char name[7]; +}; + +#seekto 0x0010; +struct memory channels[128]; + +#seekto 0x08C0; +struct name names[128]; + +#seekto 0x2020; +struct memory vfo1; +struct memory vfo2; +""" + + +POWER_LEVELS = [chirp_common.PowerLevel('Low', watts=1), + chirp_common.PowerLevel('High', watts=5)] + + +class TYTTHUVF8_V2(chirp_common.CloneModeRadio): + VENDOR = "TYT" + MODEL = "TH-UVF8F" + BAUD_RATE = 9600 + _FILEID = 'OEMOEM \XFF' + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 128) + rf.has_bank = False + rf.has_ctone = True + rf.has_tuning_step = False + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_settings = False + rf.can_odd_split = False + rf.valid_duplexes = ['', '-', '+'] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-" + rf.valid_bands = [(136000000, 174000000), + (400000000, 480000000)] + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_modes = ["FM", "NFM"] + rf.valid_name_length = 7 + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + return rf + + def sync_in(self): + self._mmap = _download(self) + self.process_mmap() + + def sync_out(self): + _upload(self) + + @classmethod + def match_model(cls, filedata, filename): + return (filedata.endswith("PGM2015") and + filedata[0x840:0x848] == cls._FILEID) + + def process_mmap(self): + print MEM_FORMAT + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return (repr(self._memobj.channels[number - 1]) + + repr(self._memobj.names[number - 1])) + + def _get_memobjs(self, number): + return (self._memobj.channels[number - 1], + self._memobj.names[number - 1]) + + def _decode_tone(self, toneval): + pol = "N" + rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF) + + if toneval[0].get_bits(0xFF) == 0xFF: + mode = "" + val = 0 + elif toneval[1].get_bits(0xC0) == 0xC0: + mode = "DTCS" + val = int("%x" % (rawval & 0x3FFF)) + pol = "R" + elif toneval[1].get_bits(0x80): + mode = "DTCS" + val = int("%x" % (rawval & 0x3FFF)) + else: + mode = "Tone" + val = int(toneval) / 10.0 + + return mode, val, pol + + def _encode_tone(self, _toneval, mode, val, pol): + toneval = 0 + if mode == "Tone": + toneval = int("%i" % (val * 10), 16) + elif mode == "DTCS": + toneval = int("%i" % val, 16) + toneval |= 0x8000 + if pol == "R": + toneval |= 0x4000 + else: + toneval = 0xFFFF + + _toneval[0].set_raw(toneval & 0xFF) + _toneval[1].set_raw((toneval >> 8) & 0xFF) + + def get_memory(self, number): + _mem, _name = self._get_memobjs(number) + + mem = chirp_common.Memory() + + if isinstance(number, str): + mem.number = SPECIALS[number] + mem.extd_number = number + else: + mem.number = number + + if _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"): + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10 + if not offset: + mem.offset = 0 + mem.duplex = '' + elif offset < 0: + mem.offset = abs(offset) + mem.duplex = '-' + else: + mem.offset = offset + mem.duplex = '+' + + txmode, txval, txpol = self._decode_tone(_mem.tx_tone) + rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone) + + chirp_common.split_tone_decode(mem, + (txmode, txval, txpol), + (rxmode, rxval, rxpol)) + + mem.mode = 'NFM' if _mem.narrow else 'FM' + mem.skip = '' if _mem.scanadd else 'S' + mem.power = POWER_LEVELS[int(_mem.highpower)] + mem.name = str(_name.name).rstrip('\xFF ') + + return mem + + def set_memory(self, mem): + _mem, _name = self._get_memobjs(mem.number) + if mem.empty: + _mem.set_raw('\xFF' * 16) + _name.set_raw('\xFF' * 7) + return + _mem.set_raw('\x00' * 16) + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == '-': + mult = -1 + elif not mem.duplex: + mult = 0 + else: + mult = 1 + _mem.tx_freq = (mem.freq + (mem.offset * mult)) / 10 + + (txmode, txval, txpol), (rxmode, rxval, rxpol) = \ + chirp_common.split_tone_encode(mem) + + self._encode_tone(_mem.tx_tone, txmode, txval, txpol) + self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol) + + _mem.narrow = mem.mode == 'NFM' + _mem.scanadd = mem.skip != 'S' + _mem.highpower = POWER_LEVELS.index(mem.power) if mem.power else 1 + _name.name = mem.name.rstrip(' ').ljust(7, '\xFF') + + +@directory.register +class RH5RV2(TYTTHUVF8_V2): + VENDOR = "Rugged" + MODEL = "RH5R-V2" + _FILEID = 'RUGGED \xFF' diff --git a/chirp/drivers/tdxone_tdq8a.py b/chirp/drivers/tdxone_tdq8a.py new file mode 100644 index 0000000..6793a9f --- /dev/null +++ b/chirp/drivers/tdxone_tdq8a.py @@ -0,0 +1,1155 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, \ + InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown1:2, + dtmf:1, // DTMF + unknown2:1, + bcl:1, // Busy Channel Lockout + unknown3:3; + u8 unknown4:1, + scan:1, // Scan Add + highpower:1, // TX Power Level + wide:1, // BandWidth + unknown5:4; + u8 unknown6[2]; +} memory[128]; + +#seekto 0x0E17; +struct { + u8 displayab:1, // Selected Display + unknown1:6, + unknown2:1; +} settings1; + +#seekto 0x0E22; +struct { + u8 squelcha; // menu 02a Squelch Level 0xe22 + u8 unknown1; + u8 tdrab; // TDR A/B 0xe24 + u8 roger; // menu 20 Roger Beep 0xe25 + u8 timeout; // menu 16 TOT 0xe26 + u8 vox; // menu 05 VOX 0xe27 + u8 unknown2; + u8 mdfb; // menu 27b Memory Display Format B 0xe37 + u8 dw; // menu 37 DW 0xe2a + u8 tdr; // menu 29 Dual Watch 0xe2b + u8 voice; // menu 03 Voice Prompts 0xe2c + u8 beep; // menu 01 Key Beep 0xe2d + u8 ani; // menu 30 ANI 0xe2e + u8 unknown3[4]; + u8 pttdly; // menu 31 PTT-ID Delay 0xe33 + u8 unknown4; + u8 dtmfst; // menu 33 DTMF Side Tone 0xe35 + u8 toa; // menu 15 TOT Pre-Alert 0xe36 + u8 mdfa; // menu 27a Memory Display Format A 0xe37 + u8 screv; // menu 09 Scan Resume Method 0xe38 + u8 pttid; // menu 32 PTT-ID Enable 0xe39 + u8 ponmsg; // menu 36 Power-on Message 0xe3a + u8 pf1; // menu 28 Programmable Function Key 0xe3b + u8 unknown5; + u8 wtled; // menu 17 Standby LED Color 0xe3d + u8 rxled; // menu 18 RX LED Color 0xe3e + u8 txled; // menu 19 TX LED Color 0xe3f + u8 unknown6; + u8 autolk; // menu 06 Auto Key Lock 0xe41 + u8 squelchb; // menu 02b Squelch Level 0xe42 + u8 control; // Control Code 0xe43 + u8 unknown7; + u8 ach; // Selected A channel Number 0xe45 + u8 unknown8[4]; + u8 password[6]; // Control Password 0xe4a-0xe4f + u8 unknown9[7]; + u8 code[3]; // PTT ID Code 0xe57-0xe59 + u8 vfomr; // Frequency/Channel Modevel 0xe5a + u8 keylk; // Key Lock 0xe5b + u8 unknown10[2]; + u8 prioritych; // Priority Channel 0xe5e + u8 bch; // Selected B channel Number 0xe5f +} settings; + +struct vfo { + u8 unknown0[8]; + u8 freq[8]; + u8 offset[6]; + ul16 rxtone; + ul16 txtone; + u8 unused0:7, + band:1; + u8 unknown3; + u8 unknown4:2, + sftd:2, + scode:4; + u8 unknown5; + u8 unknown6:1, + step:3, + unknown7:4; + u8 txpower:1, + widenarr:1, + unknown8:6; +}; + +#seekto 0x0F10; +struct { + struct vfo a; + struct vfo b; +} vfo; + +#seekto 0x1010; +struct { + u8 name[6]; + u8 unknown[10]; +} names[128]; + +""" + +##### MAGICS ######################################################### + +# TDXone TD-Q8A magic string +MSTRING_TDQ8A = "\x02PYNCRAM" + +#STIMEOUT = 2 + +LIST_DTMF = ["QT", "QT+DTMF"] +LIST_VOICE = ["Off", "Chinese", "English"] +LIST_OFF1TO9 = ["Off"] + list("123456789") +LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"] +LIST_RESUME = ["Time Operated(TO)", "Carrier Operated(CO)", "Search(SE)"] +LIST_COLOR = ["Off", "Blue", "Orange", "Purple"] +LIST_MODE = ["Channel", "Frequency", "Name"] +LIST_PF1 = ["Off", "Scan", "Lamp", "FM Radio", "Alarm"] +LIST_OFF1TO30 = ["OFF"] + ["%s" % x for x in range(1, 31)] +LIST_DTMFST = ["Off", "DTMF Sidetone", "ANI Sidetone", "DTMF+ANI Sidetone"] +LIST_PONMSG = ["Full", "Welcome", "Battery Voltage"] +LIST_TIMEOUT = ["Off"] + ["%s sec" % x for x in range(15, 615, 15)] +LIST_PTTID = ["BOT", "EOT", "Both"] +LIST_ROGER = ["Off"] + LIST_PTTID +LIST_PRIORITY = ["Off"] + ["%s" % x for x in range(1, 129)] +LIST_WORKMODE = ["Frequency", "Channel"] +LIST_AB = ["A", "B"] + +LIST_ALMOD = ["Site", "Tone", "Code"] +LIST_BANDWIDTH = ["Wide", "Narrow"] +LIST_DELAYPROCTIME = ["%s ms" % x for x in range(100, 4100, 100)] +LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_OFFAB = ["Off"] + LIST_AB +LIST_RESETTIME = ["%s ms" % x for x in range(100, 16100, 100)] +LIST_SCODE = ["%s" % x for x in range(1, 16)] +LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SHIFTD = ["Off", "+", "-"] +LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)] +#LIST_STEP = [str(x) for x in STEPS] +LIST_TXPOWER = ["High", "Low"] +LIST_DTMF_SPECIAL_DIGITS = [ "*", "#", "A", "B", "C", "D"] +LIST_DTMF_SPECIAL_VALUES = [ 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00] + +CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ?+-*" +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0] +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5), + chirp_common.PowerLevel("Low", watts=1)] +VALID_BANDS = [(136000000, 174000000), + (400000000, 520000000)] + + +#def _clean_buffer(radio): +# radio.pipe.timeout = 0.005 +# junk = radio.pipe.read(256) +# radio.pipe.timeout = STIMEOUT +# if junk: +# LOG.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + msg = "Error reading data from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">BHB", ord(cmd), addr, length) + # add the data if set + if len(data) != 0: + frame += data + # return the data + return frame + + +def _recv(radio, addr, length): + """Get data from the radio """ + # read 4 bytes of header + hdr = _rawrecv(radio, 4) + + # read data + data = _rawrecv(radio, length) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(hdr + data)) + + c, a, l = struct.unpack(">BHB", hdr) + if a != addr or l != length or c != ord("W"): + LOG.error("Invalid answer for block 0x%04x:" % addr) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l)) + raise errors.RadioError("Unknown response from the radio") + + return data + + +def _do_ident(radio, magic): + """Put the radio in PROGRAM mode""" + # set the serial discipline + radio.pipe.baudrate = 9600 + ####radio.pipe.timeout = STIMEOUT + + ## flush input buffer + #_clean_buffer(radio) + + # send request to enter program mode + _rawsend(radio, magic) + + ack = _rawrecv(radio, 1) + if ack != "\x06": + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond") + + _rawsend(radio, "\x02") + + # Ok, get the response + ident = _rawrecv(radio, radio._magic_response_length) + + # check if response is OK + if not ident.startswith("P3107"): + # bad response + msg = "Unexpected response, got this:" + msg += util.hexprint(ident) + LOG.debug(msg) + raise errors.RadioError("Unexpected response from radio.") + + # DEBUG + LOG.info("Valid response, got this:") + LOG.debug(util.hexprint(ident)) + + _rawsend(radio, "\x06") + ack = _rawrecv(radio, 1) + if ack != "\x06": + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio refused clone") + + return ident + + +def _ident_radio(radio): + for magic in radio._magic: + error = None + try: + data = _do_ident(radio, magic) + return data + except errors.RadioError, e: + print e + error = e + time.sleep(2) + if error: + raise error + raise errors.RadioError("Radio did not respond") + + +def _download(radio): + """Get the memory map""" + # put radio in program mode + ident = _ident_radio(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._mem_size / radio._recv_block_size + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, radio._mem_size, radio._recv_block_size): + frame = _make_frame("R", addr, radio._recv_block_size) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # sending the read request + _rawsend(radio, frame) + + # now we read + d = _recv(radio, addr, radio._recv_block_size) + + time.sleep(0.05) + + _rawsend(radio, "\x06") + + ack = _rawrecv(radio, 1) + if ack != "\x06": + raise errors.RadioError( + "Radio refused to send block 0x%04x" % addr) + + ####time.sleep(0.05) + + # aggregate the data + data += d + + # UI Update + status.cur = addr / radio._recv_block_size + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data += radio.MODEL.ljust(8) + + return data + + +def _upload(radio): + """Upload procedure""" + # put radio in program mode + _ident_radio(radio) + + + + addr = 0x0f80 + frame = _make_frame("R", addr, radio._recv_block_size) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # sending the read request + _rawsend(radio, frame) + + # now we read + d = _recv(radio, addr, radio._recv_block_size) + + time.sleep(0.05) + + _rawsend(radio, "\x06") + + ack = _rawrecv(radio, 1) + if ack != "\x06": + raise errors.RadioError( + "Radio refused to send block 0x%04x" % addr) + + + + _ranges = radio._ranges + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._mem_size / radio._send_block_size + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # the fun start here + for start, end in _ranges: + for addr in range(start, end, radio._send_block_size): + # sending the data + data = radio.get_mmap()[addr:addr + radio._send_block_size] + + frame = _make_frame("W", addr, radio._send_block_size, data) + + _rawsend(radio, frame) + #time.sleep(0.05) + + # receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + # UI Update + status.cur = addr / radio._send_block_size + status.msg = "Cloning to radio..." + radio.status_fn(status) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + if len(data) == 0x2008: + rid = data[0x2000:0x2008] + print rid + return rid.startswith(cls.MODEL) + else: + return False + + +def _split(rf, f1, f2): + """Returns False if the two freqs are in the same band (no split) + or True otherwise""" + + # determine if the two freqs are in the same band + for low, high in rf.valid_bands: + if f1 >= low and f1 <= high and \ + f2 >= low and f2 <= high: + # if the two freqs are on the same Band this is not a split + return False + + # if you get here is because the freq pairs are split + return True + + +@directory.register +class TDXoneTDQ8A(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """TDXone TD-Q8A Radio""" + VENDOR = "TDXone" + MODEL = "TD-Q8A" + + ####_fileid = [TDQ8A_fp1, ] + + _magic = [MSTRING_TDQ8A, MSTRING_TDQ8A,] + _magic_response_length = 8 + _fw_ver_start = 0x1EF0 + _recv_block_size = 0x40 + _mem_size = 0x2000 + + #_ranges = [(0x0000, 0x2000)] + # same as radio + #_ranges = [(0x0010, 0x0810), + # (0x0F10, 0x0F30), + # (0x1010, 0x1810), + # (0x0E20, 0x0E60), + # (0x1F10, 0x1F30)] + # in increasing order + _ranges = [(0x0010, 0x0810), + (0x0E20, 0x0E60), + (0x0F10, 0x0F30), + (0x1010, 0x1810), + (0x1F10, 0x1F30)] + _send_block_size = 0x10 + + #DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + #DTCS_CODES = sorted(chirp_common.ALL_DTCS_CODES) + #POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), + # chirp_common.PowerLevel("High", watts=5.00)] + #VALID_BANDS = [(136000000, 174000000), + # (400000000, 520000000)] + + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The TDXone TD-Q8A driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + + def get_features(self): + """Get the radio's features""" + + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = False #True + rf.has_rx_dtcs = False #True + rf.has_dtcs_polarity = False #True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = ["FM", "NFM"] + #rf.valid_characters = self.VALID_CHARS + rf.valid_characters = CHARSET + rf.valid_name_length = 6 + rf.valid_duplexes = ["", "-", "+", "split", "off"] + #rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + #rf.valid_cross_modes = [ + # "Tone->Tone", + # "DTCS->", + # "->DTCS", + # "Tone->DTCS", + # "DTCS->Tone", + # "->Tone", + # "DTCS->DTCS"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "->Tone"] + rf.valid_skips = ["", "S"] + #rf.valid_dtcs_codes = self.DTCS_CODES + rf.memory_bounds = (1, 128) + rf.valid_power_levels = POWER_LEVELS + rf.valid_tuning_steps = STEPS + rf.valid_bands = VALID_BANDS + + return rf + + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + + def sync_out(self): + """Upload to radio""" + try: + _upload(self) + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.txfreq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + + def _get_mem(self, number): + return self._memobj.memory[number - 1] + + def _get_nam(self, number): + return self._memobj.names[number - 1] + + def get_memory(self, number): + _mem = self._get_mem(number) + _nam = self._get_nam(number) + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + if self._is_txinh(_mem): + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + else: + # TX freq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset != 0: + if _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10): + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + elif offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + if _nam.name: + for char in _nam.name: + try: + mem.name += CHARSET[char] + except IndexError: + break + mem.name = mem.name.rstrip() + + #dtcs_pol = ["N", "N"] + + if _mem.txtone in [0, 0xFFFF]: + txmode = "" + elif _mem.txtone >= 0x0258: + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + else: + LOG.warn("Bug: txtone is %04x" % _mem.txtone) + + #elif _mem.txtone <= 0x0258: + # txmode = "DTCS" + # if _mem.txtone > 0x69: + # index = _mem.txtone - 0x6A + # dtcs_pol[0] = "R" + # else: + # index = _mem.txtone - 1 + # mem.dtcs = self.DTCS_CODES[index] + #else: + # LOG.warn("Bug: txtone is %04x" % _mem.txtone) + + if _mem.rxtone in [0, 0xFFFF]: + rxmode = "" + elif _mem.rxtone >= 0x0258: + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + else: + LOG.warn("Bug: rxtone is %04x" % _mem.rxtone) + + #elif _mem.rxtone <= 0x0258: + # rxmode = "DTCS" + # if _mem.rxtone >= 0x6A: + # index = _mem.rxtone - 0x6A + # dtcs_pol[1] = "R" + # else: + # index = _mem.rxtone - 1 + # mem.rx_dtcs = self.DTCS_CODES[index] + #else: + # LOG.warn("Bug: rxtone is %04x" % _mem.rxtone) + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + #elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + # mem.tmode = "DTCS" + #elif rxmode or txmode: + # mem.tmode = "Cross" + # mem.cross_mode = "%s->%s" % (txmode, rxmode) + + #mem.dtcs_polarity = "".join(dtcs_pol) + + if not _mem.scan: + mem.skip = "S" + + mem.power = POWER_LEVELS[1 - _mem.highpower] + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("dtmf", "DTMF", + RadioSettingValueList(LIST_DTMF, + LIST_DTMF[_mem.dtmf])) + mem.extra.append(rs) + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + return mem + + + def _set_mem(self, number): + return self._memobj.memory[number - 1] + + def _set_nam(self, number): + return self._memobj.names[number - 1] + + def set_memory(self, mem): + _mem = self._get_mem(mem.number) + _nam = self._get_nam(mem.number) + + if mem.empty: + _mem.set_raw("\xff" * 12 + "\xbf" +"\xff" * 3) + _nam.set_raw("\xff" * 16) + return + + #_mem.set_raw("\x00" * 16) + _mem.set_raw("\xff" * 12 + "\x9f" +"\xff" * 3) + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + if _nam.name: + for i in range(0, 6): + try: + _nam.name[i] = CHARSET.index(mem.name[i]) + except IndexError: + _nam.name[i] = 0xFF + + rxmode = txmode = "" + if mem.tmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + _mem.rxtone = 0 + elif mem.tmode == "TSQL": + _mem.txtone = int(mem.ctone * 10) + _mem.rxtone = int(mem.ctone * 10) + #elif mem.tmode == "DTCS": + # rxmode = txmode = "DTCS" + # _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + # _mem.rxtone = self.DTCS_CODES.index(mem.dtcs) + 1 + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + #elif txmode == "DTCS": + # _mem.txtone = self.DTCS_CODES.index(mem.dtcs) + 1 + else: + _mem.txtone = 0 + if rxmode == "Tone": + _mem.rxtone = int(mem.ctone * 10) + #elif rxmode == "DTCS": + # _mem.rxtone = self.DTCS_CODES.index(mem.rx_dtcs) + 1 + else: + _mem.rxtone = 0 + else: + _mem.rxtone = 0 + _mem.txtone = 0 + + #if txmode == "DTCS" and mem.dtcs_polarity[0] == "R": + # _mem.txtone += 0x69 + #if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R": + # _mem.rxtone += 0x69 + + _mem.scan = mem.skip != "S" + _mem.wide = mem.mode == "FM" + + _mem.highpower = mem.power == POWER_LEVELS[0] + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + + def get_settings(self): + # """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + #other = RadioSettingGroup("other", "Other Settings") + #work = RadioSettingGroup("work", "Work Mode Settings") + #fm_preset = RadioSettingGroup("fm_preset", "FM Preset") + #dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings") + #dtmfd = RadioSettingGroup("dtmfd", "DTMF Decode Settings") + #service = RadioSettingGroup("service", "Service Settings") + #top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe, + # dtmfd, service) + top = RadioSettings(basic, advanced, ) + + # Basic settings + rs = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(rs) + + if _mem.settings.squelcha > 0x09: + val = 0x00 + else: + val = _mem.settings.squelcha + rs = RadioSetting("squelcha", "Squelch Level A", + RadioSettingValueInteger(0, 9, _mem.settings.squelcha)) + basic.append(rs) + + + if _mem.settings.squelchb > 0x09: + val = 0x00 + else: + val = _mem.settings.squelchb + rs = RadioSetting("squelchb", "Squelch Level B", + RadioSettingValueInteger(0, 9, _mem.settings.squelchb)) + basic.append(rs) + + + if _mem.settings.voice > 0x02: + val = 0x01 + else: + val = _mem.settings.voice + rs = RadioSetting("settings.voice", "Voice Prompt", + RadioSettingValueList( + LIST_VOICE, LIST_VOICE[val])) + basic.append(rs) + + if _mem.settings.vox > 0x0A: + val = 0x00 + else: + val = _mem.settings.vox + rs = RadioSetting("settings.vox", "VOX", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + rs = RadioSetting("settings.autolk", "Automatic Key Lock", + RadioSettingValueBoolean(_mem.settings.autolk)) + basic.append(rs) + + if _mem.settings.screv > 0x02: + val = 0x01 + else: + val = _mem.settings.screv + rs = RadioSetting("settings.screv", "Scan Resume", + RadioSettingValueList( + LIST_RESUME, LIST_RESUME[val])) + basic.append(rs) + + if _mem.settings.toa > 0x0A: + val = 0x00 + else: + val = _mem.settings.toa + rs = RadioSetting("settings.toa", "Time-out Pre-Alert", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + if _mem.settings.timeout > 0x28: + val = 0x03 + else: + val = _mem.settings.timeout + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + LIST_TIMEOUT, LIST_TIMEOUT[val])) + basic.append(rs) + + rs = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.wtled])) + basic.append(rs) + + rs = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.rxled])) + basic.append(rs) + + rs = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.txled])) + basic.append(rs) + + rs = RadioSetting("settings.roger", "Roger Beep", + RadioSettingValueList(LIST_ROGER, LIST_ROGER[ + _mem.settings.roger])) + basic.append(rs) + + rs = RadioSetting("settings.mdfa", "Display Mode (A)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("settings.mdfb", "Display Mode (B)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("settings.pf1", "PF1 Key Assignment", + RadioSettingValueList(LIST_PF1, LIST_PF1[ + _mem.settings.pf1])) + basic.append(rs) + + rs = RadioSetting("settings.tdr", "Dual Watch(TDR)", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(rs) + + rs = RadioSetting("settings.ani", "ANI", + RadioSettingValueBoolean(_mem.settings.ani)) + basic.append(rs) + + if _mem.settings.pttdly > 0x0A: + val = 0x00 + else: + val = _mem.settings.pttdly + rs = RadioSetting("settings.pttdly", "PTT ID Delay", + RadioSettingValueList( + LIST_OFF1TO30, LIST_OFF1TO30[val])) + basic.append(rs) + + rs = RadioSetting("settings.pttid", "When to send PTT ID", + RadioSettingValueList(LIST_PTTID, + LIST_PTTID[_mem.settings.pttid])) + basic.append(rs) + + rs = RadioSetting("settings.dtmfst", "DTMF Sidetone", + RadioSettingValueList(LIST_DTMFST, LIST_DTMFST[ + _mem.settings.dtmfst])) + basic.append(rs) + + rs = RadioSetting("settings.ponmsg", "Power-On Message", + RadioSettingValueList(LIST_PONMSG, LIST_PONMSG[ + _mem.settings.ponmsg])) + basic.append(rs) + + rs = RadioSetting("settings.dw", "DW", + RadioSettingValueBoolean(_mem.settings.dw)) + basic.append(rs) + + # Advanced settings + rs = RadioSetting("settings.prioritych", "Priority Channel", + RadioSettingValueList(LIST_PRIORITY, LIST_PRIORITY[ + _mem.settings.prioritych])) + advanced.append(rs) + + rs = RadioSetting("settings.vfomr", "Work Mode", + RadioSettingValueList(LIST_WORKMODE, LIST_WORKMODE[ + _mem.settings.vfomr])) + advanced.append(rs) + + dtmfchars = "0123456789" + _codeobj = _mem.settings.code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 3, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("settings.code", "PTT-ID Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 3): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, _mem.settings) + advanced.append(rs) + + _codeobj = _mem.settings.password + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 6, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("settings.password", "Control Password", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 6): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.password = code + rs.set_apply_callback(apply_code, _mem.settings) + advanced.append(rs) + + if _mem.settings.tdrab > 0x01: + val = 0x00 + else: + val = _mem.settings.tdrab + rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority", + RadioSettingValueList( + LIST_AB, LIST_AB[val])) + advanced.append(rs) + + rs = RadioSetting("settings.keylk", "Key Lock", + RadioSettingValueBoolean(_mem.settings.keylk)) + advanced.append(rs) + + rs = RadioSetting("settings.control", "Control Code", + RadioSettingValueBoolean(_mem.settings.control)) + advanced.append(rs) + + return top + + + + """ + # Other settings + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = _mem.sixpoweron_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val) + other.append(rs) + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val) + other.append(rs) + + _msg = _mem.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + # DTMF encode settings + + if _mem.ani.dtmfon > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + if _mem.ani.dtmfoff > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + """ + + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + for element in settings: + try: + val = element.value + if self._memobj.fm_presets <= 108.0 * 10 - 650: + value = int(val.get_value() * 10 - 650) + else: + value = int(val.get_value() * 10) + LOG.debug("Setting fm_presets = %s" % (value)) + self._memobj.fm_presets = value + except Exception, e: + LOG.debug(element.get_name()) + raise + + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == 0x2008: + match_size = True + + # testing the model fingerprint + match_model = model_match(cls, filedata) + + #if match_size and match_model: + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/template.py b/chirp/drivers/template.py new file mode 100644 index 0000000..381b103 --- /dev/null +++ b/chirp/drivers/template.py @@ -0,0 +1,130 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, directory, memmap +from chirp import bitwise + +# Here is where we define the memory map for the radio. Since +# We often just know small bits of it, we can use #seekto to skip +# around as needed. +# +# Our fake radio includes just a single array of ten memory objects, +# With some very basic settings, a 32-bit unsigned integer for the +# frequency (in Hertz) and an eight-character alpha tag +# +MEM_FORMAT = """ +#seekto 0x0000; +struct { + u32 freq; + char name[8]; +} memory[10]; +""" + + +def do_download(radio): + """This is your download function""" + # NOTE: Remove this in your real implementation! + return memmap.MemoryMap("\x00" * 1000) + + # Get the serial port connection + serial = radio.pipe + + # Our fake radio is just a simple download of 1000 bytes + # from the serial port. Do that one byte at a time and + # store them in the memory map + data = "" + for _i in range(0, 1000): + data = serial.read(1) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + """This is your upload function""" + # NOTE: Remove this in your real implementation! + raise Exception("This template driver does not really work!") + + # Get the serial port connection + serial = radio.pipe + + # Our fake radio is just a simple upload of 1000 bytes + # to the serial port. Do that one byte at a time, reading + # from our memory map + for i in range(0, 1000): + serial.write(radio.get_mmap()[i]) + + +# Uncomment this to actually register this radio in CHIRP +# @directory.register +class TemplateRadio(chirp_common.CloneModeRadio): + """Acme Template""" + VENDOR = "Acme" # Replace this with your vendor + MODEL = "Template" # Replace this with your model + BAUD_RATE = 9600 # Replace this with your baud rate + + # Return information about this radio's features, including + # how many memories it has, what bands it supports, etc + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.memory_bounds = (0, 9) # This radio supports memories 0-9 + rf.valid_bands = [(144000000, 148000000), # Supports 2-meters + (440000000, 450000000), # Supports 70-centimeters + ] + return rf + + # Do a download of the radio from the serial port + def sync_in(self): + self._mmap = do_download(self) + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + # Do an upload of the radio to the serial port + def sync_out(self): + do_upload(self) + + # Return a raw representation of the memory object, which + # is very helpful for development + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + # Extract a high-level memory object from the low-level memory map + # This is called to populate a memory in the UI + def get_memory(self, number): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[number] + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + mem.number = number # Set the memory number + # Convert your low-level frequency to Hertz + mem.freq = int(_mem.freq) + mem.name = str(_mem.name).rstrip() # Set the alpha tag + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + + return mem + + # Store details about a high-level memory to the memory map + # This is called when a user edits a memory in the UI + def set_memory(self, mem): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number] + + # Convert to low-level frequency representation + _mem.freq = mem.freq + _mem.name = mem.name.ljust(8)[:8] # Store the alpha tag diff --git a/chirp/drivers/th350.py b/chirp/drivers/th350.py new file mode 100644 index 0000000..39475db --- /dev/null +++ b/chirp/drivers/th350.py @@ -0,0 +1,398 @@ +# Copyright 2019 Zhaofeng Li +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division + +import struct +import logging +from math import floor +from chirp import chirp_common, directory, bitwise, memmap, errors, util +from .uvb5 import BaofengUVB5 + +LOG = logging.getLogger(__name__) + +mem_format = """ +struct memory { + lbcd freq[4]; + lbcd offset[4]; + u8 unknown1:2, + txpol:1, + rxpol:1, + compander:1, + scrambler:1, + unknown2:2; + u8 rxtoneb; + u8 rxtonea; + u8 txtoneb; + u8 txtonea; + u8 pttid:1, + scanadd:1, + isnarrow:1, + bcl:1, + highpower:1, + revfreq:1, + duplex:2; + u8 unknown[2]; +}; + +#seekto 0x0000; +char ident[32]; +u8 blank[16]; +struct memory vfo1; +struct memory channels[128]; +#seekto 0x0840; +struct memory vfo3; +struct memory vfo2; + +#seekto 0x09D0; +u16 fm_presets[16]; + +#seekto 0x0A30; +struct { + u8 name[5]; +} names[128]; + +#seekto 0x0D30; +struct { + u8 squelch; + u8 freqmode_ab:1, + save_funct:1, + backlight:1, + beep_tone_disabled:1, + roger:1, + tdr:1, + scantype:2; + u8 language:1, + workmode_b:1, + workmode_a:1, + workmode_fm:1, + voice_prompt:1, + fm:1, + pttid:2; + u8 unknown_0:5, + timeout:3; + u8 mdf_b:2, + mdf_a:2, + unknown_1:2, + txtdr:2; + u8 unknown_2:4, + ste_disabled:1, + unknown_3:2, + sidetone:1; + u8 vox; + u8 unk1; + u8 mem_chan_a; + u16 fm_vfo; + u8 unk4; + u8 unk5; + u8 mem_chan_b; + u8 unk6; + u8 last_menu; // number of last menu item accessed +} settings; + +#seekto 0x0D50; +struct { + u8 code[6]; +} pttid; + +#seekto 0x0F30; +struct { + lbcd lower_vhf[2]; + lbcd upper_vhf[2]; + lbcd lower_uhf[2]; + lbcd upper_uhf[2]; +} limits; + +#seekto 0x0FF0; +struct { + u8 vhfsquelch0; + u8 vhfsquelch1; + u8 vhfsquelch2; + u8 vhfsquelch3; + u8 vhfsquelch4; + u8 vhfsquelch5; + u8 vhfsquelch6; + u8 vhfsquelch7; + u8 vhfsquelch8; + u8 vhfsquelch9; + u8 unknown1[6]; + u8 uhfsquelch0; + u8 uhfsquelch1; + u8 uhfsquelch2; + u8 uhfsquelch3; + u8 uhfsquelch4; + u8 uhfsquelch5; + u8 uhfsquelch6; + u8 uhfsquelch7; + u8 uhfsquelch8; + u8 uhfsquelch9; + u8 unknown2[6]; + u8 vhfhipwr0; + u8 vhfhipwr1; + u8 vhfhipwr2; + u8 vhfhipwr3; + u8 vhfhipwr4; + u8 vhfhipwr5; + u8 vhfhipwr6; + u8 vhfhipwr7; + u8 vhflopwr0; + u8 vhflopwr1; + u8 vhflopwr2; + u8 vhflopwr3; + u8 vhflopwr4; + u8 vhflopwr5; + u8 vhflopwr6; + u8 vhflopwr7; + u8 uhfhipwr0; + u8 uhfhipwr1; + u8 uhfhipwr2; + u8 uhfhipwr3; + u8 uhfhipwr4; + u8 uhfhipwr5; + u8 uhfhipwr6; + u8 uhfhipwr7; + u8 uhflopwr0; + u8 uhflopwr1; + u8 uhflopwr2; + u8 uhflopwr3; + u8 uhflopwr4; + u8 uhflopwr5; + u8 uhflopwr6; + u8 uhflopwr7; +} test; +""" + + +def do_ident(radio): + radio.pipe.timeout = 3 + radio.pipe.write(b"\x05TROGRAM") + for x in range(10): + ack = radio.pipe.read(1) + if ack == b'\x06': + break + else: + raise errors.RadioError("Radio did not ack programming mode") + radio.pipe.write(b"\x02") + ident = radio.pipe.read(8) + LOG.debug(util.hexprint(ident)) + if not ident.startswith(b'HKT511'): + raise errors.RadioError("Unsupported model") + radio.pipe.write(b"\x06") + ack = radio.pipe.read(1) + if ack != b"\x06": + raise errors.RadioError("Radio did not ack ident") + + +def do_status(radio, direction, addr): + status = chirp_common.Status() + status.msg = "Cloning %s radio" % direction + status.cur = addr + status.max = 0x1000 + radio.status_fn(status) + + +def do_download(radio): + do_ident(radio) + data = b"TRI350 Radio Program data v1.08\x00" + data += (b"\x00" * 16) + firstack = None + for i in range(0, 0x1000, 16): + frame = struct.pack(">cHB", b"R", i, 16) + radio.pipe.write(frame) + result = radio.pipe.read(20) + if frame[1:4] != result[1:4]: + LOG.debug(util.hexprint(result)) + raise errors.RadioError("Invalid response for address 0x%04x" % i) + data += result[4:] + do_status(radio, "from", i) + + return memmap.MemoryMapBytes(data) + + +def do_upload(radio): + do_ident(radio) + data = radio._mmap.get_byte_compatible()[0x0030:] + + for i in range(0, 0x1000, 16): + frame = struct.pack(">cHB", b"W", i, 16) + frame += data[i:i + 16] + radio.pipe.write(frame) + ack = radio.pipe.read(1) + if ack != b"\x06": + # UV-B5/UV-B6 radios with 27 menus do not support service settings + # and will stop ACKing when the upload reaches 0x0F10 + if i == 0x0F10: + # must be a radio with 27 menus detected - stop upload + break + else: + LOG.debug("Radio NAK'd block at address 0x%04x" % i) + raise errors.RadioError( + "Radio NAK'd block at address 0x%04x" % i) + LOG.debug("Radio ACK'd block at address 0x%04x" % i) + do_status(radio, "to", i) + + +DUPLEX = ["", "-", "+"] +CHARSET = "0123456789- ABCDEFGHIJKLMNOPQRSTUVWXYZ/_+*" +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), + chirp_common.PowerLevel("High", watts=5)] + + +@directory.register +class Th350Radio(BaofengUVB5): + """TYT TH-350""" + VENDOR = "TYT" + MODEL = "TH-350" + BAUD_RATE = 9600 + NEEDS_COMPAT_SERIAL = False + SPECIALS = { + "VFO1": -3, + "VFO2": -2, + "VFO3": -1, + } + + _memsize = 0x1000 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ("This TYT TH-350 driver is an alpha version. " + "Proceed with Caution and backup your data. " + "Always confirm the correctness of your settings with the " + "official programmer.") + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_duplexes = DUPLEX + ["split"] + rf.can_odd_split = True + rf.valid_skips = ["", "S"] + rf.valid_characters = CHARSET + rf.valid_name_length = 5 + rf.valid_bands = [(130000000, 175000000), + (220000000, 269000000), + (400000000, 520000000)] + rf.valid_modes = ["FM", "NFM"] + rf.valid_special_chans = list(self.SPECIALS.keys()) + rf.valid_power_levels = POWER_LEVELS + rf.has_ctone = True + rf.has_bank = False + rf.has_tuning_step = False + rf.memory_bounds = (1, 128) + return rf + + def sync_in(self): + try: + self._mmap = do_download(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + do_upload(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def _decode_tone(self, _mem, which): + def _get(field): + return int(getattr(_mem, "%s%s" % (which, field))) + + tonea, toneb = _get('tonea'), _get('toneb') + + if tonea == 0xff: + mode = val = pol = None + elif tonea >= 0x80: + # DTCS + # D754N -> 0x87 0x54 + # D754I -> 0xc7 0x54 + # Yes. Decimal digits as hex. You're seeing that right. + # No idea why TYT engineers would do something like that. + pold = tonea // 16 + if pold not in [0x8, 0xc]: + LOG.warn("Bug: tone is %04x %04x" % (tonea, toneb)) + mode = val = pol = None + else: + mode = 'DTCS' + val = ((tonea % 16) * 100 + + toneb // 16 * 10 + + (toneb % 16)) + pol = 'N' if pold == 8 else 'R' + else: + # Tone + # 107.2 -> 0x10 0x72. Seriously. + mode = 'Tone' + val = (tonea // 16 * 100 + + (tonea % 16) * 10 + + toneb // 16 + + toneb % 16 / 10) + pol = None + + return mode, val, pol + + def _encode_tone(self, _mem, which, mode, val, pol): + def _set(field, value): + setattr(_mem, "%s%s" % (which, field), value) + + if mode == "Tone": + tonea = int( + val // 100 * 16 + + val // 10 % 10 + ) + toneb = int( + floor(val % 10) * 16 + + floor(val * 10) % 10 + ) + elif mode == "DTCS": + tonea = (0x80 if pol == 'N' else 0xc0) + \ + val // 100 + toneb = (val // 10) % 10 * 16 + \ + val % 10 + else: + tonea = toneb = 0xff + + _set('tonea', tonea) + _set('toneb', toneb) + + def _get_memobjs(self, number): + if isinstance(number, str): + return (getattr(self._memobj, number.lower()), None) + elif number < 0: + for k, v in list(self.SPECIALS.items()): + if number == v: + return (getattr(self._memobj, k.lower()), None) + else: + return (self._memobj.channels[number - 1], + self._memobj.names[number - 1].name) + + @classmethod + def match_model(cls, filedata, filename): + return (filedata.startswith(b"TRI350 Radio Program data") and + len(filedata) == (cls._memsize + 0x30)) diff --git a/chirp/drivers/th7800.py b/chirp/drivers/th7800.py new file mode 100644 index 0000000..231030f --- /dev/null +++ b/chirp/drivers/th7800.py @@ -0,0 +1,739 @@ +# Copyright 2014 Tom Hayward +# Copyright 2014 Jens Jensen +# Copyright 2014 James Lee N1DDK +# Copyright 2016 Nathan Crapo (TH-7800 only) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import bitwise, chirp_common, directory, errors, util, memmap +import struct +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings, \ + RadioSettingValueMap, zero_indexed_seq_map +from chirp.chirp_common import format_freq +import os +import time +import logging +from datetime import date + +LOG = logging.getLogger(__name__) + +TH7800_MEM_FORMAT = """ +struct mem { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + lbcd ctcss[2]; + lbcd dtcs[2]; + u8 power:2, + clk_sft:1, + unknown0a:2, + display:1, // freq=0, name=1 + scan:2; + u8 fmdev:2, // wide=00, mid=01, narrow=10 + scramb:1, + compand:1, + emphasis:1 + unknown1a:2, + sqlmode:1; // carrier, tone + u8 rptmod:2, // off, -, + + reverse:1, + talkaround:1, + step:4; + u8 dtcs_pol:2, + unknown3:4, + tmode:2; + lbcd offset[4]; + u8 hsdtype:2, // off, 2-tone, 5-tone, dtmf + unknown5a:1, + am:1, + unknown5b:4; + u8 unknown6[3]; + char name[6]; + u8 empty[2]; +}; + +#seekto 0x%04X; +struct mem memory[800]; + +#seekto 0x%04X; +struct { + struct mem lower; + struct mem upper; +} scanlimits[5]; + +#seekto 0x%04X; +struct { + u8 unk0xdc20:5, + left_sql:3; + u8 apo; + u8 unk0xdc22:5, + backlight:3; + u8 unk0xdc23; + u8 beep:1, + keylock:1, + pttlock:2, + unk0xdc24_32:2, + hyper_chan:1, + right_func_key:1; + u8 tbst_freq:2, + unk0xdc25_4:2, + mute_mode:2, + unk0xdc25_10:2; + u8 ars:1, + unk0xdc26_54:3, + auto_am:1, + unk0xdc26_210:3; + u8 unk0xdc27_76543:5, + scan_mode:1, + unk0xdc27_1:1, + scan_resume:1; + u16 scramb_freq; + u16 scramb_freq1; + u8 unk0xdc2c; + u8 unk0xdc2d; + u8 unk0xdc2e:5, + right_sql:3; + u8 unk0xdc2f:8; + u8 tot; + u8 unk0xdc30; + u8 unk0xdc31; + u8 unk0xdc32; + u8 unk0xdc34; + u8 unk0xdc35; + u8 unk0xdc36; + u8 unk0xdc37; + u8 p1; + u8 p2; + u8 p3; + u8 p4; +} settings; + +#seekto 0x%04X; +u8 chan_active[128]; +u8 scan_enable[128]; +u8 priority[128]; + +#seekto 0x%04X; +struct { + char sn[8]; + char model[8]; + char code[16]; + u8 empty[8]; + lbcd prog_yr[2]; + lbcd prog_mon; + lbcd prog_day; + u8 empty_10f2c[4]; +} info; + +struct { + lbcd lorx[4]; + lbcd hirx[4]; + lbcd lotx[4]; + lbcd hitx[4]; +} bandlimits[9]; + +""" + + +BLANK_MEMORY = "\xFF" * 8 + "\x00\x10\x23\x00\xC0\x08\x06\x00" \ + "\x00\x00\x76\x00\x00\x00" + "\xFF" * 10 +DTCS_POLARITY = ["NN", "RN", "NR", "RR"] +SCAN_MODES = ["", "S", "P"] +MODES = ["WFM", "FM", "NFM"] +TMODES = ["", "Tone", "TSQL", "DTCS"] +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("Mid2", watts=10.00), + chirp_common.PowerLevel("Mid1", watts=20.00), + chirp_common.PowerLevel("High", watts=50.00)] +BUSY_LOCK = ["off", "Carrier", "2 tone"] +MICKEYFUNC = ["None", "SCAN", "SQL.OFF", "TCALL", "PPTR", "PRI", "LOW", "TONE", + "MHz", "REV", "HOME", "BAND", "VFO/MR"] +SQLPRESET = ["Off", "2", "5", "9", "Full"] +BANDS = ["30MHz", "50MHz", "60MHz", "108MHz", "150MHz", "250MHz", "350MHz", + "450MHz", "850MHz"] +STEPS = [2.5, 5.0, 6.25, 7.5, 8.33, 10.0, 12.5, + 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + + +def add_radio_setting(radio_setting_group, mem_field, ui_name, option_map, + current, doc=None): + setting = RadioSetting(mem_field, ui_name, + RadioSettingValueMap(option_map, current)) + if doc is not None: + setting.set_doc(doc) + radio_setting_group.append(setting) + + +def add_radio_bool(radio_setting_group, mem_field, ui_name, current, doc=None): + setting = RadioSetting(mem_field, ui_name, + RadioSettingValueBoolean(bool(current))) + radio_setting_group.append(setting) + + +class TYTTH7800Base(chirp_common.Radio): + """Base class for TYT TH-7800""" + VENDOR = "TYT" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 800) + rf.has_bank = False + rf.has_tuning_step = True + rf.valid_tuning_steps = STEPS + rf.can_odd_split = True + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = TMODES + rf.has_ctone = False + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "#*-+" + rf.valid_bands = [(108000000, 180000000), + (350000000, 399995000), + (400000000, 512000000)] + rf.valid_skips = SCAN_MODES + rf.valid_modes = MODES + ["AM"] + rf.valid_name_length = 6 + rf.has_settings = True + return rf + + def process_mmap(self): + self._memobj = bitwise.parse( + TH7800_MEM_FORMAT % + (self._mmap_offset, self._scanlimits_offset, self._settings_offset, + self._chan_active_offset, self._info_offset), self._mmap) + + def get_active(self, banktype, num): + """get active flag for channel active, + scan enable, or priority banks""" + bank = getattr(self._memobj, banktype) + index = (num - 1) / 8 + bitpos = (num - 1) % 8 + mask = 2**bitpos + enabled = bank[index] & mask + if enabled: + return True + else: + return False + + def set_active(self, banktype, num, enable=True): + """set active flag for channel active, + scan enable, or priority banks""" + bank = getattr(self._memobj, banktype) + index = (num - 1) / 8 + bitpos = (num - 1) % 8 + mask = 2**bitpos + if enable: + bank[index] |= mask + else: + bank[index] &= ~mask + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + + mem.empty = not self.get_active("chan_active", number) + if mem.empty: + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + txfreq = int(_mem.tx_freq) * 10 + if txfreq == mem.freq: + mem.duplex = "" + elif txfreq == 0: + mem.duplex = "off" + mem.offset = 0 + elif abs(txfreq - mem.freq) > 70000000: + mem.duplex = "split" + mem.offset = txfreq + elif txfreq < mem.freq: + mem.duplex = "-" + mem.offset = mem.freq - txfreq + elif txfreq > mem.freq: + mem.duplex = "+" + mem.offset = txfreq - mem.freq + + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol] + + mem.tmode = TMODES[int(_mem.tmode)] + mem.ctone = mem.rtone = int(_mem.ctcss) / 10.0 + mem.dtcs = int(_mem.dtcs) + + mem.name = str(_mem.name) + mem.name = mem.name.replace("\xFF", " ").rstrip() + + if not self.get_active("scan_enable", number): + mem.skip = "S" + elif self.get_active("priority", number): + mem.skip = "P" + else: + mem.skip = "" + + mem.mode = _mem.am and "AM" or MODES[int(_mem.fmdev)] + + mem.power = POWER_LEVELS[_mem.power] + mem.tuning_step = STEPS[_mem.step] + + mem.extra = RadioSettingGroup("extra", "Extra") + + add_radio_setting(mem.extra, "display", "Display", + zero_indexed_seq_map(["Frequency", "Name"]), + _mem.display) + add_radio_setting(mem.extra, "hsdtype", "HSD TYPE", + zero_indexed_seq_map(["OFF", "2TON", "5TON", + "DTMF"]), + _mem.hsdtype) + add_radio_bool(mem.extra, "clk_sft", "CLK-SFT", _mem.clk_sft) + add_radio_bool(mem.extra, "compand", "Compand", _mem.compand, + doc="Compress Audio") + add_radio_bool(mem.extra, "talkaround", "Talk Around", _mem.talkaround, + doc="Simplex mode when out of range of repeater") + + add_radio_bool(mem.extra, "scramb", "Scramble", _mem.scramb, + doc="Frequency inversion Scramble") + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + _prev_active = self.get_active("chan_active", mem.number) + self.set_active("chan_active", mem.number, not mem.empty) + if mem.empty or not _prev_active: + LOG.debug("initializing memory channel %d" % mem.number) + _mem.set_raw(BLANK_MEMORY) + + if mem.empty: + return + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "split": + _mem.tx_freq = mem.offset / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "off": + _mem.tx_freq = 0 + _mem.offset = 0 + else: + _mem.tx_freq = mem.freq / 10 + + _mem.tmode = TMODES.index(mem.tmode) + if mem.tmode == "TSQL" or mem.tmode == "DTCS": + _mem.sqlmode = 1 + else: + _mem.sqlmode = 0 + _mem.ctcss = mem.rtone * 10 + _mem.dtcs = mem.dtcs + _mem.dtcs_pol = DTCS_POLARITY.index(mem.dtcs_polarity) + + _mem.name = mem.name.ljust(6, "\xFF") + + # autoset display to name if filled, else show frequency + if mem.extra: + # mem.extra only seems to be populated when called from edit panel + display = mem.extra["display"] + else: + display = None + if mem.name: + _mem.display = True + if display and not display.changed(): + display.value = "Name" + else: + _mem.display = False + if display and not display.changed(): + display.value = "Frequency" + + _mem.scan = SCAN_MODES.index(mem.skip) + if mem.skip == "P": + self.set_active("priority", mem.number, True) + self.set_active("scan_enable", mem.number, True) + elif mem.skip == "S": + self.set_active("priority", mem.number, False) + self.set_active("scan_enable", mem.number, False) + elif mem.skip == "": + self.set_active("priority", mem.number, False) + self.set_active("scan_enable", mem.number, True) + + if mem.mode == "AM": + _mem.am = True + _mem.fmdev = 0 + else: + _mem.am = False + _mem.fmdev = MODES.index(mem.mode) + + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 # low + _mem.step = STEPS.index(mem.tuning_step) + + for setting in mem.extra: + LOG.debug("@set_mem:", setting.get_name(), setting.value) + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + _info = self._memobj.info + _bandlimits = self._memobj.bandlimits + basic = RadioSettingGroup("basic", "Basic") + info = RadioSettingGroup("info", "Model Info") + top = RadioSettings(basic, info) + add_radio_bool(basic, "beep", "Beep", _settings.beep) + add_radio_bool(basic, "ars", "Auto Repeater Shift", _settings.ars) + add_radio_setting(basic, "keylock", "Key Lock", + zero_indexed_seq_map(["Manual", "Auto"]), + _settings.keylock) + add_radio_bool(basic, "auto_am", "Auto AM", _settings.auto_am) + add_radio_setting(basic, "left_sql", "Left Squelch", + zero_indexed_seq_map(SQLPRESET), + _settings.left_sql) + add_radio_setting(basic, "right_sql", "Right Squelch", + zero_indexed_seq_map(SQLPRESET), + _settings.right_sql) + add_radio_setting(basic, "apo", "Auto Power off (Hours)", + [("Off", 0), ("0.5", 5), ("1.0", 10), ("1.5", 15), + ("2.0", 20)], + _settings.apo) + add_radio_setting(basic, "backlight", "Display Backlight", + zero_indexed_seq_map(["Off", "1", "2", "3", "Full"]), + _settings.backlight) + add_radio_setting(basic, "pttlock", "PTT Lock", + zero_indexed_seq_map(["Off", "Right", "Left", + "Both"]), + _settings.pttlock) + add_radio_setting(basic, "hyper_chan", "Hyper Channel", + zero_indexed_seq_map(["Manual", "Auto"]), + _settings.hyper_chan) + add_radio_setting(basic, "right_func_key", "Right Function Key", + zero_indexed_seq_map(["Key 1", "Key 2"]), + _settings.right_func_key) + add_radio_setting(basic, "mute_mode", "Mute Mode", + zero_indexed_seq_map(["Off", "TX", "RX", "TX RX"]), + _settings.mute_mode) + add_radio_setting(basic, "scan_mode", "Scan Mode", + zero_indexed_seq_map(["MEM", "MSM"]), + _settings.scan_mode, + doc="MEM = Normal scan, bypass channels marked " + "skip. MSM = Scan only channels marked priority.") + add_radio_setting(basic, "scan_resume", "Scan Resume", + zero_indexed_seq_map(["Time", "Busy"]), + _settings.scan_resume) + basic.append(RadioSetting( + "tot", "Time Out Timer (minutes)", + RadioSettingValueInteger(0, 30, _settings.tot))) + add_radio_setting(basic, "p1", "P1 Function", + zero_indexed_seq_map(MICKEYFUNC), + _settings.p1) + add_radio_setting(basic, "p2", "P2 Function", + zero_indexed_seq_map(MICKEYFUNC), + _settings.p2) + add_radio_setting(basic, "p3", "P3 Function", + zero_indexed_seq_map(MICKEYFUNC), + _settings.p3) + add_radio_setting(basic, "p4", "P4 Function", + zero_indexed_seq_map(MICKEYFUNC), + _settings.p4) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + rsvs = RadioSettingValueString(0, 8, _filter(_info.sn)) + rsvs.set_mutable(False) + rs = RadioSetting("sn", "Serial Number", rsvs) + info.append(rs) + + rsvs = RadioSettingValueString(0, 8, _filter(_info.model)) + rsvs.set_mutable(False) + rs = RadioSetting("model", "Model Name", rsvs) + info.append(rs) + + rsvs = RadioSettingValueString(0, 16, _filter(_info.code)) + rsvs.set_mutable(False) + rs = RadioSetting("code", "Model Code", rsvs) + info.append(rs) + + progdate = "%d/%d/%d" % (_info.prog_mon, _info.prog_day, + _info.prog_yr) + rsvs = RadioSettingValueString(0, 10, progdate) + rsvs.set_mutable(False) + rs = RadioSetting("progdate", "Last Program Date", rsvs) + info.append(rs) + + # Band Limits + for i in range(0, len(BANDS)): + rx_start = int(_bandlimits[i].lorx) * 10 + if not rx_start == 0: + objname = BANDS[i] + "lorx" + objnamepp = BANDS[i] + " Rx Start" + rsv = RadioSettingValueString(0, 10, format_freq(rx_start)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + + rx_end = int(_bandlimits[i].hirx) * 10 + objname = BANDS[i] + "hirx" + objnamepp = BANDS[i] + " Rx end" + rsv = RadioSettingValueString(0, 10, format_freq(rx_end)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + + tx_start = int(_bandlimits[i].lotx) * 10 + if not tx_start == 0: + objname = BANDS[i] + "lotx" + objnamepp = BANDS[i] + " Tx Start" + rsv = RadioSettingValueString(0, 10, format_freq(tx_start)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + + tx_end = int(_bandlimits[i].hitx) * 10 + objname = BANDS[i] + "hitx" + objnamepp = BANDS[i] + " Tx end" + rsv = RadioSettingValueString(0, 10, format_freq(tx_end)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + _info = self._memobj.info + _bandlimits = self._memobj.bandlimits + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + oldval = getattr(_settings, setting) + newval = element.value + + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception, e: + LOG.debug(element.get_name()) + raise + + +@directory.register +class TYTTH7800File(TYTTH7800Base, chirp_common.FileBackedRadio): + """TYT TH-7800 .dat file""" + MODEL = "TH-7800 File" + + FILE_EXTENSION = "dat" + + _memsize = 69632 + _mmap_offset = 0x1100 + _scanlimits_offset = 0xC800 + _mmap_offset + _settings_offset = 0xCB20 + _mmap_offset + _chan_active_offset = 0xCB80 + _mmap_offset + _info_offset = 0xfe00 + _mmap_offset + + def __init__(self, pipe): + self.errors = [] + self._mmap = None + + if isinstance(pipe, str): + self.pipe = None + self.load_mmap(pipe) + else: + chirp_common.FileBackedRadio.__init__(self, pipe) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and filename.endswith('.dat') + + +def _identify(radio): + """Do identify handshake with TYT""" + try: + radio.pipe.write("\x02SPECPR") + ack = radio.pipe.read(1) + if ack != "A": + util.hexprint(ack) + raise errors.RadioError("Radio did not ACK first command: %x" + % ord(ack)) + except: + LOG.debug(util.hexprint(ack)) + raise errors.RadioError("Unable to communicate with the radio") + + radio.pipe.write("G\x02") + ident = radio.pipe.read(16) + radio.pipe.write("A") + r = radio.pipe.read(2) + if r != "A": + raise errors.RadioError("Ack failed") + return ident + + +def _download(radio, memsize=0x10000, blocksize=0x80): + """Download from TYT TH-7800""" + data = _identify(radio) + LOG.info("ident:", util.hexprint(data)) + offset = 0x100 + for addr in range(offset, memsize, blocksize): + msg = struct.pack(">cHB", "R", addr, blocksize) + radio.pipe.write(msg) + block = radio.pipe.read(blocksize + 4) + if len(block) != (blocksize + 4): + LOG.debug(util.hexprint(block)) + raise errors.RadioError("Radio sent a short block") + radio.pipe.write("A") + ack = radio.pipe.read(1) + if ack != "A": + LOG.debug(util.hexprint(ack)) + raise errors.RadioError("Radio NAKed block") + data += block[4:] + + if radio.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = memsize + status.msg = "Cloning from radio" + radio.status_fn(status) + + radio.pipe.write("ENDR") + + return memmap.MemoryMap(data) + + +def _upload(radio, memsize=0xF400, blocksize=0x80): + """Upload to TYT TH-7800""" + data = _identify(radio) + + radio.pipe.timeout = 1 + + if data != radio._mmap[:radio._mmap_offset]: + raise errors.RadioError( + "Model mis-match: \n%s\n%s" % + (util.hexprint(data), + util.hexprint(radio._mmap[:radio._mmap_offset]))) + # in the factory software they update the last program date when + # they upload, So let's do the same + today = date.today() + y = today.year + m = today.month + d = today.day + _info = radio._memobj.info + + ly = _info.prog_yr + lm = _info.prog_mon + ld = _info.prog_day + LOG.debug("Updating last program date:%d/%d/%d" % (lm, ld, ly)) + LOG.debug(" to today:%d/%d/%d" % (m, d, y)) + + _info.prog_yr = y + _info.prog_mon = m + _info.prog_day = d + + offset = 0x0100 + for addr in range(offset, memsize, blocksize): + mapaddr = addr + radio._mmap_offset - offset + LOG.debug("addr: 0x%04X, mmapaddr: 0x%04X" % (addr, mapaddr)) + msg = struct.pack(">cHB", "W", addr, blocksize) + msg += radio._mmap[mapaddr:(mapaddr + blocksize)] + LOG.debug(util.hexprint(msg)) + radio.pipe.write(msg) + ack = radio.pipe.read(1) + if ack != "A": + LOG.debug(util.hexprint(ack)) + raise errors.RadioError("Radio did not ack block 0x%04X" % addr) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = memsize + status.msg = "Cloning to radio" + radio.status_fn(status) + + # End of clone + radio.pipe.write("ENDW") + + # Checksum? + final_data = radio.pipe.read(3) + LOG.debug("final:", util.hexprint(final_data)) + + +@directory.register +class TYTTH7800Radio(TYTTH7800Base, chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + VENDOR = "TYT" + MODEL = "TH-7800" + BAUD_RATE = 38400 + + _memsize = 65296 + _mmap_offset = 0x0010 + _scanlimits_offset = 0xC800 + _mmap_offset + _settings_offset = 0xCB20 + _mmap_offset + _chan_active_offset = 0xCB80 + _mmap_offset + _info_offset = 0xfe00 + _mmap_offset + + @classmethod + def match_model(cls, filedata, filename): + if len(filedata) != cls._memsize: + return False + # TYT used TH9800 as model for TH-7800 _AND_ TH-9800. Check + # for TH7800 in case they fix it or if users update the model + # in their own radio. + if not (filedata[0xfe18:0xfe1e] == "TH9800" or + filedata[0xfe18:0xfe1e] == "TH7800"): + return False + # TH-7800 bandlimits differ from TH-9800. First band Invalid + # (zero). + first_bandlimit = struct.unpack("BBBBBBBBBBBBBBBB", + filedata[0xfe40:0xfe50]) + if not all(v == 0 for v in first_bandlimit): + return False + return True + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ( + 'This is experimental support for TH-7800 ' + 'which is still under development.\n' + 'Please ensure you have a good backup with OEM software.\n' + 'Also please send in bug and enhancement requests!\n' + 'You have been warned. Proceed at your own risk!') + return rp + + def sync_in(self): + try: + self._mmap = _download(self) + except Exception, e: + raise errors.RadioError( + "Failed to communicate with the radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + _upload(self) + except Exception, e: + raise errors.RadioError( + "Failed to communicate with the radio: %s" % e) diff --git a/chirp/drivers/th9000.py b/chirp/drivers/th9000.py new file mode 100644 index 0000000..ad2d7eb --- /dev/null +++ b/chirp/drivers/th9000.py @@ -0,0 +1,852 @@ +# Copyright 2015 David Fannin KK6DF +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import struct +import time +import logging + +from chirp import bitwise +from chirp import chirp_common +from chirp import directory +from chirp import errors +from chirp import memmap +from chirp import util +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \ + RadioSettingValueList, RadioSettingValueString, RadioSettingValueBoolean, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError + +LOG = logging.getLogger(__name__) + +# +# Chirp Driver for TYT TH-9000D (models: 2M (144 Mhz), 1.25M (220 Mhz) and 70cm (440 Mhz) radios) +# +# Version 1.0 +# +# - Skip channels +# +# Global Parameters +# +MMAPSIZE = 16384 +TONES = [62.5] + list(chirp_common.TONES) +TMODES = ['','Tone','DTCS',''] +DUPLEXES = ['','err','-','+'] # index 2 not used +MODES = ['WFM','FM','NFM'] # 25k, 20k,15k bw +TUNING_STEPS=[ 5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0 ] # index 0-9 +POWER_LEVELS=[chirp_common.PowerLevel("High", watts=65), + chirp_common.PowerLevel("Mid", watts=25), + chirp_common.PowerLevel("Low", watts=10)] + +CROSS_MODES = chirp_common.CROSS_MODES + +APO_LIST = [ "Off","30 min","1 hr","2 hrs" ] +BGCOLOR_LIST = ["Blue","Orange","Purple"] +BGBRIGHT_LIST = ["%s" % x for x in range(1,32)] +SQUELCH_LIST = ["Off"] + ["Level %s" % x for x in range(1,20)] +TIMEOUT_LIST = ["Off"] + ["%s min" % x for x in range(1,30)] +TXPWR_LIST = ["60W","25W"] # maximum power for Hi setting +TBSTFREQ_LIST = ["1750Hz","2100Hz","1000Hz","1450Hz"] +BEEP_LIST = ["Off","On"] + +SETTING_LISTS = { + "auto_power_off": APO_LIST, + "bg_color" : BGCOLOR_LIST, + "bg_brightness" : BGBRIGHT_LIST, + "squelch" : SQUELCH_LIST, + "timeout_timer" : TIMEOUT_LIST, + "choose_tx_power": TXPWR_LIST, + "tbst_freq" : TBSTFREQ_LIST, + "voice_prompt" : BEEP_LIST +} + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + u8 unknown0000[16]; + char idhdr[16]; + u8 unknown0001[16]; +} fidhdr; +""" +#Overall Memory Map: +# +# Memory Map (Range 0x0100-3FF0, step 0x10): +# +# Field Start End Size +# (hex) (hex) (hex) +# +# 1 Channel Set Flag 0100 011F 20 +# 2 Channel Skip Flag 0120 013F 20 +# 3 Blank/Unknown 0140 01EF B0 +# 4 Unknown 01F0 01FF 10 +# 5 TX/RX Range 0200 020F 10 +# 6 Bootup Passwd 0210 021F 10 +# 7 Options, Radio 0220 023F 20 +# 8 Unknown 0240 019F +# 8B Startup Label 03E0 03E7 07 +# 9 Channel Bank 2000 38FF 1900 +# Channel 000 2000 201F 20 +# Channel 001 2020 202F 20 +# ... +# Channel 199 38E0 38FF 20 +# 10 Blank/Unknown 3900 3FFF 6FF 14592 16383 1792 +# Total Map Size 16128 (2^8 = 16384) +# +# TH9000/220 memory map +# section: 1 and 2: Channel Set/Skip Flags +# +# Channel Set (starts 0x100) : Channel Set bit is value 0 if a memory location in the channel bank is active. +# Channel Skip (starts 0x120): Channel Skip bit is value 0 if a memory location in the channel bank is active. +# +# Both flag maps are a total 24 bytes in length, aligned on 32 byte records. +# bit = 0 channel set/no skip, 1 is channel not set/skip +# +# to index a channel: +# cbyte = channel / 8 ; +# cbit = channel % 8 ; +# setflag = csetflag[cbyte].c[cbit] ; +# skipflag = cskipflag[cbyte].c[cbit] ; +# +# channel range is 0-199, range is 32 bytes (last 7 unknown) +# +MEM_FORMAT = MEM_FORMAT + """ +#seekto 0x0100; +struct { + bit c[8]; +} csetflag[32]; + +struct { + u8 unknown0100[7]; +} ropt0100; + +#seekto 0x0120; +struct { + bit c[8]; +} cskipflag[32]; + +struct { + u8 unknown0120[7]; +} ropt0120; +""" +# TH9000 memory map +# section: 5 TX/RX Range +# used to set the TX/RX range of the radio (e.g. 222-228Mhz for 220 meter) +# possible to set range for tx/rx +# +MEM_FORMAT = MEM_FORMAT + """ +#seekto 0x0200; +struct { + bbcd txrangelow[4]; + bbcd txrangehi[4]; + bbcd rxrangelow[4]; + bbcd rxrangehi[4]; +} freqrange; +""" +# TH9000 memory map +# section: 6 bootup_passwd +# used to set bootup passwd (see boot_passwd checkbox option) +# +# options - bootup password +# +# bytes:bit type description +# --------------------------------------------------------------------------- +# 6 u8 bootup_passwd[6] bootup passwd, 6 chars, numberic chars 30-39 , see boot_passwd checkbox to set +# 10 u8 unknown; +# + +MEM_FORMAT = MEM_FORMAT + """ +#seekto 0x0210; +struct { + u8 bootup_passwd[6]; + u8 unknown2010[10]; +} ropt0210; +""" +# TH9000/220 memory map +# section: 7 Radio Options +# used to set a number of radio options +# +# bytes:bit type description +# --------------------------------------------------------------------------- +# 1 u8 display_mode display mode, range 0-2, 0=freq,1=channel,2=name (selecting name affects vfo_mr) +# 1 u8 vfo_mr; vfo_mr , 0=vfo, mr=1 +# 1 u8 unknown; +# 1 u8 squelch; squelch level, range 0-19, hex for menu +# 1 u8 unknown[2]; +# 1 u8 channel_lock; if display_mode[channel] selected, then lock=1,no lock =0 +# 1 u8 unknown; +# 1 u8 bg_brightness ; background brightness, range 0-21, hex, menu index +# 1 u8 unknown; +# 1 u8 bg_color ; bg color, menu index, blue 0 , orange 1, purple 2 +# 1 u8 tbst_freq ; tbst freq , menu 0 = 1750Hz, 1=2100 , 2=1000 , 3=1450hz +# 1 u8 timeout_timer; timeout timer, hex, value = minutes, 0= no timeout +# 1 u8 unknown; +# 1 u8 auto_power_off; auto power off, range 0-3, off,30min, 1hr, 2hr, hex menu index +# 1 u8 voice_prompt; voice prompt, value 0,1 , Beep ON = 1, Beep Off = 2 +# +# description of function setup options, starting at 0x0230 +# +# bytes:bit type description +# --------------------------------------------------------------------------- +# 1 u8 // 0 +# :4 unknown:6 +# :1 elim_sql_tail:1 eliminate squelsh tail when no ctcss checkbox (1=checked) +# :1 sql_key_function "squelch off" 1 , "squelch momentary off" 0 , menu index +# 2 u8 unknown[2] /1-2 +# 1 u8 // 3 +# :4 unknown:4 +# :1 inhibit_init_ops:1 //bit 5 +# :1 unknownD:1 +# :1 inhibit_setup_bg_chk:1 //bit 7 +# :1 unknown:1 +# 1 u8 tail_elim_type menu , (off=0,120=1,180=2), // 4 +# 1 u8 choose_tx_power menu , (60w=0,25w=1) // 5 +# 2 u8 unknown[2]; // 6-7 +# 1 u8 bootup_passwd_flag checkbox 1=on, 0=off // 8 +# 7 u8 unknown[7]; // 9-F +# +MEM_FORMAT = MEM_FORMAT + """ +#seekto 0x0220; +struct { + u8 display_mode; + u8 vfo_mr; + u8 unknown0220A; + u8 squelch; + u8 unknown0220B[2]; + u8 channel_lock; + u8 unknown0220C; + u8 bg_brightness; + u8 unknown0220D; + u8 bg_color; + u8 tbst_freq; + u8 timeout_timer; + u8 unknown0220E; + u8 auto_power_off; + u8 voice_prompt; + u8 unknown0230A:6, + elim_sql_tail:1, + sql_key_function:1; + u8 unknown0230B[2]; + u8 unknown0230C:4, + inhibit_init_ops:1, + unknown0230D:1, + inhibit_setup_bg_chk:1, + unknown0230E:1; + u8 tail_elim_type; + u8 choose_tx_power; + u8 unknown0230F[2]; + u8 bootup_passwd_flag; + u8 unknown0230G[7]; +} settings; +""" +# TH9000 memory map +# section: 8B Startup Label +# +# bytes:bit type description +# --------------------------------------------------------------------------- +# 7 char start_label[7] label displayed at startup (usually your call sign) +# +MEM_FORMAT = MEM_FORMAT + """ +#seekto 0x03E0; +struct { + char startname[7]; +} slabel; +""" +# TH9000/220 memory map +# section: 9 Channel Bank +# description of channel bank (200 channels , range 0-199) +# Each 32 Byte (0x20 hex) record: +# bytes:bit type description +# --------------------------------------------------------------------------- +# 4 bbcd freq[4] receive frequency in packed binary coded decimal +# 4 bbcd offset[4] transmit offset in packed binary coded decimal (note: plus/minus direction set by 'duplex' field) +# 1 u8 +# :4 unknown:4 +# :4 tuning_step:4 tuning step, menu index value from 0-9 +# 5,6.25,8.33,10,12.5,15,20,25,30,50 +# 1 u8 +# :4 unknown:4 not yet decoded, used for DCS coding? +# :2 channel_width:2 channel spacing, menu index value from 0-3 +# 25,20,12.5 +# :1 reverse:1 reverse flag, 0=off, 1=on (reverses tx and rx freqs) +# :1 txoff:1 transmitt off flag, 0=transmit , 1=do not transmit +# 1 u8 +# :1 talkaround:1 talkaround flag, 0=off, 1=on (bypasses repeater) +# :1 compander:1 compander flag, 0=off, 1=on (turns on/off voice compander option) +# :2 unknown:2 +# :2 power:2 tx power setting, value range 0-2, 0=hi,1=med,2=lo +# :2 duplex:2 duplex settings, 0=simplex,2= minus(-) offset, 3= plus (+) offset (see offset field) +# +# 1 u8 +# :4 unknown:4 +# :2 rxtmode:2 rx tone mode, value range 0-2, 0=none, 1=CTCSS, 2=DCS (ctcss tone in field rxtone) +# :2 txtmode:2 tx tone mode, value range 0-2, 0=none, 1=CTCSS, 3=DCS (ctcss tone in field txtone) +# 1 u8 +# :2 unknown:2 +# :6 txtone:6 tx ctcss tone, menu index +# 1 u8 +# :2 unknown:2 +# :6 rxtone:6 rx ctcss tone, menu index +# 1 u8 txcode ?, not used for ctcss +# 1 u8 rxcode ?, not used for ctcss +# 3 u8 unknown[3] +# 7 char name[7] 7 byte char string for channel name +# 1 u8 +# :6 unknown:6, +# :2 busychannellockout:2 busy channel lockout option , 0=off, 1=repeater, 2=busy (lock out tx if channel busy) +# 4 u8 unknownI[4]; +# 1 u8 +# :7 unknown:7 +# :1 scrambler:1 scrambler flag, 0=off, 1=on (turns on tyt scrambler option) +# +MEM_FORMAT = MEM_FORMAT + """ +#seekto 0x2000; +struct { + bbcd freq[4]; + bbcd offset[4]; + u8 unknown2000A:4, + tuning_step:4; + u8 rxdcsextra:1, + txdcsextra:1, + rxinv:1, + txinv:1, + channel_width:2, + reverse:1, + txoff:1; + u8 talkaround:1, + compander:1, + unknown2000C:2, + power:2, + duplex:2; + u8 unknown2000D:4, + rxtmode:2, + txtmode:2; + u8 unknown2000E:2, + txtone:6; + u8 unknown2000F:2, + rxtone:6; + u8 txcode; + u8 rxcode; + u8 unknown2000G[3]; + char name[7]; + u8 unknown2000H:6, + busychannellockout:2; + u8 unknown2000I[4]; + u8 unknown2000J:7, + scrambler:1; +} memory[200] ; +""" + +def _echo_write(radio, data): + try: + radio.pipe.write(data) + radio.pipe.read(len(data)) + except Exception, e: + LOG.error("Error writing to radio: %s" % e) + raise errors.RadioError("Unable to write to radio") + + +def _checksum(data): + cs = 0 + for byte in data: + cs += ord(byte) + return cs % 256 + +def _read(radio, length): + try: + data = radio.pipe.read(length) + except Exception, e: + LOG.error( "Error reading from radio: %s" % e) + raise errors.RadioError("Unable to read from radio") + + if len(data) != length: + LOG.error( "Short read from radio (%i, expected %i)" % (len(data), + length)) + LOG.debug(util.hexprint(data)) + raise errors.RadioError("Short read from radio") + return data + + + +def _ident(radio): + radio.pipe.timeout = 1 + _echo_write(radio,"PROGRAM") + response = radio.pipe.read(3) + if response != "QX\06": + LOG.debug( "Response was :\n%s" % util.hexprint(response)) + raise errors.RadioError("Unsupported model") + _echo_write(radio, "\x02") + response = radio.pipe.read(16) + LOG.debug(util.hexprint(response)) + if response[1:8] != "TH-9000": + LOG.error( "Looking for:\n%s" % util.hexprint("TH-9000")) + LOG.error( "Response was:\n%s" % util.hexprint(response)) + raise errors.RadioError("Unsupported model") + +def _send(radio, cmd, addr, length, data=None): + frame = struct.pack(">cHb", cmd, addr, length) + if data: + frame += data + frame += chr(_checksum(frame[1:])) + frame += "\x06" + _echo_write(radio, frame) + LOG.debug("Sent:\n%s" % util.hexprint(frame)) + if data: + result = radio.pipe.read(1) + if result != "\x06": + LOG.debug( "Ack was: %s" % repr(result)) + raise errors.RadioError("Radio did not accept block at %04x" % addr) + return + result = _read(radio, length + 6) + LOG.debug("Got:\n%s" % util.hexprint(result)) + header = result[0:4] + data = result[4:-2] + ack = result[-1] + if ack != "\x06": + LOG.debug("Ack was: %s" % repr(ack)) + raise errors.RadioError("Radio NAK'd block at %04x" % addr) + _cmd, _addr, _length = struct.unpack(">cHb", header) + if _addr != addr or _length != _length: + LOG.debug( "Expected/Received:") + LOG.debug(" Length: %02x/%02x" % (length, _length)) + LOG.debug( " Addr: %04x/%04x" % (addr, _addr)) + raise errors.RadioError("Radio send unexpected block") + cs = _checksum(result[1:-2]) + if cs != ord(result[-2]): + LOG.debug( "Calculated: %02x" % cs) + LOG.debug( "Actual: %02x" % ord(result[-2])) + raise errors.RadioError("Block at 0x%04x failed checksum" % addr) + return data + + +def _finish(radio): + endframe = "\x45\x4E\x44" + _echo_write(radio, endframe) + result = radio.pipe.read(1) + # TYT radios acknowledge the "endframe" command, Luiton radios do not. + if result != "" and result != "\x06": + LOG.error( "Got:\n%s" % util.hexprint(result)) + raise errors.RadioError("Radio did not finish cleanly") + +def do_download(radio): + + _ident(radio) + + _memobj = None + data = "" + + for start,end in radio._ranges: + for addr in range(start,end,0x10): + block = _send(radio,'R',addr,0x10) + data += block + status = chirp_common.Status() + status.cur = len(data) + status.max = end + status.msg = "Downloading from radio" + radio.status_fn(status) + + _finish(radio) + + return memmap.MemoryMap(data) + +def do_upload(radio): + + _ident(radio) + + for start,end in radio._ranges: + for addr in range(start,end,0x10): + if addr < 0x0100: + continue + block = radio._mmap[addr:addr+0x10] + _send(radio,'W',addr,len(block),block) + status = chirp_common.Status() + status.cur = addr + status.max = end + status.msg = "Uploading to Radio" + radio.status_fn(status) + + _finish(radio) + + + +# +# The base class, extended for use with other models +# +class Th9000Radio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """TYT TH-9000""" + VENDOR = "TYT" + MODEL = "TH9000 Base" + BAUD_RATE = 9600 + valid_freq = [(900000000, 999000000)] + + + _memsize = MMAPSIZE + _ranges = [(0x0000,0x4000)] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("The TYT TH-9000 driver is an beta version." + "Proceed with Caution and backup your data") + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_cross = True + rf.has_tuning_step = False + rf.has_rx_dtcs = True + rf.valid_skips = ["","S"] + rf.memory_bounds = (0, 199) + rf.valid_name_length = 7 + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-" + rf.valid_modes = MODES + rf.valid_tmodes = ['','Tone','TSQL','DTCS','Cross'] + rf.valid_cross_modes = ['Tone->DTCS','DTCS->Tone', + '->Tone','->DTCS','Tone->Tone'] + rf.valid_power_levels = POWER_LEVELS + rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES + rf.valid_bands = self.valid_freq + return rf + + # Do a download of the radio from the serial port + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + # Do an upload of the radio to the serial port + def sync_out(self): + do_upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + + # Return a raw representation of the memory object, which + # is very helpful for development + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + # not working yet + def _get_dcs_index(self, _mem,which): + base = getattr(_mem, '%scode' % which) + extra = getattr(_mem, '%sdcsextra' % which) + return (int(extra) << 8) | int(base) + + def _set_dcs_index(self, _mem, which, index): + base = getattr(_mem, '%scode' % which) + extra = getattr(_mem, '%sdcsextra' % which) + base.set_value(index & 0xFF) + extra.set_value(index >> 8) + + + # Extract a high-level memory object from the low-level memory map + # This is called to populate a memory in the UI + def get_memory(self, number): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[number] + + # get flag info + cbyte = number / 8 ; + cbit = 7 - (number % 8) ; + setflag = self._memobj.csetflag[cbyte].c[cbit]; + skipflag = self._memobj.cskipflag[cbyte].c[cbit]; + + mem = chirp_common.Memory() + + mem.number = number # Set the memory number + + if setflag == 1: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 100 + mem.offset = int(_mem.offset) * 100 + mem.name = str(_mem.name).rstrip() # Set the alpha tag + mem.duplex = DUPLEXES[_mem.duplex] + mem.mode = MODES[_mem.channel_width] + mem.power = POWER_LEVELS[_mem.power] + + rxtone = txtone = None + + + rxmode = TMODES[_mem.rxtmode] + txmode = TMODES[_mem.txtmode] + + + + # doesn't work + if rxmode == "Tone": + rxtone = TONES[_mem.rxtone] + elif rxmode == "DTCS": + rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,'rx')] + + if txmode == "Tone": + txtone = TONES[_mem.txtone] + elif txmode == "DTCS": + txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,'tx')] + + rxpol = _mem.rxinv and "R" or "N" + txpol = _mem.txinv and "R" or "N" + + chirp_common.split_tone_decode(mem, + (txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) + + mem.skip = "S" if skipflag == 1 else "" + + + # We'll consider any blank (i.e. 0MHz frequency) to be empty + if mem.freq == 0: + mem.empty = True + + return mem + + # Store details about a high-level memory to the memory map + # This is called when a user edits a memory in the UI + def set_memory(self, mem): + # Get a low-level memory object mapped to the image + + _mem = self._memobj.memory[mem.number] + + cbyte = mem.number / 8 + cbit = 7 - (mem.number % 8) + + if mem.empty: + self._memobj.csetflag[cbyte].c[cbit] = 1 + self._memobj.cskipflag[cbyte].c[cbit] = 1 + return + + self._memobj.csetflag[cbyte].c[cbit] = 0 + self._memobj.cskipflag[cbyte].c[cbit] = 1 if (mem.skip == "S") else 0 + + _mem.set_raw("\x00" * 32) + + _mem.freq = mem.freq / 100 # Convert to low-level frequency + _mem.offset = mem.offset / 100 # Convert to low-level frequency + + _mem.name = mem.name.ljust(7)[:7] # Store the alpha tag + _mem.duplex = DUPLEXES.index(mem.duplex) + + + try: + _mem.channel_width = MODES.index(mem.mode) + except ValueError: + _mem.channel_width = 0 + + ((txmode, txtone, txpol), + (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem) + + _mem.txtmode = TMODES.index(txmode) + + _mem.rxtmode = TMODES.index(rxmode) + + if txmode == "Tone": + _mem.txtone = TONES.index(txtone) + elif txmode == "DTCS": + self._set_dcs_index(_mem,'tx',chirp_common.ALL_DTCS_CODES.index(txtone)) + + if rxmode == "Tone": + _mem.rxtone = TONES.index(rxtone) + elif rxmode == "DTCS": + self._set_dcs_index(_mem, 'rx', chirp_common.ALL_DTCS_CODES.index(rxtone)) + + _mem.txinv = txpol == "R" + _mem.rxinv = rxpol == "R" + + + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + def _get_settings(self): + _settings = self._memobj.settings + _freqrange = self._memobj.freqrange + _slabel = self._memobj.slabel + + basic = RadioSettingGroup("basic","Global Settings") + freqrange = RadioSettingGroup("freqrange","Frequency Ranges") + top = RadioSettingGroup("top","All Settings",basic,freqrange) + settings = RadioSettings(top) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += "" + return filtered + + val = RadioSettingValueString(0,7,_filter(_slabel.startname)) + rs = RadioSetting("startname","Startup Label",val) + basic.append(rs) + + rs = RadioSetting("bg_color","LCD Color", + RadioSettingValueList(BGCOLOR_LIST, BGCOLOR_LIST[_settings.bg_color])) + basic.append(rs) + + rs = RadioSetting("bg_brightness","LCD Brightness", + RadioSettingValueList(BGBRIGHT_LIST, BGBRIGHT_LIST[_settings.bg_brightness])) + basic.append(rs) + + rs = RadioSetting("squelch","Squelch Level", + RadioSettingValueList(SQUELCH_LIST, SQUELCH_LIST[_settings.squelch])) + basic.append(rs) + + rs = RadioSetting("timeout_timer","Timeout Timer (TOT)", + RadioSettingValueList(TIMEOUT_LIST, TIMEOUT_LIST[_settings.timeout_timer])) + basic.append(rs) + + rs = RadioSetting("auto_power_off","Auto Power Off (APO)", + RadioSettingValueList(APO_LIST, APO_LIST[_settings.auto_power_off])) + basic.append(rs) + + rs = RadioSetting("voice_prompt","Beep Prompt", + RadioSettingValueList(BEEP_LIST, BEEP_LIST[_settings.voice_prompt])) + basic.append(rs) + + rs = RadioSetting("tbst_freq","Tone Burst Frequency", + RadioSettingValueList(TBSTFREQ_LIST, TBSTFREQ_LIST[_settings.tbst_freq])) + basic.append(rs) + + rs = RadioSetting("choose_tx_power","Max Level of TX Power", + RadioSettingValueList(TXPWR_LIST, TXPWR_LIST[_settings.choose_tx_power])) + basic.append(rs) + + (flow,fhigh) = self.valid_freq[0] + flow /= 1000 + fhigh /= 1000 + fmidrange = (fhigh- flow)/2 + + rs = RadioSetting("txrangelow","TX Freq, Lower Limit (khz)", RadioSettingValueInteger(flow, + flow + fmidrange, + int(_freqrange.txrangelow)/10)) + freqrange.append(rs) + + rs = RadioSetting("txrangehi","TX Freq, Upper Limit (khz)", RadioSettingValueInteger(fhigh-fmidrange, + fhigh, + int(_freqrange.txrangehi)/10)) + freqrange.append(rs) + + rs = RadioSetting("rxrangelow","RX Freq, Lower Limit (khz)", RadioSettingValueInteger(flow, + flow+fmidrange, + int(_freqrange.rxrangelow)/10)) + freqrange.append(rs) + + rs = RadioSetting("rxrangehi","RX Freq, Upper Limit (khz)", RadioSettingValueInteger(fhigh-fmidrange, + fhigh, + int(_freqrange.rxrangehi)/10)) + freqrange.append(rs) + + return settings + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error( "failed to parse settings") + traceback.print_exc() + return None + + def set_settings(self,settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element,RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + + if name in ["txrangelow","txrangehi","rxrangelow","rxrangehi"]: + LOG.debug( "setting %s = %s" % (name,int(element.value)*10)) + setattr(self._memobj.freqrange,name,int(element.value)*10) + continue + + if name in ["startname"]: + LOG.debug( "setting %s = %s" % (name, element.value)) + setattr(self._memobj.slabel,name,element.value) + continue + + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug( "using apply callback") + element.run_apply_callback() + else: + LOG.debug( "Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug( element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + if MMAPSIZE == len(filedata): + (flow,fhigh) = cls.valid_freq[0] + flow /= 1000000 + fhigh /= 1000000 + + txmin=ord(filedata[0x200])*100 + (ord(filedata[0x201])>>4)*10 + ord(filedata[0x201])%16 + txmax=ord(filedata[0x204])*100 + (ord(filedata[0x205])>>4)*10 + ord(filedata[0x205])%16 + rxmin=ord(filedata[0x208])*100 + (ord(filedata[0x209])>>4)*10 + ord(filedata[0x209])%16 + rxmax=ord(filedata[0x20C])*100 + (ord(filedata[0x20D])>>4)*10 + ord(filedata[0x20D])%16 + + if ( rxmin >= flow and rxmax <= fhigh and txmin >= flow and txmax <= fhigh ): + return True + + return False + +# Declaring Aliases (Clones of the real radios) +class LT580VHF(chirp_common.Alias): + VENDOR = "LUITON" + MODEL = "LT-580_VHF" + + +class LT580UHF(chirp_common.Alias): + VENDOR = "LUITON" + MODEL = "LT-580_UHF" + + +@directory.register +class Th9000220Radio(Th9000Radio): + """TYT TH-9000 220""" + VENDOR = "TYT" + MODEL = "TH9000_220" + BAUD_RATE = 9600 + valid_freq = [(220000000, 260000000)] + +@directory.register +class Th9000144Radio(Th9000220Radio): + """TYT TH-9000 144""" + VENDOR = "TYT" + MODEL = "TH9000_144" + BAUD_RATE = 9600 + valid_freq = [(136000000, 174000000)] + ALIASES = [LT580VHF, ] + +@directory.register +class Th9000440Radio(Th9000220Radio): + """TYT TH-9000 440""" + VENDOR = "TYT" + MODEL = "TH9000_440" + BAUD_RATE = 9600 + valid_freq = [(400000000, 490000000)] + ALIASES = [LT580UHF, ] diff --git a/chirp/drivers/th9800.py b/chirp/drivers/th9800.py new file mode 100644 index 0000000..2dba301 --- /dev/null +++ b/chirp/drivers/th9800.py @@ -0,0 +1,798 @@ +# Copyright 2014 Tom Hayward +# Copyright 2014 Jens Jensen +# Copyright 2014 James Lee N1DDK +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import bitwise, chirp_common, directory, errors, util, memmap +import struct +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings +from chirp.chirp_common import format_freq +import os +import time +import logging +from datetime import date + +LOG = logging.getLogger(__name__) + +TH9800_MEM_FORMAT = """ +struct mem { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + lbcd ctcss[2]; + lbcd dtcs[2]; + u8 power:2, + BeatShift:1, + unknown0a:2, + display:1, // freq=0, name=1 + scan:2; + u8 fmdev:2, // wide=00, mid=01, narrow=10 + scramb:1, + compand:1, + emphasis:1 + unknown1a:2, + sqlmode:1; // carrier, tone + u8 rptmod:2, // off, -, + + reverse:1, + talkaround:1, + step:4; + u8 dtcs_pol:2, + bclo:2, + unknown3:2, + tmode:2; + lbcd offset[4]; + u8 hsdtype:2, // off, 2-tone, 5-tone, dtmf + unknown5a:1, + am:1, + unknown5b:4; + u8 unknown6[3]; + char name[6]; + u8 empty[2]; +}; + +#seekto 0x%04X; +struct mem memory[800]; + +#seekto 0x%04X; +struct { + struct mem lower; + struct mem upper; +} scanlimits[5]; + +#seekto 0x%04X; +struct { + u8 unk0xdc20:5, + left_sql:3; + u8 apo; + u8 unk0xdc22:5, + backlight:3; + u8 unk0xdc23; + u8 beep:1, + keylock:1, + pttlock:2, + unk0xdc24_32:2, + hyper_chan:1, + right_func_key:1; + u8 tbst_freq:2, + ani_display:1, + unk0xdc25_4:1 + mute_mode:2, + unk0xdc25_10:2; + u8 auto_xfer:1, + auto_contact:1, + unk0xdc26_54:2, + auto_am:1, + unk0xdc26_210:3; + u8 unk0xdc27_76543:5, + scan_mode:1, + unk0xdc27_1:1, + scan_resume:1; + u16 scramb_freq; + u16 scramb_freq1; + u8 exit_delay; + u8 unk0xdc2d; + u8 unk0xdc2e:5, + right_sql:3; + u8 unk0xdc2f:4, + beep_vol:4; + u8 tot; + u8 tot_alert; + u8 tot_rekey; + u8 tot_reset; + u8 unk0xdc34; + u8 unk0xdc35; + u8 unk0xdc36; + u8 unk0xdc37; + u8 p1; + u8 p2; + u8 p3; + u8 p4; +} settings; + +#seekto 0x%04X; +u8 chan_active[128]; +u8 scan_enable[128]; +u8 priority[128]; + +#seekto 0x%04X; +struct { + char sn[8]; + char model[8]; + char code[16]; + u8 empty[8]; + lbcd prog_yr[2]; + lbcd prog_mon; + lbcd prog_day; + u8 empty_10f2c[4]; +} info; + +struct { + lbcd lorx[4]; + lbcd hirx[4]; + lbcd lotx[4]; + lbcd hitx[4]; +} bandlimits[9]; + +""" + + +BLANK_MEMORY = "\xFF" * 8 + "\x00\x10\x23\x00\xC0\x08\x06\x00" \ + "\x00\x00\x76\x00\x00\x00" + "\xFF" * 10 +DTCS_POLARITY = ["NN", "RN", "NR", "RR"] +SCAN_MODES = ["", "S", "P"] +MODES = ["WFM", "FM", "NFM"] +TMODES = ["", "Tone", "TSQL", "DTCS"] +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("Mid2", watts=10.00), + chirp_common.PowerLevel("Mid1", watts=20.00), + chirp_common.PowerLevel("High", watts=50.00)] +BUSY_LOCK = ["off", "Carrier", "2 tone"] +MICKEYFUNC = ["None", "SCAN", "SQL.OFF", "TCALL", "PPTR", "PRI", "LOW", "TONE", + "MHz", "REV", "HOME", "BAND", "VFO/MR"] +SQLPRESET = ["Off", "2", "5", "9", "Full"] +BANDS = ["30MHz", "50MHz", "60MHz", "108MHz", "150MHz", "250MHz", "350MHz", + "450MHz", "850MHz"] +STEPS = [2.5, 5.0, 6.25, 7.5, 8.33, 10.0, 12.5, + 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + + +class TYTTH9800Base(chirp_common.Radio): + """Base class for TYT TH-9800""" + VENDOR = "TYT" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 800) + rf.has_bank = False + rf.has_tuning_step = True + rf.valid_tuning_steps = STEPS + rf.can_odd_split = True + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = TMODES + rf.has_ctone = False + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "#*-+" + rf.valid_bands = [(26000000, 33000000), + (47000000, 54000000), + (108000000, 180000000), + (220000000, 260000000), + (350000000, 399995000), + (400000000, 512000000), + (750000000, 950000000)] + rf.valid_skips = SCAN_MODES + rf.valid_modes = MODES + ["AM"] + rf.valid_name_length = 6 + rf.has_settings = True + return rf + + def process_mmap(self): + self._memobj = bitwise.parse( + TH9800_MEM_FORMAT % + (self._mmap_offset, self._scanlimits_offset, self._settings_offset, + self._chan_active_offset, self._info_offset), self._mmap) + + def get_active(self, banktype, num): + """get active flag for channel active, + scan enable, or priority banks""" + bank = getattr(self._memobj, banktype) + index = (num - 1) / 8 + bitpos = (num - 1) % 8 + mask = 2**bitpos + enabled = bank[index] & mask + if enabled: + return True + else: + return False + + def set_active(self, banktype, num, enable=True): + """set active flag for channel active, + scan enable, or priority banks""" + bank = getattr(self._memobj, banktype) + index = (num - 1) / 8 + bitpos = (num - 1) % 8 + mask = 2**bitpos + if enable: + bank[index] |= mask + else: + bank[index] &= ~mask + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + + mem.empty = not self.get_active("chan_active", number) + if mem.empty: + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + txfreq = int(_mem.tx_freq) * 10 + if txfreq == mem.freq: + mem.duplex = "" + elif txfreq == 0: + mem.duplex = "off" + mem.offset = 0 + elif abs(txfreq - mem.freq) > 70000000: + mem.duplex = "split" + mem.offset = txfreq + elif txfreq < mem.freq: + mem.duplex = "-" + mem.offset = mem.freq - txfreq + elif txfreq > mem.freq: + mem.duplex = "+" + mem.offset = txfreq - mem.freq + + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol] + + mem.tmode = TMODES[int(_mem.tmode)] + mem.ctone = mem.rtone = int(_mem.ctcss) / 10.0 + mem.dtcs = int(_mem.dtcs) + + mem.name = str(_mem.name) + mem.name = mem.name.replace("\xFF", " ").rstrip() + + if not self.get_active("scan_enable", number): + mem.skip = "S" + elif self.get_active("priority", number): + mem.skip = "P" + else: + mem.skip = "" + + mem.mode = _mem.am and "AM" or MODES[int(_mem.fmdev)] + + mem.power = POWER_LEVELS[_mem.power] + mem.tuning_step = STEPS[_mem.step] + + mem.extra = RadioSettingGroup("extra", "Extra") + + opts = ["Frequency", "Name"] + display = RadioSetting( + "display", "Display", + RadioSettingValueList(opts, opts[_mem.display])) + mem.extra.append(display) + + bclo = RadioSetting( + "bclo", "Busy Lockout", + RadioSettingValueList(BUSY_LOCK, BUSY_LOCK[_mem.bclo])) + bclo.set_doc("Busy Lockout") + mem.extra.append(bclo) + + emphasis = RadioSetting( + "emphasis", "Emphasis", + RadioSettingValueBoolean(bool(_mem.emphasis))) + emphasis.set_doc("Boosts 300Hz to 2500Hz mic response") + mem.extra.append(emphasis) + + compand = RadioSetting( + "compand", "Compand", + RadioSettingValueBoolean(bool(_mem.compand))) + compand.set_doc("Compress Audio") + mem.extra.append(compand) + + BeatShift = RadioSetting( + "BeatShift", "BeatShift", + RadioSettingValueBoolean(bool(_mem.BeatShift))) + BeatShift.set_doc("Beat Shift") + mem.extra.append(BeatShift) + + TalkAround = RadioSetting( + "talkaround", "Talk Around", + RadioSettingValueBoolean(bool(_mem.talkaround))) + TalkAround.set_doc("Simplex mode when out of range of repeater") + mem.extra.append(TalkAround) + + scramb = RadioSetting( + "scramb", "Scramble", + RadioSettingValueBoolean(bool(_mem.scramb))) + scramb.set_doc("Frequency inversion Scramble") + mem.extra.append(scramb) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + _prev_active = self.get_active("chan_active", mem.number) + self.set_active("chan_active", mem.number, not mem.empty) + if mem.empty or not _prev_active: + LOG.debug("initializing memory channel %d" % mem.number) + _mem.set_raw(BLANK_MEMORY) + + if mem.empty: + return + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "split": + _mem.tx_freq = mem.offset / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "off": + _mem.tx_freq = 0 + _mem.offset = 0 + else: + _mem.tx_freq = mem.freq / 10 + + _mem.tmode = TMODES.index(mem.tmode) + if mem.tmode == "TSQL" or mem.tmode == "DTCS": + _mem.sqlmode = 1 + else: + _mem.sqlmode = 0 + _mem.ctcss = mem.rtone * 10 + _mem.dtcs = mem.dtcs + _mem.dtcs_pol = DTCS_POLARITY.index(mem.dtcs_polarity) + + _mem.name = mem.name.ljust(6, "\xFF") + + # autoset display to name if filled, else show frequency + if mem.extra: + # mem.extra only seems to be populated when called from edit panel + display = mem.extra["display"] + else: + display = None + if mem.name: + _mem.display = True + if display and not display.changed(): + display.value = "Name" + else: + _mem.display = False + if display and not display.changed(): + display.value = "Frequency" + + _mem.scan = SCAN_MODES.index(mem.skip) + if mem.skip == "P": + self.set_active("priority", mem.number, True) + self.set_active("scan_enable", mem.number, True) + elif mem.skip == "S": + self.set_active("priority", mem.number, False) + self.set_active("scan_enable", mem.number, False) + elif mem.skip == "": + self.set_active("priority", mem.number, False) + self.set_active("scan_enable", mem.number, True) + + if mem.mode == "AM": + _mem.am = True + _mem.fmdev = 0 + else: + _mem.am = False + _mem.fmdev = MODES.index(mem.mode) + + if mem.power: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 # low + _mem.step = STEPS.index(mem.tuning_step) + + for setting in mem.extra: + LOG.debug("@set_mem:", setting.get_name(), setting.value) + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + _info = self._memobj.info + _bandlimits = self._memobj.bandlimits + basic = RadioSettingGroup("basic", "Basic") + info = RadioSettingGroup("info", "Model Info") + top = RadioSettings(basic, info) + basic.append(RadioSetting( + "beep", "Beep", + RadioSettingValueBoolean(_settings.beep))) + basic.append(RadioSetting( + "beep_vol", "Beep Volume", + RadioSettingValueInteger(0, 15, _settings.beep_vol))) + basic.append(RadioSetting( + "keylock", "Key Lock", + RadioSettingValueBoolean(_settings.keylock))) + basic.append(RadioSetting( + "ani_display", "ANI Display", + RadioSettingValueBoolean(_settings.ani_display))) + basic.append(RadioSetting( + "auto_xfer", "Auto Transfer", + RadioSettingValueBoolean(_settings.auto_xfer))) + basic.append(RadioSetting( + "auto_contact", "Auto Contact Always Remind", + RadioSettingValueBoolean(_settings.auto_contact))) + basic.append(RadioSetting( + "auto_am", "Auto AM", + RadioSettingValueBoolean(_settings.auto_am))) + basic.append(RadioSetting( + "left_sql", "Left Squelch", + RadioSettingValueList( + SQLPRESET, SQLPRESET[_settings.left_sql]))) + basic.append(RadioSetting( + "right_sql", "Right Squelch", + RadioSettingValueList( + SQLPRESET, SQLPRESET[_settings.right_sql]))) +# basic.append(RadioSetting("apo", "Auto Power off (0.1h)", +# RadioSettingValueInteger(0, 20, _settings.apo))) + opts = ["Off"] + ["%0.1f" % (t / 10.0) for t in range(1, 21, 1)] + basic.append(RadioSetting( + "apo", "Auto Power off (Hours)", + RadioSettingValueList(opts, opts[_settings.apo]))) + opts = ["Off", "1", "2", "3", "Full"] + basic.append(RadioSetting( + "backlight", "Display Backlight", + RadioSettingValueList(opts, opts[_settings.backlight]))) + opts = ["Off", "Right", "Left", "Both"] + basic.append(RadioSetting( + "pttlock", "PTT Lock", + RadioSettingValueList(opts, opts[_settings.pttlock]))) + opts = ["Manual", "Auto"] + basic.append(RadioSetting( + "hyper_chan", "Hyper Channel", + RadioSettingValueList(opts, opts[_settings.hyper_chan]))) + opts = ["Key 1", "Key 2"] + basic.append(RadioSetting( + "right_func_key", "Right Function Key", + RadioSettingValueList(opts, opts[_settings.right_func_key]))) + opts = ["1000Hz", "1450Hz", "1750Hz", "2100Hz"] + basic.append(RadioSetting( + "tbst_freq", "Tone Burst Frequency", + RadioSettingValueList(opts, opts[_settings.tbst_freq]))) + opts = ["Off", "TX", "RX", "TX RX"] + basic.append(RadioSetting( + "mute_mode", "Mute Mode", + RadioSettingValueList(opts, opts[_settings.mute_mode]))) + opts = ["MEM", "MSM"] + scanmode = RadioSetting( + "scan_mode", "Scan Mode", + RadioSettingValueList(opts, opts[_settings.scan_mode])) + scanmode.set_doc("MEM = Normal scan, bypass channels marked skip. " + " MSM = Scan only channels marked priority.") + basic.append(scanmode) + opts = ["TO", "CO"] + basic.append(RadioSetting( + "scan_resume", "Scan Resume", + RadioSettingValueList(opts, opts[_settings.scan_resume]))) + opts = ["%0.1f" % (t / 10.0) for t in range(0, 51, 1)] + basic.append(RadioSetting( + "exit_delay", "Span Transit Exit Delay", + RadioSettingValueList(opts, opts[_settings.exit_delay]))) + basic.append(RadioSetting( + "tot", "Time Out Timer (minutes)", + RadioSettingValueInteger(0, 30, _settings.tot))) + basic.append(RadioSetting( + "tot_alert", "Time Out Timer Pre Alert(seconds)", + RadioSettingValueInteger(0, 15, _settings.tot_alert))) + basic.append(RadioSetting( + "tot_rekey", "Time Out Rekey (seconds)", + RadioSettingValueInteger(0, 15, _settings.tot_rekey))) + basic.append(RadioSetting( + "tot_reset", "Time Out Reset(seconds)", + RadioSettingValueInteger(0, 15, _settings.tot_reset))) + basic.append(RadioSetting( + "p1", "P1 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p1]))) + basic.append(RadioSetting( + "p2", "P2 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p2]))) + basic.append(RadioSetting( + "p3", "P3 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p3]))) + basic.append(RadioSetting( + "p4", "P4 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p4]))) +# opts = ["0", "1"] +# basic.append(RadioSetting("x", "Desc", +# RadioSettingValueList(opts, opts[_settings.x]))) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + rsvs = RadioSettingValueString(0, 8, _filter(_info.sn)) + rsvs.set_mutable(False) + rs = RadioSetting("sn", "Serial Number", rsvs) + info.append(rs) + + rsvs = RadioSettingValueString(0, 8, _filter(_info.model)) + rsvs.set_mutable(False) + rs = RadioSetting("model", "Model Name", rsvs) + info.append(rs) + + rsvs = RadioSettingValueString(0, 16, _filter(_info.code)) + rsvs.set_mutable(False) + rs = RadioSetting("code", "Model Code", rsvs) + info.append(rs) + + progdate = "%d/%d/%d" % (_info.prog_mon, _info.prog_day, + _info.prog_yr) + rsvs = RadioSettingValueString(0, 10, progdate) + rsvs.set_mutable(False) + rs = RadioSetting("progdate", "Last Program Date", rsvs) + info.append(rs) + + # 9 band limits + for i in range(0, 9): + objname = BANDS[i] + "lorx" + objnamepp = BANDS[i] + " Rx Start" + # rsv = RadioSettingValueInteger(0, 100000000, + # int(_bandlimits[i].lorx)) + rsv = RadioSettingValueString( + 0, 10, format_freq(int(_bandlimits[i].lorx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + objname = BANDS[i] + "hirx" + objnamepp = BANDS[i] + " Rx end" + rsv = RadioSettingValueString( + 0, 10, format_freq(int(_bandlimits[i].hirx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + objname = BANDS[i] + "lotx" + objnamepp = BANDS[i] + " Tx Start" + rsv = RadioSettingValueString( + 0, 10, format_freq(int(_bandlimits[i].lotx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + objname = BANDS[i] + "hitx" + objnamepp = BANDS[i] + " Tx end" + rsv = RadioSettingValueString( + 0, 10, format_freq(int(_bandlimits[i].hitx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + _info = self._memobj.info + _bandlimits = self._memobj.bandlimits + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + oldval = getattr(_settings, setting) + newval = element.value + + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception, e: + LOG.debug(element.get_name()) + raise + + +@directory.register +class TYTTH9800File(TYTTH9800Base, chirp_common.FileBackedRadio): + """TYT TH-9800 .dat file""" + MODEL = "TH-9800 File" + + FILE_EXTENSION = "dat" + + _memsize = 69632 + _mmap_offset = 0x1100 + _scanlimits_offset = 0xC800 + _mmap_offset + _settings_offset = 0xCB20 + _mmap_offset + _chan_active_offset = 0xCB80 + _mmap_offset + _info_offset = 0xfe00 + _mmap_offset + + def __init__(self, pipe): + self.errors = [] + self._mmap = None + + if isinstance(pipe, str): + self.pipe = None + self.load_mmap(pipe) + else: + chirp_common.FileBackedRadio.__init__(self, pipe) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and filename.endswith('.dat') + + +def _identify(radio): + """Do identify handshake with TYT""" + try: + radio.pipe.write("\x02PROGRA") + ack = radio.pipe.read(1) + if ack != "A": + util.hexprint(ack) + raise errors.RadioError("Radio did not ACK first command: %x" + % ord(ack)) + except: + LOG.debug(util.hexprint(ack)) + raise errors.RadioError("Unable to communicate with the radio") + + radio.pipe.write("M\x02") + ident = radio.pipe.read(16) + radio.pipe.write("A") + r = radio.pipe.read(1) + if r != "A": + raise errors.RadioError("Ack failed") + return ident + + +def _download(radio, memsize=0x10000, blocksize=0x80): + """Download from TYT TH-9800""" + data = _identify(radio) + LOG.info("ident:", util.hexprint(data)) + offset = 0x100 + for addr in range(offset, memsize, blocksize): + msg = struct.pack(">cHB", "R", addr, blocksize) + radio.pipe.write(msg) + block = radio.pipe.read(blocksize + 4) + if len(block) != (blocksize + 4): + LOG.debug(util.hexprint(block)) + raise errors.RadioError("Radio sent a short block") + radio.pipe.write("A") + ack = radio.pipe.read(1) + if ack != "A": + LOG.debug(util.hexprint(ack)) + raise errors.RadioError("Radio NAKed block") + data += block[4:] + + if radio.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = memsize + status.msg = "Cloning from radio" + radio.status_fn(status) + + radio.pipe.write("ENDR") + + return memmap.MemoryMap(data) + + +def _upload(radio, memsize=0xF400, blocksize=0x80): + """Upload to TYT TH-9800""" + data = _identify(radio) + + radio.pipe.timeout = 1 + + if data != radio._mmap[:radio._mmap_offset]: + raise errors.RadioError( + "Model mis-match: \n%s\n%s" % + (util.hexprint(data), + util.hexprint(radio._mmap[:radio._mmap_offset]))) + # in the factory software they update the last program date when + # they upload, So let's do the same + today = date.today() + y = today.year + m = today.month + d = today.day + _info = radio._memobj.info + + ly = _info.prog_yr + lm = _info.prog_mon + ld = _info.prog_day + LOG.debug("Updating last program date:%d/%d/%d" % (lm, ld, ly)) + LOG.debug(" to today:%d/%d/%d" % (m, d, y)) + + _info.prog_yr = y + _info.prog_mon = m + _info.prog_day = d + + offset = 0x0100 + for addr in range(offset, memsize, blocksize): + mapaddr = addr + radio._mmap_offset - offset + LOG.debug("addr: 0x%04X, mmapaddr: 0x%04X" % (addr, mapaddr)) + msg = struct.pack(">cHB", "W", addr, blocksize) + msg += radio._mmap[mapaddr:(mapaddr + blocksize)] + LOG.debug(util.hexprint(msg)) + radio.pipe.write(msg) + ack = radio.pipe.read(1) + if ack != "A": + LOG.debug(util.hexprint(ack)) + raise errors.RadioError("Radio did not ack block 0x%04X" % addr) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = memsize + status.msg = "Cloning to radio" + radio.status_fn(status) + + # End of clone + radio.pipe.write("ENDW") + + # Checksum? + final_data = radio.pipe.read(3) + LOG.debug("final:", util.hexprint(final_data)) + + +@directory.register +class TYTTH9800Radio(TYTTH9800Base, chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + VENDOR = "TYT" + MODEL = "TH-9800" + BAUD_RATE = 38400 + + _memsize = 65296 + _mmap_offset = 0x0010 + _scanlimits_offset = 0xC800 + _mmap_offset + _settings_offset = 0xCB20 + _mmap_offset + _chan_active_offset = 0xCB80 + _mmap_offset + _info_offset = 0xfe00 + _mmap_offset + + @classmethod + def match_model(cls, filedata, filename): + if len(filedata) != cls._memsize: + return False + # TYT set this model for TH-7800 _AND_ TH-9800 + if not filedata[0xfe18:0xfe1e] == "TH9800": + return False + # TH-9800 bandlimits differ from TH-7800. First band is used + # (non-zero). + first_bandlimit = struct.unpack("BBBBBBBBBBBBBBBB", + filedata[0xfe40:0xfe50]) + if all(v == 0 for v in first_bandlimit): + return False + return True + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ( + 'This is experimental support for TH-9800 ' + 'which is still under development.\n' + 'Please ensure you have a good backup with OEM software.\n' + 'Also please send in bug and enhancement requests!\n' + 'You have been warned. Proceed at your own risk!') + return rp + + def sync_in(self): + try: + self._mmap = _download(self) + except Exception, e: + raise errors.RadioError( + "Failed to communicate with the radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + _upload(self) + except Exception, e: + raise errors.RadioError( + "Failed to communicate with the radio: %s" % e) diff --git a/chirp/drivers/th_uv3r.py b/chirp/drivers/th_uv3r.py new file mode 100644 index 0000000..8e40f3a --- /dev/null +++ b/chirp/drivers/th_uv3r.py @@ -0,0 +1,270 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""TYT uv3r radio management module""" + +import os +import logging +from chirp import chirp_common, bitwise, errors, directory +from chirp.drivers.wouxun import do_download, do_upload + +LOG = logging.getLogger(__name__) + + +def tyt_uv3r_prep(radio): + try: + radio.pipe.write("PROGRAMa") + ack = radio.pipe.read(1) + if ack != "\x06": + raise errors.RadioError("Radio did not ACK first command") + except: + raise errors.RadioError("Unable to communicate with the radio") + + +def tyt_uv3r_download(radio): + tyt_uv3r_prep(radio) + return do_download(radio, 0x0000, 0x0910, 0x0010) + + +def tyt_uv3r_upload(radio): + tyt_uv3r_prep(radio) + return do_upload(radio, 0x0000, 0x0910, 0x0010) + +mem_format = """ +struct memory { + ul24 duplex:2, + bit:1, + iswide:1, + bits:2, + is625:1, + freq:17; + ul16 offset; + ul16 rx_tone; + ul16 tx_tone; + u8 unknown; + u8 name[6]; +}; + +#seekto 0x0010; +struct memory memory[128]; + +#seekto 0x0870; +u8 emptyflags[16]; +u8 skipflags[16]; +""" + +THUV3R_DUPLEX = ["", "+", "-"] +THUV3R_CHARSET = "".join([chr(ord("0") + x) for x in range(0, 10)] + + [" -*+"] + + [chr(ord("A") + x) for x in range(0, 26)] + + ["_/"]) + + +@directory.register +class TYTUV3RRadio(chirp_common.CloneModeRadio): + VENDOR = "TYT" + MODEL = "TH-UV3R" + BAUD_RATE = 2400 + _memsize = 2320 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_tuning_step = False + rf.has_cross = True + rf.memory_bounds = (1, 128) + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", + "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS"] + rf.valid_skips = [] + rf.valid_modes = ["FM", "NFM"] + rf.valid_name_length = 6 + rf.valid_characters = THUV3R_CHARSET + rf.valid_bands = [(136000000, 520000000)] + rf.valid_tuning_steps = [5.0, 6.25, 10.0, 12.5, 25.0, 37.50, + 50.0, 100.0] + rf.valid_skips = ["", "S"] + return rf + + def sync_in(self): + self.pipe.timeout = 2 + self._mmap = tyt_uv3r_download(self) + self.process_mmap() + + def sync_out(self): + tyt_uv3r_upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _decode_tone_value(self, value): + if value == 0xFFFF: + return "", "N", 0 + elif value & 0x8000: + # FIXME: rev pol + pol = value & 0x4000 and "R" or "N" + return "DTCS", pol, int("%x" % (value & 0x0FFF)) + else: + return "Tone", "N", int("%x" % value) / 10.0 + + def _decode_tone(self, mem, _mem): + tx_mode, tpol, tx_tone = self._decode_tone_value(_mem.tx_tone) + rx_mode, rpol, rx_tone = self._decode_tone_value(_mem.rx_tone) + + if rx_mode == tx_mode == "": + return + + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + if rx_mode == tx_mode == "DTCS": + # Break this for now until we can support this in chirp + tx_tone = rx_tone + + if rx_mode in ["", tx_mode] and rx_tone in [0, tx_tone]: + mem.tmode = rx_mode == "Tone" and "TSQL" or tx_mode + if mem.tmode == "DTCS": + mem.dtcs = tx_tone + elif mem.tmode == "TSQL": + mem.ctone = tx_tone + else: + mem.rtone = tx_tone + return + + mem.cross_mode = "%s->%s" % (tx_mode, rx_mode) + mem.tmode = "Cross" + if tx_mode == "Tone": + mem.rtone = tx_tone + elif tx_mode == "DTCS": + mem.dtcs = tx_tone + if rx_mode == "Tone": + mem.ctone = rx_tone + elif rx_mode == "DTCS": + mem.dtcs = rx_tone # No support for different codes yet + + def _encode_tone(self, mem, _mem): + if mem.tmode == "": + _mem.tx_tone = _mem.rx_tone = 0xFFFF + return + + def _tone(val): + return int("%i" % (val * 10), 16) + + def _dcs(val, pol): + polmask = pol == "R" and 0xC000 or 0x8000 + return int("%i" % (val), 16) | polmask + + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + rx_mode = "" + tx_mode = "Tone" + tx_tone = _tone(mem.rtone) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = _tone(mem.ctone) + elif mem.tmode == "DTCS": + rx_mode = tx_mode = "DTCS" + tx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->", 1) + if tx_mode == "DTCS": + tx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = _tone(mem.rtone) + if rx_mode == "DTCS": + rx_tone = _dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = _tone(mem.ctone) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + + bit = 1 << ((number - 1) % 8) + byte = (number - 1) / 8 + + if self._memobj.emptyflags[byte] & bit: + mem.empty = True + return mem + + mult = _mem.is625 and 6250 or 5000 + mem.freq = _mem.freq * mult + mem.offset = _mem.offset * 5000 + mem.duplex = THUV3R_DUPLEX[_mem.duplex] + mem.mode = _mem.iswide and "FM" or "NFM" + self._decode_tone(mem, _mem) + mem.skip = (self._memobj.skipflags[byte] & bit) and "S" or "" + + for char in _mem.name: + try: + c = THUV3R_CHARSET[char] + except: + c = "" + mem.name += c + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + bit = 1 << ((mem.number - 1) % 8) + byte = (mem.number - 1) / 8 + + if mem.empty: + self._memobj.emptyflags[byte] |= bit + _mem.set_raw("\xFF" * 16) + return + + self._memobj.emptyflags[byte] &= ~bit + + if chirp_common.is_fractional_step(mem.freq): + mult = 6250 + _mem.is625 = True + else: + mult = 5000 + _mem.is625 = False + _mem.freq = mem.freq / mult + _mem.offset = mem.offset / 5000 + _mem.duplex = THUV3R_DUPLEX.index(mem.duplex) + _mem.iswide = mem.mode == "FM" + self._encode_tone(mem, _mem) + + if mem.skip: + self._memobj.skipflags[byte] |= bit + else: + self._memobj.skipflags[byte] &= ~bit + + name = [] + for char in mem.name.ljust(6): + try: + c = THUV3R_CHARSET.index(char) + except: + c = THUV3R_CHARSET.index(" ") + name.append(c) + _mem.name = name + LOG.debug(repr(_mem)) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == 2320 diff --git a/chirp/drivers/th_uv3r25.py b/chirp/drivers/th_uv3r25.py new file mode 100644 index 0000000..ece7959 --- /dev/null +++ b/chirp/drivers/th_uv3r25.py @@ -0,0 +1,209 @@ +# Copyright 2015 Eric Allen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""TYT uv3r (2.5kHz) radio management module""" + +from chirp import chirp_common, bitwise, directory +from chirp.drivers.wouxun import do_download, do_upload + +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString + +from th_uv3r import TYTUV3RRadio, tyt_uv3r_prep, THUV3R_CHARSET + + +def tyt_uv3r_download(radio): + tyt_uv3r_prep(radio) + return do_download(radio, 0x0000, 0x0B30, 0x0010) + + +def tyt_uv3r_upload(radio): + tyt_uv3r_prep(radio) + return do_upload(radio, 0x0000, 0x0B30, 0x0010) + +mem_format = """ +// 20 bytes per memory +struct memory { + ul32 rx_freq; // 4 bytes + ul32 tx_freq; // 8 bytes + + ul16 rx_tone; // 10 bytes + ul16 tx_tone; // 12 bytes + + u8 unknown1a:1, + iswide:1, + bclo_n:1, + vox_n:1, + tail:1, + power_high:1, + voice_mode:2; + u8 name[6]; // 19 bytes + u8 unknown2; // 20 bytes +}; + +#seekto 0x0010; +struct memory memory[128]; + +#seekto 0x0A80; +u8 emptyflags[16]; +u8 skipflags[16]; +""" + +VOICE_MODE_LIST = ["Compander", "Scrambler", "None"] + + +@directory.register +class TYTUV3R25Radio(TYTUV3RRadio): + MODEL = "TH-UV3R-25" + _memsize = 2864 + + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00), + chirp_common.PowerLevel("Low", watts=0.80)] + + def get_features(self): + rf = super(TYTUV3R25Radio, self).get_features() + + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 37.50, + 50.0, 100.0] + rf.valid_power_levels = self.POWER_LEVELS + return rf + + def sync_in(self): + self.pipe.timeout = 2 + self._mmap = tyt_uv3r_download(self) + self.process_mmap() + + def sync_out(self): + tyt_uv3r_upload(self) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + + bit = 1 << ((number - 1) % 8) + byte = (number - 1) / 8 + + if self._memobj.emptyflags[byte] & bit: + mem.empty = True + return mem + + mem.freq = _mem.rx_freq * 10 + mem.offset = abs(_mem.rx_freq - _mem.tx_freq) * 10 + if _mem.tx_freq == _mem.rx_freq: + mem.duplex = "" + elif _mem.tx_freq < _mem.rx_freq: + mem.duplex = "-" + elif _mem.tx_freq > _mem.rx_freq: + mem.duplex = "+" + + mem.mode = _mem.iswide and "FM" or "NFM" + self._decode_tone(mem, _mem) + mem.skip = (self._memobj.skipflags[byte] & bit) and "S" or "" + + for char in _mem.name: + try: + c = THUV3R_CHARSET[char] + except: + c = "" + mem.name += c + mem.name = mem.name.rstrip() + + mem.power = self.POWER_LEVELS[not _mem.power_high] + + mem.extra = RadioSettingGroup("extra", "Extra Settings") + + rs = RadioSetting("bclo_n", "Busy Channel Lockout", + RadioSettingValueBoolean(not _mem.bclo_n)) + mem.extra.append(rs) + + rs = RadioSetting("vox_n", "VOX", + RadioSettingValueBoolean(not _mem.vox_n)) + mem.extra.append(rs) + + rs = RadioSetting("tail", "Squelch Tail Elimination", + RadioSettingValueBoolean(_mem.tail)) + mem.extra.append(rs) + + rs = RadioSetting("voice_mode", "Voice Mode", + RadioSettingValueList( + VOICE_MODE_LIST, + VOICE_MODE_LIST[_mem.voice_mode-1])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + bit = 1 << ((mem.number - 1) % 8) + byte = (mem.number - 1) / 8 + + if mem.empty: + self._memobj.emptyflags[byte] |= bit + _mem.set_raw("\xFF" * 20) + return + + self._memobj.emptyflags[byte] &= ~bit + + _mem.rx_freq = mem.freq / 10 + + if mem.duplex == "": + _mem.tx_freq = _mem.rx_freq + elif mem.duplex == "-": + _mem.tx_freq = _mem.rx_freq - mem.offset / 10.0 + elif mem.duplex == "+": + _mem.tx_freq = _mem.rx_freq + mem.offset / 10.0 + + _mem.iswide = mem.mode == "FM" + + self._encode_tone(mem, _mem) + + if mem.skip: + self._memobj.skipflags[byte] |= bit + else: + self._memobj.skipflags[byte] &= ~bit + + name = [] + for char in mem.name.ljust(6): + try: + c = THUV3R_CHARSET.index(char) + except: + c = THUV3R_CHARSET.index(" ") + name.append(c) + _mem.name = name + + if mem.power == self.POWER_LEVELS[0]: + _mem.power_high = 1 + else: + _mem.power_high = 0 + + for element in mem.extra: + if element.get_name() == 'voice_mode': + setattr(_mem, element.get_name(), int(element.value) + 1) + elif element.get_name().endswith('_n'): + setattr(_mem, element.get_name(), 1 - int(element.value)) + else: + setattr(_mem, element.get_name(), element.value) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize diff --git a/chirp/drivers/th_uv8000.py b/chirp/drivers/th_uv8000.py new file mode 100644 index 0000000..3897d54 --- /dev/null +++ b/chirp/drivers/th_uv8000.py @@ -0,0 +1,1491 @@ +# Copyright 2019: Rick DeWitt (RJD), +# Version 1.0 for TYT-UV8000D/E +# Thanks to Damon Schaefer (K9CQB) and the Loudoun County, VA ARES +# club for the donated radio. +# And thanks to Ian Harris (VA3IHX) for decoding the memory map. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re +import math +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct chns { + ul32 rxfreq; + ul32 txfreq; + u8 rxtone[2]; + u8 txtone[2]; + u8 wide:1 // 0x0c + vox_on:1 + chunk01:1 + bcl:1 // inv bool + epilogue:1 + power:1 + chunk02:1 + chunk03:1; + u8 ani:1 // 0x0d inv + chunk08:1 + ptt:2 + chpad04:4; + u8 chunk05; // 0x0e + u16 id_code; // 0x0f, 10 + u8 chunk06; + u8 name[7]; + ul32 chpad06; // Need 56 byte pad + ul16 chpad07; + u8 chpad08; +}; + +struct fm_chn { + ul16 rxfreq; +}; + +struct frqx { + ul32 rxfreq; + ul24 ofst; + u8 fqunk01:4 // 0x07 + funk10:2 + duplx:2; + u8 rxtone[2]; // 0x08, 9 + u8 txtone[2]; // 0x0a, b + u8 wide:1 // 0x0c + vox_on:1 + funk11:1 + bcl:1 // inv bool + epilogue:1 + power:1 + fqunk02:2; + u8 ani:1 // 0x0d inv bool + fqunk03:1 + ptt:2 + fqunk12:1 + fqunk04:3; + u8 fqunk07; // 0x0e + u16 id_code; // 0x0f, 0x10 + u8 name[7]; // dummy + u8 fqunk09[8]; // empty bytes after 1st entry +}; + +struct bitmap { + u8 map[16]; +}; + +#seekto 0x0010; +struct chns chan_mem[128]; + +#seekto 0x1010; +struct frqx frq[2]; + +#seekto 0x1050; +struct fm_chn fm_stations[25]; + +#seekto 0x1080; +struct { + u8 fmunk01[14]; + ul16 fmcur; +} fmfrqs; + +#seekto 0x1190; +struct bitmap chnmap; + +#seekto 0x11a0; +struct bitmap skpchns; + +#seekto 0x011b0; +struct { + u8 fmset[4]; +} fmmap; + +#seekto 0x011b4; +struct { + u8 setunk01[4]; + u8 setunk02[3]; + u8 chs_name:1 // 0x11bb + txsel:1 + dbw:1 + setunk05:1 + ponfmchs:2 + ponchs:2; + u8 voltx:2 // 0x11bc + setunk04:1 + keylok:1 + setunk07:1 + batsav:3; + u8 setunk09:1 // 0x11bd + rxinhib:1 + rgrbeep:1 // inv bool + lampon:2 + voice:2 + beepon:1; + u8 setunk11:1 // 0x11be + manualset:1 + xbandon:1 // inv + xbandenable:1 + openmsg:2 + ledclr:2; + u8 tot:4 // 0x11bf + sql:4; + u8 setunk27:1 // 0x11c0 + voxdelay:2 + setunk28:1 + voxgain:4; + u8 fmstep:4 // 0x11c1 + freqstep:4; + u8 scanspeed:4 // 0x11c2 + scanmode:4; + u8 scantmo; // 0x11c3 + u8 prichan; // 0x11c4 + u8 setunk12:4 // 0x11c5 + supersave:4; + u8 setunk13; + u8 fmsclo; // 0x11c7 ??? placeholder + u8 radioname[7]; // hex char codes, not true ASCII + u8 fmschi; // ??? placeholder + u8 setunk14[3]; // 0x11d0 + u8 setunk17[2]; // 0x011d3, 4 + u8 setunk18:4 + dtmfspd:4; + u8 dtmfdig1dly:4 // 0x11d6 + dtmfdig1time:4; + u8 stuntype:1 + setunk19:1 + dtmfspms:2 + grpcode:4; + u8 setunk20:1 // 0x11d8 + txdecode:1 + codeabcd:1 + idedit:1 + pttidon:2 + setunk40:1, + dtmfside:1; + u8 setunk50:4, + autoresettmo:4; + u8 codespctim:4, // 0x11da + decodetmo:4; + u8 pttecnt:4 // 0x11db + pttbcnt:4; + lbcd dtmfdecode[3]; + u8 setunk22; + u8 stuncnt; // 0x11e0 + u8 stuncode[5]; + u8 setunk60; + u8 setunk61; + u8 pttbot[8]; // 0x11e8-f + u8 ptteot[8]; // 0x11f0-7 + u8 setunk62; // 0x11f8 + u8 setunk63; + u8 setunk64; // 0x11fa + u8 setunk65; + u8 setunk66; + u8 manfrqyn; // 0x11fd + u8 setunk27:3 + frqr3:1 + setunk28:1 + frqr2:1 + setunk29:1 + frqr1:1; + u8 setunk25; + ul32 frqr1lo; // 0x1200 + ul32 frqr1hi; + ul32 frqr2lo; + ul32 frqr2hi; + ul32 frqr3lo; // 0x1210 + ul32 frqr3hi; + u8 setunk26[8]; +} setstuf; + +#seekto 0x1260; +struct { + u8 modnum[7]; +} modcode; + +#seekto 0x1300; +struct { + char mod_num[9]; +} mod_id; +""" + +MEM_SIZE = 0x1300 +BLOCK_SIZE = 0x10 # can read 0x20, but must write 0x10 +STIMEOUT = 2 +BAUDRATE = 4800 +# Channel power: 2 levels +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("High", watts=10.00)] + +LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"] +LIST_COLOR = ["Off", "Orange", "Blue", "Purple"] +LIST_LEDSW = ["Auto", "On"] +LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 390, 30)] +LIST_VFOMODE = ["Frequency Mode", "Channel Mode"] +# Tones are numeric, Defined in \chirp\chirp_common.py +TONES_CTCSS = sorted(chirp_common.TONES) +# Converted to strings +LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS] +# Now append the DxxxN and DxxxI DTCS codes from chirp_common +for x in chirp_common.DTCS_CODES: + LIST_CTCSS.append("D{:03d}N".format(x)) +for x in chirp_common.DTCS_CODES: + LIST_CTCSS.append("D{:03d}R".format(x)) +LIST_BW = ["Narrow", "Wide"] +LIST_SHIFT = ["off", "+", "-"] +STEPS = [0.5, 2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 37.5, 50.0, 100.0] +LIST_STEPS = [str(x) for x in STEPS] +LIST_VOXDLY = ["0.5", "1.0", "2.0", "3.0"] # LISTS must be strings +LIST_PTT = ["Both", "EoT", "BoT", "Off"] + +SETTING_LISTS = {"tot": LIST_TIMEOUT, "wtled": LIST_COLOR, + "rxled": LIST_COLOR, "txled": LIST_COLOR, + "ledsw": LIST_LEDSW, "frq_chn_mode": LIST_VFOMODE, + "rx_tone": LIST_CTCSS, "tx_tone": LIST_CTCSS, + "rx_mode": LIST_RECVMODE, "fm_bw": LIST_BW, + "shift": LIST_SHIFT, "step": LIST_STEPS, + "vox_dly": LIST_VOXDLY, "ptt": LIST_PTT} + + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + LOG.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except Exception: + _exit_program_mode(radio) + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + _exit_program_mode(radio) + msg = "Error reading from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except Exception: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">shB", cmd, addr, length) + # Add the data if set + if len(data) != 0: + frame += data + # Return the data + return frame + + +def _recv(radio, addr, length): + """Get data from the radio """ + + data = _rawrecv(radio, length) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(data)) + + return data + + +def _do_ident(radio): + """Put the radio in PROGRAM mode & identify it""" + radio.pipe.baudrate = BAUDRATE + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # Flush input buffer + _clean_buffer(radio) + + magic = "PROGRAMa" + _rawsend(radio, magic) + ack = _rawrecv(radio, 1) + # LOG.warning("PROGa Ack:" + util.hexprint(ack)) + if ack != "\x06": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond") + magic = "PROGRAMb" + _rawsend(radio, magic) + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond to B") + magic = chr(0x02) + _rawsend(radio, magic) + ack = _rawrecv(radio, 1) # s/b: 0x50 + magic = _rawrecv(radio, 7) # s/b TC88... + magic = "MTC88CUMHS3E7BN-" + _rawsend(radio, magic) + ack = _rawrecv(radio, 1) # s/b 0x80 + magic = chr(0x06) + _rawsend(radio, magic) + ack = _rawrecv(radio, 1) + + return True + + +def _exit_program_mode(radio): + endframe = "E" + _rawsend(radio, endframe) + + +def _download(radio): + """Get the memory map""" + + # Put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + frame = _make_frame("R", addr, BLOCK_SIZE) + # DEBUG + LOG.info("Request sent:") + LOG.debug("Frame=" + util.hexprint(frame)) + + # Sending the read request + _rawsend(radio, frame) + dx = _rawrecv(radio, 4) + + # Now we read data + d = _recv(radio, addr, BLOCK_SIZE) + # LOG.warning("Data= " + util.hexprint(d)) + + # Aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + return data + + +def _upload(radio): + """Upload procedure""" + # Put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # The fun starts here + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # Sending the data + data = radio.get_mmap()[addr:addr + BLOCK_SIZE] + + frame = _make_frame("W", addr, BLOCK_SIZE, data) + # LOG.warning("Frame:%s:" % util.hexprint(frame)) + _rawsend(radio, frame) + + # Receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + +def set_tone(_mem, txrx, ctdt, tval, pol): + """Set rxtone[] or txtone[] word values as decimal bytes""" + # txrx: Boolean T= set Rx tones, F= set Tx tones + # ctdt: Boolean T = CTCSS, F= DTCS + # tval = integer tone freq (*10) or DTCS code + # pol = string for DTCS polarity "R" or "N" + xv = int(str(tval), 16) + if txrx: # True = set rxtones + _mem.rxtone[0] = xv & 0xFF # Low byte + _mem.rxtone[1] = (xv >> 8) # Hi byte + if not ctdt: # dtcs, + if pol == "R": + _mem.rxtone[1] = _mem.rxtone[1] | 0xC0 + else: + _mem.rxtone[1] = _mem.rxtone[1] | 0x80 + else: # txtones + _mem.txtone[0] = xv & 0xFF # Low byte + _mem.txtone[1] = (xv >> 8) + if not ctdt: # dtcs + if pol == "R": + _mem.txtone[1] = _mem.txtone[1] | 0xC0 + else: + _mem.txtone[1] = _mem.txtone[1] | 0x80 + + return 0 + + +def _do_map(chn, sclr, mary): + """Set or Clear the chn (1-128) bit in mary[] word array map""" + # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state + # mary[] is u8 array, but the map is by nibbles + ndx = int(math.floor((chn - 1) / 8)) + bv = (chn - 1) % 8 + msk = 1 << bv + mapbit = sclr + if sclr == 1: # Set the bit + mary[ndx] = mary[ndx] | msk + elif sclr == 0: # clear + mary[ndx] = mary[ndx] & (~ msk) # ~ is complement + else: # return current bit state + mapbit = 0 + if (mary[ndx] & msk) > 0: + mapbit = 1 + return mapbit + + +@directory.register +class THUV8000Radio(chirp_common.CloneModeRadio): + """TYT UV8000D Radio""" + VENDOR = "TYT" + MODEL = "TH-UV8000" + MODES = ["NFM", "FM"] + TONES = chirp_common.TONES + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + NAME_LENGTH = 7 + DTMF_CHARS = list("0123456789ABCD*#") + # NOTE: SE Model supports 220-260 MHz + # The following bands are the the range the radio is capable of, + # not the legal FCC amateur bands + VALID_BANDS = [(87500000, 107900000), (136000000, 174000000), + (220000000, 260000000), (400000000, 520000000)] + + # Valid chars on the LCD + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`!\"#$%&'()*+,-./:;<=>?@[]^_" + + # Special Channels Declaration + # WARNING Indecis are hard wired in get/set_memory code !!! + # Channels print in + increasing index order (most negative first) + SPECIAL_MEMORIES = { + "UpVFO": -2, + "LoVFO": -1 + } + FIRST_FREQ_INDEX = -1 + LAST_FREQ_INDEX = -2 + + SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(), + SPECIAL_MEMORIES.keys())) + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = \ + ('Click on the "Special Channels" toggle-button of the memory ' + 'editor to see/set the upper and lower frequency-mode values.\n') + + rp.pre_download = _(dedent("""\ + Follow these instructions to download the radio memory: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio, volume @ 50% + 4 - Radio > Download from radio + """)) + rp.pre_upload = _(dedent("""\ + Follow these instructions to upload the radio memory: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio, volume @ 50% + 4 - Radio > Upload to radio + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + # .has. attributes are boolean, .valid. are lists + rf.has_settings = True + rf.has_bank = False + rf.has_comment = False + rf.has_nostep_tuning = True # Radio accepts any entered freq + rf.has_tuning_step = False # Not as chan feature + rf.can_odd_split = False + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.has_sub_devices = False + rf.valid_name_length = self.NAME_LENGTH + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_duplexes = ["-", "+", "off", ""] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS", + "Tone->DTCS", "DTCS->Tone", "->Tone", + "DTCS->DTCS"] + rf.valid_skips = [] + rf.valid_power_levels = POWER_LEVELS + rf.valid_dtcs_codes = self.DTCS_CODES + rf.valid_bands = self.VALID_BANDS + rf.memory_bounds = (1, 128) + rf.valid_skips = ["", "S"] + rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys()) + return rf + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + + try: + _upload(self) + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def get_memory(self, number): + if isinstance(number, str): + return self._get_special(number) + elif number < 0: + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special(self.SPECIAL_MEMORIES_REV[number]) + else: + return self._get_normal(number) + + def set_memory(self, memory): + """A value in a UI column for chan 'number' has been modified.""" + # update all raw channel memory values (_mem) from UI (mem) + if memory.number < 0: + return self._set_special(memory) + else: + return self._set_normal(memory) + + def _get_normal(self, number): + # radio first channel is 1, mem map is base 0 + _mem = self._memobj.chan_mem[number - 1] + mem = chirp_common.Memory() + mem.number = number + + return self._get_memory(mem, _mem) + + def _get_memory(self, mem, _mem): + """Convert raw channel memory data into UI columns""" + mem.extra = RadioSettingGroup("extra", "Extra") + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.empty = False + # This function process both 'normal' and Freq up/down' entries + mem.freq = int(_mem.rxfreq) * 10 + mem.power = POWER_LEVELS[_mem.power] + mem.mode = self.MODES[_mem.wide] + dtcs_pol = ["N", "N"] + + if _mem.rxtone[0] == 0xFF: + rxmode = "" + elif _mem.rxtone[1] < 0x26: + # CTCSS + rxmode = "Tone" + tonehi = int(str(_mem.rxtone[1])[2:]) + tonelo = int(str(_mem.rxtone[0])[2:]) + mem.ctone = int(tonehi * 100 + tonelo) / 10.0 + else: + # Digital + rxmode = "DTCS" + tonehi = int(str(_mem.rxtone[1] & 0x3f)) + tonelo = int(str(_mem.rxtone[0])[2:]) + mem.rx_dtcs = int(tonehi * 100 + tonelo) + if (_mem.rxtone[1] & 0x40) != 0: + dtcs_pol[1] = "R" + + if _mem.txtone[0] == 0xFF: + txmode = "" + elif _mem.txtone[1] < 0x26: + # CTCSS + txmode = "Tone" + tonehi = int(str(_mem.txtone[1])[2:]) + tonelo = int(str(_mem.txtone[0])[2:]) + mem.rtone = int(tonehi * 100 + tonelo) / 10.0 + else: + # Digital + txmode = "DTCS" + tonehi = int(str(_mem.txtone[1] & 0x3f)) + tonelo = int(str(_mem.txtone[0])[2:]) + mem.dtcs = int(tonehi * 100 + tonelo) + if (_mem.txtone[1] & 0x40) != 0: + dtcs_pol[0] = "R" + + mem.tmode = "" + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + # Now test the mem.number to process special vs normal + if mem.number >= 0: # Normal + mem.name = "" + for i in range(self.NAME_LENGTH): # 0 - 6 + mem.name += chr(_mem.name[i] + 32) + mem.name = mem.name.rstrip() # remove trailing spaces + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \ + and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + if _do_map(mem.number, 2, self._memobj.skpchns.map) > 0: + mem.skip = "S" + else: + mem.skip = "" + + else: # specials VFO + mem.name = "----" + mem.duplex = LIST_SHIFT[_mem.duplx] + mem.offset = int(_mem.ofst) * 10 + mem.skip = "" + # End if specials + + # Channel Extra settings: Only Boolean & List methods, no call-backs + rx = RadioSettingValueBoolean(bool(not _mem.bcl)) # Inverted bool + # NOTE: first param of RadioSetting is the object attribute name + rset = RadioSetting("bcl", "Busy Channel Lockout", rx) + mem.extra.append(rset) + + rx = RadioSettingValueBoolean(bool(not _mem.vox_on)) + rset = RadioSetting("vox_on", "Vox", rx) + mem.extra.append(rset) + + rx = RadioSettingValueBoolean(bool(not _mem.ani)) + rset = RadioSetting("ani", "Auto Number ID (ANI)", rx) + mem.extra.append(rset) + + # ID code can't be done in extra - no Integer method or call-back + + rx = RadioSettingValueList(LIST_PTT, LIST_PTT[_mem.ptt]) + rset = RadioSetting("ptt", "Xmit PTT ID", rx) + mem.extra.append(rset) + + rx = RadioSettingValueBoolean(bool(_mem.epilogue)) + rset = RadioSetting("epilogue", "Epilogue/Tail", rx) + mem.extra.append(rset) + + return mem + + def _get_special(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_MEMORIES[number] + mem.extd_number = number + # Unused attributes are ignored in Set_memory + if (mem.number == -1) or (mem.number == -2): + # Print Upper[1] first, and Lower[0] next + rx = 0 + if mem.number == -2: + rx = 1 + _mem = self._memobj.frq[rx] + # immutable = ["number", "extd_number", "name"] + mem = self._get_memory(mem, _mem) + else: + raise Exception("Sorry, you can't edit that special" + " memory channel %i." % mem.number) + + # mem.immutable = immutable + + return mem + + def _set_memory(self, mem, _mem): + """Convert UI column data (mem) into MEM_FORMAT memory (_mem).""" + # At this point mem points to either normal or Freq chans + # These first attributes are common to all types + if mem.empty: + if mem.number > 0: + _mem.rxfreq = 0xffffffff + # Set 'empty' and 'skip' bits + _do_map(mem.number, 1, self._memobj.chnmap.map) + _do_map(mem.number, 1, self._memobj.skpchns.map) + elif mem.number == -2: # upper VFO Freq + _mem.rxfreq = 14652000 # VHF National Calling freq + elif mem.number == -1: # lower VFO + _mem.rxfreq = 44600000 # UHF National Calling freq + return + + _mem.rxfreq = mem.freq / 10 + + if str(mem.power) == "Low": + _mem.power = 0 + else: + _mem.power = 1 + + _mem.wide = self.MODES.index(mem.mode) + + rxmode = "" + txmode = "" + + if mem.tmode == "Tone": + txmode = "Tone" + elif mem.tmode == "TSQL": + rxmode = "Tone" + txmode = "TSQL" + elif mem.tmode == "DTCS": + rxmode = "DTCSSQL" + txmode = "DTCS" + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + + sx = mem.dtcs_polarity[1] + if rxmode == "": + _mem.rxtone[0] = 0xFF + _mem.rxtone[1] = 0xFF + elif rxmode == "Tone": + val = int(mem.ctone * 10) + i = set_tone(_mem, True, True, val, sx) + elif rxmode == "DTCSSQL": + i = set_tone(_mem, True, False, mem.dtcs, sx) + elif rxmode == "DTCS": + i = set_tone(_mem, True, False, mem.rx_dtcs, sx) + + sx = mem.dtcs_polarity[0] + if txmode == "": + _mem.txtone[0] = 0xFF + _mem.txtone[1] = 0xFF + elif txmode == "Tone": + val = int(mem.rtone * 10) + i = set_tone(_mem, False, True, val, sx) + elif txmode == "TSQL": + val = int(mem.ctone * 10) + i = set_tone(_mem, False, True, val, sx) + elif txmode == "DTCS": + i = set_tone(_mem, False, False, mem.dtcs, sx) + + if mem.number > 0: # Normal chans + for i in range(self.NAME_LENGTH): + pq = ord(mem.name.ljust(self.NAME_LENGTH)[i]) - 32 + if pq < 0: + pq = 0 + _mem.name[i] = pq + + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + # Set the channel map bit FALSE = Enabled + _do_map(mem.number, 0, self._memobj.chnmap.map) + # Skip + if mem.skip == "S": + _do_map(mem.number, 1, self._memobj.skpchns.map) + else: + _do_map(mem.number, 0, self._memobj.skpchns.map) + + else: # Freq (VFO) chans + _mem.duplx = 0 + _mem.ofst = 0 + if mem.duplex == "+": + _mem.duplx = 1 + _mem.ofst = mem.offset / 10 + elif mem.duplex == "-": + _mem.duplx = 2 + _mem.ofst = mem.offset / 10 + for i in range(self.NAME_LENGTH): + _mem.name[i] = 0xff + + # All mem.extra << Once the channel is defined + for setting in mem.extra: + # Overide list strings with signed value + if setting.get_name() == "ptt": + sx = str(setting.value) + for i in range(0, 4): + if sx == LIST_PTT[i]: + val = i + setattr(_mem, "ptt", val) + elif setting.get_name() == "epilogue": # not inverted bool + setattr(_mem, setting.get_name(), setting.value) + else: # inverted booleans + setattr(_mem, setting.get_name(), not setting.value) + + def _set_special(self, mem): + + cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[mem.number]) + + if mem.number == -2: # upper frq[1] + _mem = self._memobj.frq[1] + elif mem.number == -1: # lower frq[0] + _mem = self._memobj.frq[0] + else: + raise Exception("Sorry, you can't edit that special memory.") + + self._set_memory(mem, _mem) # Now update the _mem + + def _set_normal(self, mem): + _mem = self._memobj.chan_mem[mem.number - 1] + + self._set_memory(mem, _mem) + + def get_settings(self): + """Translate the MEM_FORMAT structs into setstuf in the UI""" + # Define mem struct write-back shortcuts + _sets = self._memobj.setstuf + _fmx = self._memobj.fmfrqs + + basic = RadioSettingGroup("basic", "Basic Settings") + adv = RadioSettingGroup("adv", "Other Settings") + fmb = RadioSettingGroup("fmb", "FM Broadcast") + scn = RadioSettingGroup("scn", "Scan Settings") + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + frng = RadioSettingGroup("frng", "Frequency Ranges") + group = RadioSettings(basic, adv, scn, fmb, dtmf, frng) + + def my_val_list(setting, obj, atrb): + """Callback:from ValueList with non-sequential, actual values.""" + # This call back also used in get_settings + value = int(str(setting.value)) # Get the integer value + setattr(obj, atrb, value) + return + + def my_adjraw(setting, obj, atrb, fix): + """Callback from Integer add or subtract fix from value.""" + vx = int(str(setting.value)) + value = vx + int(fix) + if value < 0: + value = 0 + setattr(obj, atrb, value) + return + + def my_strnam(setting, obj, atrb, mln): + """Callback from String to build u8 array with -32 offset.""" + # mln is max string length + ary = [] + knt = mln + for j in range(mln - 1, -1, -1): # Strip trailing spaces or nulls + pq = str(setting.value)[j] + if pq == "" or pq == " ": + knt = knt - 1 + else: + break + for j in range(mln): # 0 to mln-1 + pq = str(setting.value).ljust(mln)[j] + if j < knt: + ary.append(ord(pq) - 32) + else: + ary.append(0) + setattr(obj, atrb, ary) + return + + def unpack_str(cary, cknt, mxw): + """Convert u8 nibble array to a string: NOT a callback.""" + # cknt is char count, 2/word; mxw is max WORDS + stx = "" + mty = True + for i in range(mxw): # unpack entire array + nib = (cary[i] & 0xf0) >> 4 # LE, Hi nib first + if nib != 0xf: + mty = False + stx += format(nib, '0X') + nib = cary[i] & 0xf + if nib != 0xf: + mty = False + stx += format(nib, '0X') + stx = stx[:cknt] + if mty: # all ff, empty string + sty = "" + else: + # Convert E to #, F to * + sty = "" + for i in range(cknt): + if stx[i] == "E": + sty += "#" + elif stx[i] == "F": + sty += "*" + else: + sty += stx[i] + + return sty + + def pack_chars(setting, obj, atrstr, atrcnt, mxl): + """Callback to build 0-9,A-D,*# nibble array from string""" + # cknt is generated char count, 2 chars per word + # String will be f padded to mxl + # Chars are stored as hex values + # store cknt-1 in atrcnt, 0xf if empty + cknt = 0 + ary = [] + stx = str(setting.value).upper() + stx = stx.strip() # trim spaces + # Remove illegal characters first + sty = "" + for j in range(0, len(stx)): + if stx[j] in self.DTMF_CHARS: + sty += stx[j] + for j in range(mxl): + if j < len(sty): + if sty[j] == "#": + chrv = 0xE + elif sty[j] == "*": + chrv = 0xF + else: + chrv = int(sty[j], 16) + cknt += 1 # char count + else: # pad to mxl, cknt does not increment + chrv = 0xF + if (j % 2) == 0: # odd count (0-based), high nibble + hi_nib = chrv + else: # even count, lower nibble + lo_nib = chrv + nibs = lo_nib | (hi_nib << 4) + ary.append(nibs) # append word + setattr(obj, atrstr, ary) + if setting.get_name() != "setstuf.stuncode": # cknt is actual + if cknt > 0: + cknt = cknt - 1 + else: + cknt = 0xf + setattr(obj, atrcnt, cknt) + return + + def myset_freq(setting, obj, atrb, mult): + """ Callback to set frequency by applying multiplier""" + value = int(float(str(setting.value)) * mult) + setattr(obj, atrb, value) + return + + def my_invbool(setting, obj, atrb): + """Callback to invert the boolean """ + bval = not setting.value + setattr(obj, atrb, bval) + return + + def my_batsav(setting, obj, atrb): + """Callback to set batsav attribute """ + stx = str(setting.value) # Off, 1:1... + if stx == "Off": + value = 0x1 # bit value 4 clear, ratio 1 = 1:2 + elif stx == "1:1": + value = 0x4 # On, ratio 0 = 1:1 + elif stx == "1:2": + value = 0x5 # On, ratio 1 = 1:2 + elif stx == "1:3": + value = 0x6 # On, ratio 2 = 1:3 + else: + value = 0x7 # On, ratio 3 = 1:4 + # LOG.warning("Batsav stx:%s:, value= %x" % (stx, value)) + setattr(obj, atrb, value) + return + + def my_manfrq(setting, obj, atrb): + """Callback to set 2-byte manfrqyn yes/no """ + # LOG.warning("Manfrq value = %d" % setting.value) + if (str(setting.value)) == "No": + value = 0xff + else: + value = 0xaa + setattr(obj, atrb, value) + return + + def myset_mask(setting, obj, atrb, nx): + if bool(setting.value): # Enabled = 0 + vx = 0 + else: + vx = 1 + _do_map(nx + 1, vx, self._memobj.fmmap.fmset) + return + + def myset_fmfrq(setting, obj, atrb, nx): + """ Callback to set xx.x FM freq in memory as xx.x * 40""" + # in-valid even KHz freqs are allowed; to satisfy run_tests + vx = float(str(setting.value)) + vx = int(vx * 40) + setattr(obj[nx], atrb, vx) + return + + rx = RadioSettingValueInteger(1, 9, _sets.voxgain + 1) + rset = RadioSetting("setstuf.voxgain", "Vox Level", rx) + rset.set_apply_callback(my_adjraw, _sets, "voxgain", -1) + basic.append(rset) + + rx = RadioSettingValueList(LIST_VOXDLY, LIST_VOXDLY[_sets.voxdelay]) + rset = RadioSetting("setstuf.voxdelay", "Vox Delay (secs)", rx) + basic.append(rset) + + rx = RadioSettingValueInteger(0, 9, _sets.sql) + rset = RadioSetting("setstuf.sql", "Squelch", rx) + basic.append(rset) + + rx = RadioSettingValueList(LIST_STEPS, LIST_STEPS[_sets.freqstep]) + rset = RadioSetting("setstuf.freqstep", "VFO Tune Step (KHz))", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.dbw)) # true logic + rset = RadioSetting("setstuf.dbw", "Dual Band Watch (D.WAIT)", rx) + basic.append(rset) + + options = ["Off", "On", "Auto"] + rx = RadioSettingValueList(options, options[_sets.lampon]) + rset = RadioSetting("setstuf.lampon", "Backlight (LED)", rx) + basic.append(rset) + + options = ["Orange", "Purple", "Blue"] + rx = RadioSettingValueList(options, options[_sets.ledclr]) + rset = RadioSetting("setstuf.ledclr", "Backlight Color (LIGHT)", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.beepon)) + rset = RadioSetting("setstuf.beepon", "Keypad Beep", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.xbandenable)) + rset = RadioSetting("setstuf.xbandenable", "Cross Band Allowed", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(not _sets.xbandon)) + rset = RadioSetting("setstuf.xbandon", "Cross Band On", rx) + rset.set_apply_callback(my_invbool, _sets, "xbandon") + basic.append(rset) + + rx = RadioSettingValueList(LIST_TIMEOUT, LIST_TIMEOUT[_sets.tot]) + rset = RadioSetting("setstuf.tot", "TX Timeout (Secs)", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(not _sets.rgrbeep)) # Invert + rset = RadioSetting("setstuf.rgrbeep", "Beep at Eot (Roger)", rx) + rset.set_apply_callback(my_invbool, _sets, "rgrbeep") + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(not _sets.keylok)) + rset = RadioSetting("setstuf.keylok", "Keypad AutoLock", rx) + rset.set_apply_callback(my_invbool, _sets, "keylok") + basic.append(rset) + + options = ["None", "Message", "DC Volts"] + rx = RadioSettingValueList(options, options[_sets.openmsg]) + rset = RadioSetting("setstuf.openmsg", "Power-On Display", rx) + basic.append(rset) + + options = ["Channel Name", "Frequency"] + rx = RadioSettingValueList(options, options[_sets.chs_name]) + rset = RadioSetting("setstuf.chs_name", "Display Name/Frq", rx) + basic.append(rset) + + sx = "" + for i in range(7): + if _sets.radioname[i] != 0: + sx += chr(_sets.radioname[i] + 32) + rx = RadioSettingValueString(0, 7, sx) + rset = RadioSetting("setstuf.radioname", "Power-On Message", rx) + rset.set_apply_callback(my_strnam, _sets, "radioname", 7) + basic.append(rset) + + # Advanced (Strange) Settings + options = ["Busy: Last Tx Band", "Edit: Current Band"] + rx = RadioSettingValueList(options, options[_sets.txsel]) + rset = RadioSetting("setstuf.txsel", "Transmit Priority", rx) + rset.set_doc("'Busy' transmits on last band used, not current one.") + adv.append(rset) + + options = ["Off", "English", "Unk", "Chinese"] + val = _sets.voice + rx = RadioSettingValueList(options, options[val]) + rset = RadioSetting("setstuf.voice", "Voice", rx) + adv.append(rset) + + options = ["Off", "1:1", "1:2", "1:3", "1:4"] + val = (_sets.batsav & 0x3) + 1 # ratio + if (_sets.batsav & 0x4) == 0: # Off + val = 0 + rx = RadioSettingValueList(options, options[val]) + rset = RadioSetting("setstuf.batsav", "Battery Saver", rx) + rset.set_apply_callback(my_batsav, _sets, "batsav") + adv.append(rset) + + # Find out what & where SuperSave is + options = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + rx = RadioSettingValueList(options, options[_sets.supersave]) + rset = RadioSetting("setstuf.supersave", "Super Save (Secs)", rx) + rset.set_doc("Unknown radio attribute??") + adv.append(rset) + + sx = unpack_str(_sets.pttbot, _sets.pttbcnt + 1, 8) + rx = RadioSettingValueString(0, 16, sx) + rset = RadioSetting("setstuf.pttbot", "PTT BoT Code", rx) + rset.set_apply_callback(pack_chars, _sets, "pttbot", "pttbcnt", 16) + adv.append(rset) + + sx = unpack_str(_sets.ptteot, _sets.pttecnt + 1, 8) + rx = RadioSettingValueString(0, 16, sx) + rset = RadioSetting("setstuf.ptteot", "PTT EoT Code", rx) + rset.set_apply_callback(pack_chars, _sets, "ptteot", "pttecnt", 16) + adv.append(rset) + + options = ["None", "Low", "High", "Both"] + rx = RadioSettingValueList(options, options[_sets.voltx]) + rset = RadioSetting("setstuf.voltx", "Transmit Inhibit Voltage", rx) + rset.set_doc("Block Transmit if battery volts are too high or low,") + adv.append(rset) + + val = 0 # No = 0xff + if _sets.manfrqyn == 0xaa: + val = 1 + options = ["No", "Yes"] + rx = RadioSettingValueList(options, options[val]) + rset = RadioSetting("setstuf.manfrqyn", "Manual Frequency", rx) + rset.set_apply_callback(my_manfrq, _sets, "manfrqyn") + adv.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.manualset)) + rset = RadioSetting("setstuf.manualset", "Manual Setting", rx) + adv.append(rset) + + # Scan Settings + options = ["CO: During Rx", "TO: Timed", "SE: Halt"] + rx = RadioSettingValueList(options, options[_sets.scanmode]) + rset = RadioSetting("setstuf.scanmode", + "Scan Mode (Scan Pauses When)", rx) + scn.append(rset) + + options = ["100", "150", "200", "250", + "300", "350", "400", "450"] + rx = RadioSettingValueList(options, options[_sets.scanspeed]) + rset = RadioSetting("setstuf.scanspeed", "Scan Speed (ms)", rx) + scn.append(rset) + + val = _sets.scantmo + 3 + rx = RadioSettingValueInteger(3, 30, val) + rset = RadioSetting("setstuf.scantmo", + "TO Mode Timeout (secs)", rx) + rset.set_apply_callback(my_adjraw, _sets, "scantmo", -3) + scn.append(rset) + + val = _sets.prichan + if val <= 0: + val = 1 + rx = RadioSettingValueInteger(1, 128, val) + rset = RadioSetting("setstuf.prichan", "Priority Channel", rx) + scn.append(rset) + + # FM Broadcast Settings + val = _fmx.fmcur + val = val / 40.0 + if val < 87.5 or val > 107.9: + val = 88.0 + rx = RadioSettingValueFloat(87.5, 107.9, val, 0.1, 1) + rset = RadioSetting("fmfrqs.fmcur", "Manual FM Freq (MHz)", rx) + rset.set_apply_callback(myset_freq, _fmx, "fmcur", 40) + fmb.append(rset) + + options = ["5", "50", "100", "200(USA)"] # 5 is not used + rx = RadioSettingValueList(options, options[_sets.fmstep]) + rset = RadioSetting("setstuf.fmstep", "FM Freq Step (KHz)", rx) + fmb.append(rset) + + # FM Scan Range fmsclo and fmschi are unknown memory locations, + # Not supported at this time + + rx = RadioSettingValueBoolean(bool(_sets.rxinhib)) + rset = RadioSetting("setstuf.rxinhib", + "Rcvr Will Interupt FM (DW)", rx) + fmb.append(rset) + + _fmfrq = self._memobj.fm_stations + _fmap = self._memobj.fmmap + for j in range(0, 25): + val = _fmfrq[j].rxfreq + if val == 0xFFFF: + val = 88.0 + fmset = False + else: + val = (float(int(val)) / 40) + # get fmmap bit value: 0 = enabled + ndx = int(math.floor((j) / 8)) + bv = j % 8 + msk = 1 << bv + vx = _fmap.fmset[ndx] + fmset = not bool(vx & msk) + rx = RadioSettingValueBoolean(fmset) + rset = RadioSetting("fmmap.fmset/%d" % j, + "FM Preset %02d" % (j + 1), rx) + rset.set_apply_callback(myset_mask, _fmap, "fmset", j) + fmb.append(rset) + + rx = RadioSettingValueFloat(87.5, 107.9, val, 0.1, 1) + rset = RadioSetting("fm_stations/%d.rxfreq" % j, + " Preset %02d Freq" % (j + 1), rx) + # This callback uses the array index + rset.set_apply_callback(myset_fmfrq, _fmfrq, "rxfreq", j) + fmb.append(rset) + + # DTMF Settings + options = [str(x) for x in range(4, 16)] + rx = RadioSettingValueList(options, options[_sets.dtmfspd]) + rset = RadioSetting("setstuf.dtmfspd", + "Tx Speed (digits/sec)", rx) + dtmf.append(rset) + + options = [str(x) for x in range(0, 1100, 100)] + rx = RadioSettingValueList(options, options[_sets.dtmfdig1time]) + rset = RadioSetting("setstuf.dtmfdig1time", + "Tx 1st Digit Time (ms)", rx) + dtmf.append(rset) + + options = [str(x) for x in range(100, 1100, 100)] + rx = RadioSettingValueList(options, options[_sets.dtmfdig1dly]) + rset = RadioSetting("setstuf.dtmfdig1dly", + "Tx 1st Digit Delay (ms)", rx) + dtmf.append(rset) + + options = ["0", "100", "500", "1000"] + rx = RadioSettingValueList(options, options[_sets.dtmfspms]) + rset = RadioSetting("setstuf.dtmfspms", + "Tx Star & Pound Time (ms)", rx) + dtmf.append(rset) + + options = ["None"] + [str(x) for x in range(600, 2100, 100)] + rx = RadioSettingValueList(options, options[_sets.codespctim]) + rset = RadioSetting("setstuf.codespctim", + "Tx Code Space Time (ms)", rx) + dtmf.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.codeabcd)) + rset = RadioSetting("setstuf.codeabcd", "Tx Codes A,B,C,D", rx) + dtmf.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.dtmfside)) + rset = RadioSetting("setstuf.dtmfside", "DTMF Side Tone", rx) + dtmf.append(rset) + + options = ["Off", "A", "B", "C", "D"] + rx = RadioSettingValueList(options, options[_sets.grpcode]) + rset = RadioSetting("setstuf.grpcode", "Rx Group Code", rx) + dtmf.append(rset) + + options = ["Off"] + [str(x) for x in range(1, 16)] + rx = RadioSettingValueList(options, options[_sets.autoresettmo]) + rset = RadioSetting("setstuf.autoresettmo", + "Rx Auto Reset Timeout (secs)", rx) + dtmf.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.txdecode)) + rset = RadioSetting("setstuf.txdecode", "Tx Decode", rx) + dtmf.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.idedit)) + rset = RadioSetting("setstuf.idedit", "Allow ANI Code Edit", rx) + dtmf.append(rset) + + options = [str(x) for x in range(500, 1600, 100)] + rx = RadioSettingValueList(options, options[_sets.decodetmo]) + rset = RadioSetting("setstuf.decodetmo", + "Rx Decode Timeout (ms)", rx) + dtmf.append(rset) + + options = ["Tx & Rx Inhibit", "Tx Inhibit"] + rx = RadioSettingValueList(options, options[_sets.stuntype]) + rset = RadioSetting("setstuf.stuntype", "Stun Type", rx) + dtmf.append(rset) + + sx = unpack_str(_sets.stuncode, _sets.stuncnt, 5) + rx = RadioSettingValueString(0, 10, sx) + rset = RadioSetting("setstuf.stuncode", "Stun Code", rx) + rset.set_apply_callback(pack_chars, _sets, + "stuncode", "stuncnt", 10) + dtmf.append(rset) + + # Frequency ranges + rx = RadioSettingValueBoolean(bool(_sets.frqr1)) + rset = RadioSetting("setstuf.frqr1", "Freq Range 1 (UHF)", rx) + rset.set_doc("Enable the UHF frequency bank.") + frng.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.frqr2)) + rset = RadioSetting("setstuf.frqr2", "Freq Range 2 (VHF)", rx) + rset.set_doc("Enable the VHF frequency bank.") + frng.append(rset) + + mod_se = True # UV8000SE has 3rd freq bank + if mod_se: + rx = RadioSettingValueBoolean(bool(_sets.frqr3)) + rset = RadioSetting("setstuf.frqr3", "Freq Range 3 (220Mhz)", rx) + rset.set_doc("Enable the 220MHz frequency bank.") + frng.append(rset) + + frqm = 100000 + val = _sets.frqr1lo / frqm + rx = RadioSettingValueFloat(400.0, 520.0, val, 0.005, 3) + rset = RadioSetting("setstuf.frqr1lo", + "UHF Range Low Limit (MHz)", rx) + rset.set_apply_callback(myset_freq, _sets, "frqr1lo", frqm) + rset.set_doc("Low limit of the UHF frequency bank.") + frng.append(rset) + + val = _sets.frqr1hi / frqm + rx = RadioSettingValueFloat(400.0, 520.0, val, 0.005, 3) + rset = RadioSetting("setstuf.frqr1hi", + "UHF Range High Limit (MHz)", rx) + rset.set_apply_callback(myset_freq, _sets, "frqr1hi", frqm) + rset.set_doc("High limit of the UHF frequency bank.") + frng.append(rset) + + val = _sets.frqr2lo / frqm + rx = RadioSettingValueFloat(136.0, 174.0, val, 0.005, 3) + rset = RadioSetting("setstuf.frqr2lo", + "VHF Range Low Limit (MHz)", rx) + rset.set_apply_callback(myset_freq, _sets, "frqr2lo", frqm) + rset.set_doc("Low limit of the VHF frequency bank.") + frng.append(rset) + + val = _sets.frqr2hi / frqm + rx = RadioSettingValueFloat(136.0, 174.0, val, 0.005, 3) + rset = RadioSetting("setstuf.frqr2hi", + "VHF Range High Limit (MHz)", rx) + rset.set_apply_callback(myset_freq, _sets, "frqr2hi", frqm) + rset.set_doc("High limit of the VHF frequency bank.") + frng.append(rset) + + if mod_se: + val = _sets.frqr3lo / frqm + if val < 220.0: + val = 220.0 + rx = RadioSettingValueFloat(220.0, 260.0, val, 0.005, 3) + rset = RadioSetting("setstuf.frqr3lo", + "1.25m Range Low Limit (MHz)", rx) + rset.set_apply_callback(myset_freq, _sets, "frqr3lo", frqm) + frng.append(rset) + + val = _sets.frqr3hi / frqm + if val < 220.0: + val = 260.0 + rx = RadioSettingValueFloat(220.0, 260.0, val, 0.005, 3) + rset = RadioSetting("setstuf.frqr3hi", + "1.25m Range High Limit (MHz)", rx) + rset.set_apply_callback(myset_freq, _sets, "frqr3hi", 1000) + frng.append(rset) + + return group # END get_settings() + + def set_settings(self, settings): + _settings = self._memobj.setstuf + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/th_uvf8d.py b/chirp/drivers/th_uvf8d.py new file mode 100644 index 0000000..e302784 --- /dev/null +++ b/chirp/drivers/th_uvf8d.py @@ -0,0 +1,639 @@ +# Copyright 2013 Dan Smith +# Eric Allen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""TYT TH-UVF8D radio management module""" + +# TODO: support FM Radio memories +# TODO: support bank B (another 128 memories) +# TODO: [setting] Battery Save +# TODO: [setting] Tail Eliminate +# TODO: [setting] Tail Mode + +import struct +import logging + +from chirp import chirp_common, bitwise, errors, directory, memmap, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + + +def uvf8d_identify(radio): + """Do identify handshake with TYT TH-UVF8D""" + try: + radio.pipe.write("\x02PROGRAM") + ack = radio.pipe.read(2) + if ack != "PG": + raise errors.RadioError("Radio did not ACK first command: %x" % + ord(ack)) + except: + raise errors.RadioError("Unable to communicate with the radio") + + radio.pipe.write("\x02") + ident = radio.pipe.read(32) + radio.pipe.write("A") + r = radio.pipe.read(1) + if r != "A": + raise errors.RadioError("Ack failed") + return ident + + +def tyt_uvf8d_download(radio): + data = uvf8d_identify(radio) + for i in range(0, 0x4000, 0x20): + msg = struct.pack(">cHb", "R", i, 0x20) + radio.pipe.write(msg) + block = radio.pipe.read(0x20 + 4) + if len(block) != (0x20 + 4): + raise errors.RadioError("Radio sent a short block") + radio.pipe.write("A") + ack = radio.pipe.read(1) + if ack != "A": + raise errors.RadioError("Radio NAKed block") + data += block[4:] + + if radio.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = 0x4000 + status.msg = "Cloning from radio" + radio.status_fn(status) + + radio.pipe.write("ENDR") + + return memmap.MemoryMap(data) + + +def tyt_uvf8d_upload(radio): + """Upload to TYT TH-UVF8D""" + data = uvf8d_identify(radio) + + radio.pipe.timeout = 1 + + if data != radio._mmap[:32]: + raise errors.RadioError("Model mis-match: \n%s\n%s" % + (util.hexprint(data), + util.hexprint(radio._mmap[:32]))) + + for i in range(0, 0x4000, 0x20): + addr = i + 0x20 + msg = struct.pack(">cHb", "W", i, 0x20) + msg += radio._mmap[addr:(addr + 0x20)] + + radio.pipe.write(msg) + ack = radio.pipe.read(1) + if ack != "A": + raise errors.RadioError("Radio did not ack block %i" % i) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = 0x4000 + status.msg = "Cloning to radio" + radio.status_fn(status) + + # End of clone? + radio.pipe.write("ENDW") + + # Checksum? + final_data = radio.pipe.read(3) + +# these require working desktop software +# TODO: DTMF features (ID, delay, speed, kill, etc.) + +# TODO: Display Name + + +UVF8D_MEM_FORMAT = """ +struct memory { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + + u8 apro:4, + rpt_md:2, + unknown1:2; + u8 bclo:2, + wideband:1, + ishighpower:1, + unknown21:1, + vox:1, + pttid:2; + u8 unknown3:8; + + u8 unknown4:6, + duplex:2; + + lbcd offset[4]; + + char unknown5[4]; + + char name[7]; + + char unknown6[1]; +}; + +struct fm_broadcast_memory { + lbcd freq[3]; + u8 unknown; +}; + +struct enable_flags { + bit flags[8]; +}; + +#seekto 0x0020; +struct memory channels[128]; + +#seekto 0x2020; +struct memory vfo1; +struct memory vfo2; + +#seekto 0x2060; +struct { + u8 unknown2060:4, + tot:4; + u8 unknown2061; + u8 squelch; + u8 unknown2063:4, + vox_level:4; + u8 tuning_step; + char unknown12; + u8 lamp_t; + char unknown11; + u8 unknown2068; + u8 ani:1, + scan_mode:2, + unknown2069:2, + beep:1, + tx_sel:1, + roger:1; + u8 light:2, + led:2, + unknown206a:1, + autolk:1, + unknown206ax:2; + u8 unknown206b:1, + b_display:2, + a_display:2, + ab_switch:1, + dwait:1, + mode:1; + u8 dw:1, + unknown206c:6, + voice:1; + u8 unknown206d:2, + rxsave:2, + opnmsg:2, + lock_mode:2; + u8 a_work_area:1, + b_work_area:1, + unknown206ex:6; + u8 a_channel; + u8 b_channel; + u8 pad3[15]; + char ponmsg[7]; +} settings; + +#seekto 0x2E60; +struct enable_flags enable[16]; +struct enable_flags skip[16]; + +#seekto 0x2FA0; +struct fm_broadcast_memory fm_current; + +#seekto 0x2FA8; +struct fm_broadcast_memory fm_memories[20]; +""" + +THUVF8D_DUPLEX = ["", "-", "+"] +THUVF8D_CHARSET = "".join([chr(ord("0") + x) for x in range(0, 10)] + + [" -*+"] + + [chr(ord("A") + x) for x in range(0, 26)] + + ["_/"]) +TXSEL_LIST = ["EDIT", "BUSY"] +LED_LIST = ["Off", "Auto", "On"] +MODE_LIST = ["Memory", "VFO"] +AB_LIST = ["A", "B"] +DISPLAY_LIST = ["Channel", "Frequency", "Name"] +LIGHT_LIST = ["Purple", "Orange", "Blue"] +RPTMD_LIST = ["Off", "Reverse", "Talkaround"] +VOX_LIST = ["1", "2", "3", "4", "5", "6", "7", "8"] +WIDEBAND_LIST = ["Narrow", "Wide"] +TOT_LIST = ["Off", "30s", "60s", "90s", "120s", "150s", "180s", "210s", + "240s", "270s"] +SCAN_MODE_LIST = ["Time", "Carry", "Seek"] +OPNMSG_LIST = ["Off", "DC (Battery)", "Message"] + +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5), + chirp_common.PowerLevel("Low", watts=0.5), + ] + +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +BCLO_LIST = ["Off", "Wave", "Call"] +APRO_LIST = ["Off", "Compander", "Scramble 1", "Scramble 2", "Scramble 3", + "Scramble 4", "Scramble 5", "Scramble 6", "Scramble 7", + "Scramble 8"] +LOCK_MODE_LIST = ["PTT", "Key", "Key+S", "All"] + +TUNING_STEPS_LIST = ["2.5", "5.0", "6.25", "10.0", "12.5", + "25.0", "50.0", "100.0"] +BACKLIGHT_TIMEOUT_LIST = ["1s", "2s", "3s", "4s", "5s", + "6s", "7s", "8s", "9s", "10s"] + +SPECIALS = { + "VFO1": -2, + "VFO2": -1} + + +@directory.register +class TYTUVF8DRadio(chirp_common.CloneModeRadio): + VENDOR = "TYT" + MODEL = "TH-UVF8D" + BAUD_RATE = 9600 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 128) + rf.has_bank = False + rf.has_ctone = True + rf.has_tuning_step = False + rf.has_cross = False + rf.has_rx_dtcs = True + rf.has_settings = True + # it may actually be supported, but I haven't tested + rf.can_odd_split = False + rf.valid_duplexes = THUVF8D_DUPLEX + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-" + rf.valid_bands = [(136000000, 174000000), + (400000000, 520000000)] + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_modes = ["FM", "NFM"] + rf.valid_special_chans = SPECIALS.keys() + rf.valid_name_length = 7 + return rf + + def sync_in(self): + self._mmap = tyt_uvf8d_download(self) + self.process_mmap() + + def sync_out(self): + tyt_uvf8d_upload(self) + + @classmethod + def match_model(cls, filedata, filename): + return filedata.startswith("TYT-F10\x00") + + def process_mmap(self): + self._memobj = bitwise.parse(UVF8D_MEM_FORMAT, self._mmap) + + def _decode_tone(self, toneval): + pol = "N" + rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF) + + if toneval[0].get_bits(0xFF) == 0xFF: + mode = "" + val = 0 + elif toneval[1].get_bits(0xC0) == 0xC0: + mode = "DTCS" + val = int("%x" % (rawval & 0x3FFF)) + pol = "R" + elif toneval[1].get_bits(0x80): + mode = "DTCS" + val = int("%x" % (rawval & 0x3FFF)) + else: + mode = "Tone" + val = int(toneval) / 10.0 + + return mode, val, pol + + def _encode_tone(self, _toneval, mode, val, pol): + toneval = 0 + if mode == "Tone": + toneval = int("%i" % (val * 10), 16) + elif mode == "DTCS": + toneval = int("%i" % val, 16) + toneval |= 0x8000 + if pol == "R": + toneval |= 0x4000 + else: + toneval = 0xFFFF + + _toneval[0].set_raw(toneval & 0xFF) + _toneval[1].set_raw((toneval >> 8) & 0xFF) + + def get_raw_memory(self, number): + return repr(self._memobj.channels[number - 1]) + + def _get_memobjs(self, number): + if isinstance(number, str): + return (getattr(self._memobj, number.lower()), None) + elif number < 0: + for k, v in SPECIALS.items(): + if number == v: + return (getattr(self._memobj, k.lower()), None) + else: + return (self._memobj.channels[number - 1], + None) + + def get_memory(self, number): + _mem, _name = self._get_memobjs(number) + + mem = chirp_common.Memory() + + if isinstance(number, str): + mem.number = SPECIALS[number] + mem.extd_number = number + else: + mem.number = number + + if _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"): + mem.empty = True + return mem + + if isinstance(number, int): + e = self._memobj.enable[(number - 1) / 8] + enabled = e.flags[7 - ((number - 1) % 8)] + s = self._memobj.skip[(number - 1) / 8] + dont_skip = s.flags[7 - ((number - 1) % 8)] + else: + enabled = True + dont_skip = True + + if not enabled: + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + mem.duplex = THUVF8D_DUPLEX[_mem.duplex] + mem.offset = int(_mem.offset) * 10 + + txmode, txval, txpol = self._decode_tone(_mem.tx_tone) + rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone) + + chirp_common.split_tone_decode(mem, + (txmode, txval, txpol), + (rxmode, rxval, rxpol)) + + mem.name = str(_mem.name).rstrip('\xFF ') + + if dont_skip: + mem.skip = '' + else: + mem.skip = 'S' + + mem.mode = _mem.wideband and "FM" or "NFM" + mem.power = POWER_LEVELS[1 - _mem.ishighpower] + + mem.extra = RadioSettingGroup("extra", "Extra Settings") + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("vox", "VOX", + RadioSettingValueBoolean(_mem.vox)) + mem.extra.append(rs) + + rs = RadioSetting("bclo", "Busy Channel Lockout", + RadioSettingValueList(BCLO_LIST, + BCLO_LIST[_mem.bclo])) + mem.extra.append(rs) + + rs = RadioSetting("apro", "APRO", + RadioSettingValueList(APRO_LIST, + APRO_LIST[_mem.apro])) + mem.extra.append(rs) + + rs = RadioSetting("rpt_md", "Repeater Mode", + RadioSettingValueList(RPTMD_LIST, + RPTMD_LIST[_mem.rpt_md])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem, _name = self._get_memobjs(mem.number) + + e = self._memobj.enable[(mem.number - 1) / 8] + s = self._memobj.skip[(mem.number - 1) / 8] + if mem.empty: + _mem.set_raw("\xFF" * 32) + e.flags[7 - ((mem.number - 1) % 8)] = False + s.flags[7 - ((mem.number - 1) % 8)] = False + return + else: + e.flags[7 - ((mem.number - 1) % 8)] = True + + if _mem.get_raw() == ("\xFF" * 32): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 32) + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + _mem.duplex = THUVF8D_DUPLEX.index(mem.duplex) + _mem.offset = mem.offset / 10 + + (txmode, txval, txpol), (rxmode, rxval, rxpol) = \ + chirp_common.split_tone_encode(mem) + + self._encode_tone(_mem.tx_tone, txmode, txval, txpol) + self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol) + + _mem.name = mem.name.rstrip(' ').ljust(7, "\xFF") + + flag_index = 7 - ((mem.number - 1) % 8) + s.flags[flag_index] = (mem.skip == "") + _mem.wideband = mem.mode == "FM" + _mem.ishighpower = mem.power == POWER_LEVELS[0] + + for element in mem.extra: + setattr(_mem, element.get_name(), element.value) + + def get_settings(self): + _settings = self._memobj.settings + + group = RadioSettingGroup("basic", "Basic") + top = RadioSettings(group) + + group.append(RadioSetting( + "mode", "Mode", + RadioSettingValueList( + MODE_LIST, MODE_LIST[_settings.mode]))) + + group.append(RadioSetting( + "ab_switch", "A/B", + RadioSettingValueList( + AB_LIST, AB_LIST[_settings.ab_switch]))) + + group.append(RadioSetting( + "a_channel", "A Selected Memory", + RadioSettingValueInteger(1, 128, _settings.a_channel + 1))) + + group.append(RadioSetting( + "b_channel", "B Selected Memory", + RadioSettingValueInteger(1, 128, _settings.b_channel + 1))) + + group.append(RadioSetting( + "a_display", "A Channel Display", + RadioSettingValueList( + DISPLAY_LIST, DISPLAY_LIST[_settings.a_display]))) + group.append(RadioSetting( + "b_display", "B Channel Display", + RadioSettingValueList( + DISPLAY_LIST, DISPLAY_LIST[_settings.b_display]))) + group.append(RadioSetting( + "tx_sel", "Priority Transmit", + RadioSettingValueList( + TXSEL_LIST, TXSEL_LIST[_settings.tx_sel]))) + group.append(RadioSetting( + "vox_level", "VOX Level", + RadioSettingValueList( + VOX_LIST, VOX_LIST[_settings.vox_level]))) + + group.append(RadioSetting( + "squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch))) + + group.append(RadioSetting( + "dwait", "Dual Wait", + RadioSettingValueBoolean(_settings.dwait))) + + group.append(RadioSetting( + "led", "LED Mode", + RadioSettingValueList(LED_LIST, LED_LIST[_settings.led]))) + + group.append(RadioSetting( + "light", "Light Color", + RadioSettingValueList( + LIGHT_LIST, LIGHT_LIST[_settings.light]))) + + group.append(RadioSetting( + "beep", "Beep", + RadioSettingValueBoolean(_settings.beep))) + + group.append(RadioSetting( + "ani", "ANI", + RadioSettingValueBoolean(_settings.ani))) + + group.append(RadioSetting( + "tot", "Timeout Timer", + RadioSettingValueList(TOT_LIST, TOT_LIST[_settings.tot]))) + + group.append(RadioSetting( + "roger", "Roger Beep", + RadioSettingValueBoolean(_settings.roger))) + + group.append(RadioSetting( + "dw", "Dual Watch", + RadioSettingValueBoolean(_settings.dw))) + + group.append(RadioSetting( + "rxsave", "RX Save", + RadioSettingValueBoolean(_settings.rxsave))) + + def _filter(name): + return str(name).rstrip("\xFF").rstrip() + + group.append(RadioSetting( + "ponmsg", "Power-On Message", + RadioSettingValueString(0, 7, _filter(_settings.ponmsg)))) + + group.append(RadioSetting( + "scan_mode", "Scan Mode", + RadioSettingValueList( + SCAN_MODE_LIST, SCAN_MODE_LIST[_settings.scan_mode]))) + + group.append(RadioSetting( + "autolk", "Auto Lock", + RadioSettingValueBoolean(_settings.autolk))) + + group.append(RadioSetting( + "lock_mode", "Keypad Lock Mode", + RadioSettingValueList( + LOCK_MODE_LIST, LOCK_MODE_LIST[_settings.lock_mode]))) + + group.append(RadioSetting( + "voice", "Voice Prompt", + RadioSettingValueBoolean(_settings.voice))) + + group.append(RadioSetting( + "opnmsg", "Opening Message", + RadioSettingValueList( + OPNMSG_LIST, OPNMSG_LIST[_settings.opnmsg]))) + + group.append(RadioSetting( + "tuning_step", "Tuning Step", + RadioSettingValueList( + TUNING_STEPS_LIST, + TUNING_STEPS_LIST[_settings.tuning_step]))) + + group.append(RadioSetting( + "lamp_t", "Backlight Timeout", + RadioSettingValueList( + BACKLIGHT_TIMEOUT_LIST, + BACKLIGHT_TIMEOUT_LIST[_settings.lamp_t]))) + + group.append(RadioSetting( + "a_work_area", "A Work Area", + RadioSettingValueList( + AB_LIST, AB_LIST[_settings.a_work_area]))) + + group.append(RadioSetting( + "b_work_area", "B Work Area", + RadioSettingValueList( + AB_LIST, AB_LIST[_settings.b_work_area]))) + + return top + + group.append(RadioSetting( + "disnm", "Display Name", + RadioSettingValueBoolean(_settings.disnm))) + + return group + + def set_settings(self, settings): + _settings = self._memobj.settings + + for element in settings: + if element.get_name() == 'rxsave': + if bool(element.value.get_value()): + _settings.rxsave = 3 + else: + _settings.rxsave = 0 + continue + if element.get_name().endswith('_channel'): + LOG.debug(element.value, type(element.value)) + setattr(_settings, element.get_name(), int(element.value) - 1) + continue + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + setattr(_settings, element.get_name(), element.value) diff --git a/chirp/drivers/thd72.py b/chirp/drivers/thd72.py new file mode 100644 index 0000000..59984e8 --- /dev/null +++ b/chirp/drivers/thd72.py @@ -0,0 +1,796 @@ +# Copyright 2010 Vernon Mauery +# Copyright 2016 Angus Ainslie +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, errors, util, directory +from chirp import bitwise, memmap +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings +from chirp.settings import RadioSettingValueInteger, RadioSettingValueString +from chirp.settings import RadioSettingValueList, RadioSettingValueBoolean +import time +import struct +import sys +import logging + +LOG = logging.getLogger(__name__) + +# TH-D72 memory map +# 0x0000..0x0200: startup password and other stuff +# 0x0200..0x0400: current channel and other settings +# 0x244,0x246: last menu numbers +# 0x249: last f menu number +# 0x0400..0x0c00: APRS settings and likely other settings +# 0x0c00..0x1500: memory channel flags +# 0x1500..0x5380: 0-999 channels +# 0x5380..0x54c0: 0-9 scan channels +# 0x54c0..0x5560: 0-9 wx channels +# 0x5560..0x5e00: ? +# 0x5e00..0x7d40: 0-999 channel names +# 0x7d40..0x7de0: ? +# 0x7de0..0x7e30: wx channel names +# 0x7e30..0x7ed0: ? +# 0x7ed0..0x7f20: group names +# 0x7f20..0x8b00: ? +# 0x8b00..0x9c00: last 20 APRS entries +# 0x9c00..0xe500: ? +# 0xe500..0xe7d0: startup bitmap +# 0xe7d0..0xe800: startup bitmap filename +# 0xe800..0xead0: gps-logger bitmap +# 0xe8d0..0xeb00: gps-logger bipmap filename +# 0xeb00..0xff00: ? +# 0xff00..0xffff: stuff? + +# memory channel +# 0 1 2 3 4 5 6 7 8 9 a b c d e f +# [freq ] ? mode tmode/duplex rtone ctone dtcs cross_mode [offset] ? + +mem_format = """ +#seekto 0x0000; +struct { + ul16 version; + u8 shouldbe32; + u8 efs[11]; + u8 unknown0[3]; + u8 radio_custom_image; + u8 gps_custom_image; + u8 unknown1[7]; + u8 passwd[6]; +} frontmatter; + +#seekto 0x02c0; +struct { + ul32 start_freq; + ul32 end_freq; +} prog_vfo[6]; + +#seekto 0x0300; +struct { + char power_on_msg[8]; + u8 unknown0[8]; + u8 unknown1[2]; + u8 lamp_timer; + u8 contrast; + u8 battery_saver; + u8 APO; + u8 unknown2; + u8 key_beep; + u8 unknown3[8]; + u8 unknown4; + u8 balance; + u8 unknown5[23]; + u8 lamp_control; +} settings; + +#seekto 0x0c00; +struct { + u8 disabled:4, + prog_vfo:4; + u8 skip; +} flag[1032]; + +#seekto 0x1500; +struct { + ul32 freq; + u8 unknown1; + u8 mode; + u8 tone_mode:4, + duplex:4; + u8 rtone; + u8 ctone; + u8 dtcs; + u8 cross_mode; + ul32 offset; + u8 unknown2; +} memory[1032]; + +#seekto 0x5e00; +struct { + char name[8]; +} channel_name[1000]; + +#seekto 0x7de0; +struct { + char name[8]; +} wx_name[10]; + +#seekto 0x7ed0; +struct { + char name[8]; +} group_name[10]; +""" + +THD72_SPECIAL = {} + +for i in range(0, 10): + THD72_SPECIAL["L%i" % i] = 1000 + (i * 2) + THD72_SPECIAL["U%i" % i] = 1000 + (i * 2) + 1 +for i in range(0, 10): + THD72_SPECIAL["WX%i" % (i + 1)] = 1020 + i +THD72_SPECIAL["C VHF"] = 1030 +THD72_SPECIAL["C UHF"] = 1031 + +THD72_SPECIAL_REV = {} +for k, v in THD72_SPECIAL.items(): + THD72_SPECIAL_REV[v] = k + +TMODES = { + 0x08: "Tone", + 0x04: "TSQL", + 0x02: "DTCS", + 0x01: "Cross", + 0x00: "", +} +TMODES_REV = { + "": 0x00, + "Cross": 0x01, + "DTCS": 0x02, + "TSQL": 0x04, + "Tone": 0x08, +} + +MODES = { + 0x00: "FM", + 0x01: "NFM", + 0x02: "AM", +} + +MODES_REV = { + "FM": 0x00, + "NFM": 0x01, + "AM": 0x2, +} + +DUPLEX = { + 0x00: "", + 0x01: "+", + 0x02: "-", + 0x04: "split", +} +DUPLEX_REV = { + "": 0x00, + "+": 0x01, + "-": 0x02, + "split": 0x04, +} + + +EXCH_R = "R\x00\x00\x00\x00" +EXCH_W = "W\x00\x00\x00\x00" + +DEFAULT_PROG_VFO = ( + (136000000, 174000000), + (410000000, 470000000), + (118000000, 136000000), + (136000000, 174000000), + (320000000, 400000000), + (400000000, 524000000), +) +# index of PROG_VFO used for setting memory.unknown1 and memory.unknown2 +# see http://chirp.danplanet.com/issues/1611#note-9 +UNKNOWN_LOOKUP = (0, 7, 4, 0, 4, 7) + + +def get_prog_vfo(frequency): + for i, (start, end) in enumerate(DEFAULT_PROG_VFO): + if start <= frequency < end: + return i + raise ValueError("Frequency is out of range.") + + +@directory.register +class THD72Radio(chirp_common.CloneModeRadio): + + BAUD_RATE = 9600 + VENDOR = "Kenwood" + MODEL = "TH-D72 (clone mode)" + HARDWARE_FLOW = sys.platform == "darwin" # only OS X driver needs hw flow + + mem_upper_limit = 1022 + _memsize = 65536 + _model = "" # FIXME: REMOVE + _dirty_blocks = [] + + _LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)] + _LAMP_CONTROL = ["Manual", "Auto"] + _LAMP_TIMER = ["Seconds %d" % x for x in range(2, 11)] + _BATTERY_SAVER = ["OFF", "0.03 Seconds", "0.2 Seconds", "0.4 Seconds", + "0.6 Seconds", "0.8 Seconds", "1 Seconds", "2 Seconds", + "3 Seconds", "4 Seconds", "5 Seconds"] + _APO = ["OFF", "15 Minutes", "30 Minutes", "60 Minutes"] + _AUDIO_BALANCE = ["Center", "A +50%", "A +100%", "B +50%", "B +100%"] + _KEY_BEEP = ["OFF", "Radio & GPS", "Radio Only", "GPS Only"] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 1031) + rf.valid_bands = [(118000000, 174000000), + (320000000, 524000000)] + rf.has_cross = True + rf.can_odd_split = True + rf.has_dtcs_polarity = False + rf.has_tuning_step = False + rf.has_bank = False + rf.has_settings = True + rf.valid_tuning_steps = [] + rf.valid_modes = MODES_REV.keys() + rf.valid_tmodes = TMODES_REV.keys() + rf.valid_duplexes = DUPLEX_REV.keys() + rf.valid_skips = ["", "S"] + rf.valid_characters = chirp_common.CHARSET_ALPHANUMERIC + rf.valid_name_length = 8 + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + self._dirty_blocks = [] + + def _detect_baud(self): + for baud in [9600, 19200, 38400, 57600]: + self.pipe.baudrate = baud + try: + self.pipe.write("\r\r") + except: + break + self.pipe.read(32) + try: + id = self.get_id() + LOG.info("Radio %s at %i baud" % (id, baud)) + return True + except errors.RadioError: + pass + + raise errors.RadioError("No response from radio") + + def get_special_locations(self): + return sorted(THD72_SPECIAL.keys()) + + def add_dirty_block(self, memobj): + block = memobj._offset / 256 + if block not in self._dirty_blocks: + self._dirty_blocks.append(block) + self._dirty_blocks.sort() + print("dirty blocks: ", self._dirty_blocks) + + def get_channel_name(self, number): + if number < 999: + name = str(self._memobj.channel_name[number].name) + '\xff' + elif number >= 1020 and number < 1030: + number -= 1020 + name = str(self._memobj.wx_name[number].name) + '\xff' + else: + return '' + return name[:name.index('\xff')].rstrip() + + def set_channel_name(self, number, name): + name = name[:8] + '\xff' * 8 + if number < 999: + self._memobj.channel_name[number].name = name[:8] + self.add_dirty_block(self._memobj.channel_name[number]) + elif number >= 1020 and number < 1030: + number -= 1020 + self._memobj.wx_name[number].name = name[:8] + self.add_dirty_block(self._memobj.wx_name[number]) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + \ + repr(self._memobj.flag[number]) + + def get_memory(self, number): + if isinstance(number, str): + try: + number = THD72_SPECIAL[number] + except KeyError: + raise errors.InvalidMemoryLocation("Unknown channel %s" % + number) + + if number < 0 or number > (max(THD72_SPECIAL.values()) + 1): + raise errors.InvalidMemoryLocation( + "Number must be between 0 and 999") + + _mem = self._memobj.memory[number] + flag = self._memobj.flag[number] + + mem = chirp_common.Memory() + mem.number = number + + if number > 999: + mem.extd_number = THD72_SPECIAL_REV[number] + if flag.disabled == 0xf: + mem.empty = True + return mem + + mem.name = self.get_channel_name(number) + mem.freq = int(_mem.freq) + mem.tmode = TMODES[int(_mem.tone_mode)] + mem.rtone = chirp_common.TONES[_mem.rtone] + mem.ctone = chirp_common.TONES[_mem.ctone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.duplex = DUPLEX[int(_mem.duplex)] + mem.offset = int(_mem.offset) + mem.mode = MODES[int(_mem.mode)] + + if number < 999: + mem.skip = chirp_common.SKIP_VALUES[int(flag.skip)] + mem.cross_mode = chirp_common.CROSS_MODES[_mem.cross_mode] + if number > 999: + mem.cross_mode = chirp_common.CROSS_MODES[0] + mem.immutable = ["number", "bank", "extd_number", "cross_mode"] + if number >= 1020 and number < 1030: + mem.immutable += ["freq", "offset", "tone", "mode", + "tmode", "ctone", "skip"] # FIXME: ALL + else: + mem.immutable += ["name"] + + return mem + + def set_memory(self, mem): + LOG.debug("set_memory(%d)" % mem.number) + if mem.number < 0 or mem.number > (max(THD72_SPECIAL.values()) + 1): + raise errors.InvalidMemoryLocation( + "Number must be between 0 and 999") + + # weather channels can only change name, nothing else + if mem.number >= 1020 and mem.number < 1030: + self.set_channel_name(mem.number, mem.name) + return + + flag = self._memobj.flag[mem.number] + self.add_dirty_block(self._memobj.flag[mem.number]) + + # only delete non-WX channels + was_empty = flag.disabled == 0xf + if mem.empty: + flag.disabled = 0xf + return + flag.disabled = 0 + + _mem = self._memobj.memory[mem.number] + self.add_dirty_block(_mem) + if was_empty: + self.initialize(_mem) + + _mem.freq = mem.freq + + if mem.number < 999: + self.set_channel_name(mem.number, mem.name) + + _mem.tone_mode = TMODES_REV[mem.tmode] + _mem.rtone = chirp_common.TONES.index(mem.rtone) + _mem.ctone = chirp_common.TONES.index(mem.ctone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.cross_mode = chirp_common.CROSS_MODES.index(mem.cross_mode) + _mem.duplex = DUPLEX_REV[mem.duplex] + _mem.offset = mem.offset + _mem.mode = MODES_REV[mem.mode] + + prog_vfo = get_prog_vfo(mem.freq) + flag.prog_vfo = prog_vfo + _mem.unknown1 = _mem.unknown2 = UNKNOWN_LOOKUP[prog_vfo] + + if mem.number < 999: + flag.skip = chirp_common.SKIP_VALUES.index(mem.skip) + + def sync_in(self): + self._detect_baud() + self._mmap = self.download() + self.process_mmap() + + def sync_out(self): + self._detect_baud() + if len(self._dirty_blocks): + self.upload(self._dirty_blocks) + else: + self.upload() + + def read_block(self, block, count=256): + self.pipe.write(struct.pack("D72: %s" % cmd) + self.pipe.write(cmd + "\r") + while not data.endswith("\r") and (time.time() - start) < timeout: + data += self.pipe.read(1) + LOG.debug("D72->PC: %s" % data.strip()) + return data.strip() + + def get_id(self): + r = self.command("ID") + if r.startswith("ID "): + return r.split(" ")[1] + else: + raise errors.RadioError("No response to ID command") + + def initialize(self, mmap): + mmap.set_raw("\x00\xc8\xb3\x08\x00\x01\x00\x08" + "\x08\x00\xc0\x27\x09\x00\x00\x00") + + def _get_settings(self): + top = RadioSettings(self._get_display_settings(), + self._get_audio_settings(), + self._get_battery_settings()) + return top + + def set_settings(self, settings): + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + if element.has_apply_callback(): + LOG.debug("Using apply callback") + try: + element.run_apply_callback() + except NotImplementedError as e: + LOG.error("thd72: %s", e) + continue + + # Find the object containing setting. + obj = _mem + bits = element.get_name().split(".") + setting = bits[-1] + for name in bits[:-1]: + if name.endswith("]"): + name, index = name.split("[") + index = int(index[:-1]) + obj = getattr(obj, name)[index] + else: + obj = getattr(obj, name) + + try: + old_val = getattr(obj, setting) + LOG.debug("Setting %s(%r) <= %s" % ( + element.get_name(), old_val, element.value)) + setattr(obj, setting, element.value) + except AttributeError as e: + LOG.error("Setting %s is not in the memory map: %s" % + (element.get_name(), e)) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + @classmethod + def apply_power_on_msg(cls, setting, obj): + message = setting.value.get_value() + setattr(obj, "power_on_msg", cls._add_ff_pad(message, 8)) + + def apply_lcd_contrast(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._LCD_CONTRAST.index(rawval) + 1 + obj.contrast = val + + def apply_lamp_control(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._LAMP_CONTROL.index(rawval) + obj.lamp_control = val + + def apply_lamp_timer(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._LAMP_TIMER.index(rawval) + 2 + obj.lamp_timer = val + + def _get_display_settings(self): + menu = RadioSettingGroup("display", "Display") + display_settings = self._memobj.settings + + val = RadioSettingValueString( + 0, 8, str(display_settings.power_on_msg).rstrip("\xFF")) + rs = RadioSetting("display.power_on_msg", "Power on message", val) + rs.set_apply_callback(self.apply_power_on_msg, display_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._LCD_CONTRAST, + self._LCD_CONTRAST[display_settings.contrast - 1]) + rs = RadioSetting("display.contrast", "LCD Contrast", + val) + rs.set_apply_callback(self.apply_lcd_contrast, display_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._LAMP_CONTROL, + self._LAMP_CONTROL[display_settings.lamp_control]) + rs = RadioSetting("display.lamp_control", "Lamp Control", + val) + rs.set_apply_callback(self.apply_lamp_control, display_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._LAMP_TIMER, + self._LAMP_TIMER[display_settings.lamp_timer - 2]) + rs = RadioSetting("display.lamp_timer", "Lamp Timer", + val) + rs.set_apply_callback(self.apply_lamp_timer, display_settings) + menu.append(rs) + + return menu + + def apply_battery_saver(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._BATTERY_SAVER.index(rawval) + obj.battery_saver = val + + def apply_APO(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._APO.index(rawval) + obj.APO = val + + def _get_battery_settings(self): + menu = RadioSettingGroup("battery", "Battery") + battery_settings = self._memobj.settings + + val = RadioSettingValueList( + self._BATTERY_SAVER, + self._BATTERY_SAVER[battery_settings.battery_saver]) + rs = RadioSetting("battery.battery_saver", "Battery Saver", + val) + rs.set_apply_callback(self.apply_battery_saver, battery_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._APO, + self._APO[battery_settings.APO]) + rs = RadioSetting("battery.APO", "Auto Power Off", + val) + rs.set_apply_callback(self.apply_APO, battery_settings) + menu.append(rs) + + return menu + + def apply_balance(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._AUDIO_BALANCE.index(rawval) + obj.balance = val + + def apply_key_beep(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._KEY_BEEP.index(rawval) + obj.key_beep = val + + def _get_audio_settings(self): + menu = RadioSettingGroup("audio", "Audio") + audio_settings = self._memobj.settings + + val = RadioSettingValueList( + self._AUDIO_BALANCE, + self._AUDIO_BALANCE[audio_settings.balance]) + rs = RadioSetting("audio.balance", "Balance", + val) + rs.set_apply_callback(self.apply_balance, audio_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._KEY_BEEP, + self._KEY_BEEP[audio_settings.key_beep]) + rs = RadioSetting("audio.key_beep", "Key Beep", + val) + rs.set_apply_callback(self.apply_key_beep, audio_settings) + menu.append(rs) + + return menu + + @staticmethod + def _add_ff_pad(val, length): + return val.ljust(length, "\xFF")[:length] + + @classmethod + def _strip_ff_pads(cls, messages): + result = [] + for msg_text in messages: + result.append(str(msg_text).rstrip("\xFF")) + return result + +if __name__ == "__main__": + import sys + import serial + import detect + import getopt + + def fixopts(opts): + r = {} + for opt in opts: + k, v = opt + r[k] = v + return r + + def usage(): + print "Usage: %s <-i input.img>|<-o output.img> -p port " \ + "[[-f first-addr] [-l last-addr] | [-b list,of,blocks]]" % \ + sys.argv[0] + sys.exit(1) + + opts, args = getopt.getopt(sys.argv[1:], "i:o:p:f:l:b:") + opts = fixopts(opts) + first = last = 0 + blocks = None + if '-i' in opts: + fname = opts['-i'] + download = False + elif '-o' in opts: + fname = opts['-o'] + download = True + else: + usage() + if '-p' in opts: + port = opts['-p'] + else: + usage() + + if '-f' in opts: + first = int(opts['-f'], 0) + if '-l' in opts: + last = int(opts['-l'], 0) + if '-b' in opts: + blocks = [int(b, 0) for b in opts['-b'].split(',')] + blocks.sort() + + ser = serial.Serial(port=port, baudrate=9600, timeout=0.25) + r = THD72Radio(ser) + memmax = r._memsize + if not download: + memmax -= 512 + + if blocks is None: + if first < 0 or first > (r._memsize - 1): + raise errors.RadioError("first address out of range") + if (last > 0 and last < first) or last > memmax: + raise errors.RadioError("last address out of range") + elif last == 0: + last = memmax + first /= 256 + if last % 256 != 0: + last += 256 + last /= 256 + blocks = range(first, last) + + if download: + data = r.download(True, blocks) + file(fname, "wb").write(data) + else: + r._mmap = file(fname, "rb").read(r._memsize) + r.upload(blocks) + print "\nDone" diff --git a/chirp/drivers/thuv1f.py b/chirp/drivers/thuv1f.py new file mode 100644 index 0000000..c39f17a --- /dev/null +++ b/chirp/drivers/thuv1f.py @@ -0,0 +1,482 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import logging + +from chirp import chirp_common, errors, util, directory, memmap +from chirp import bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + + +def uvf1_identify(radio): + """Do identify handshake with TYT TH-UVF1""" + radio.pipe.write("PROG333") + ack = radio.pipe.read(1) + if ack != "\x06": + raise errors.RadioError("Radio did not respond") + radio.pipe.write("\x02") + ident = radio.pipe.read(16) + LOG.info("Ident:\n%s" % util.hexprint(ident)) + radio.pipe.write("\x06") + ack = radio.pipe.read(1) + if ack != "\x06": + raise errors.RadioError("Radio did not ack identification") + return ident + + +def uvf1_download(radio): + """Download from TYT TH-UVF1""" + data = uvf1_identify(radio) + + for i in range(0, 0x1000, 0x10): + msg = struct.pack(">BHB", ord("R"), i, 0x10) + radio.pipe.write(msg) + block = radio.pipe.read(0x10 + 4) + if len(block) != (0x10 + 4): + raise errors.RadioError("Radio sent a short block") + radio.pipe.write("\x06") + ack = radio.pipe.read(1) + if ack != "\x06": + raise errors.RadioError("Radio NAKed block") + data += block[4:] + + status = chirp_common.Status() + status.cur = i + status.max = 0x1000 + status.msg = "Cloning from radio" + radio.status_fn(status) + + radio.pipe.write("\x45") + + return memmap.MemoryMap(data) + + +def uvf1_upload(radio): + """Upload to TYT TH-UVF1""" + data = uvf1_identify(radio) + + radio.pipe.timeout = 1 + + if data != radio._mmap[:16]: + raise errors.RadioError("Unable to talk to this model") + + for i in range(0, 0x1000, 0x10): + addr = i + 0x10 + msg = struct.pack(">BHB", ord("W"), i, 0x10) + msg += radio._mmap[addr:addr+0x10] + + radio.pipe.write(msg) + ack = radio.pipe.read(1) + if ack != "\x06": + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not ack block %i" % i) + status = chirp_common.Status() + status.cur = i + status.max = 0x1000 + status.msg = "Cloning to radio" + radio.status_fn(status) + + # End of clone? + radio.pipe.write("\x45") + + +THUV1F_MEM_FORMAT = """ +struct mem { + bbcd rx_freq[4]; + bbcd tx_freq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 unknown1:1, + pttid:2, + unknown2:2, + ishighpower:1, + unknown3:2; + u8 unknown4:4, + isnarrow:1, + vox:1, + bcl:2; + u8 unknown5:1, + scan:1, + unknown6:3, + scramble_code:3; + u8 unknown7; +}; + +struct name { + char name[7]; +}; + +#seekto 0x0020; +struct mem memory[128]; + +#seekto 0x0840; +struct { + u8 scans:2, + autolk:1, + unknown1:5; + u8 light:2, + unknown6:2, + disnm:1, + voice:1, + beep:1, + rxsave:1; + u8 led:2, + unknown5:3, + ani:1, + roger:1, + dw:1; + u8 opnmsg:2, + unknown4:1, + dwait:1, + unknown9:4; + u8 squelch; + u8 unknown2:4, + tot:4; + u8 unknown3:4, + vox_level:4; + u8 pad[10]; + char ponmsg[6]; +} settings; + +#seekto 0x08D0; +struct name names[128]; + +""" + +LED_LIST = ["Off", "On", "Auto"] +LIGHT_LIST = ["Purple", "Orange", "Blue"] +VOX_LIST = ["1", "2", "3", "4", "5", "6", "7", "8"] +TOT_LIST = ["Off", "30s", "60s", "90s", "120s", "150s", "180s", "210s", + "240s", "270s"] +SCANS_LIST = ["Time", "Carry", "Seek"] +OPNMSG_LIST = ["Off", "DC", "Message"] + +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5), + chirp_common.PowerLevel("Low", watts=1), + ] + +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +BCL_LIST = ["Off", "CSQ", "QT/DQT"] +CODES_LIST = [x for x in range(1, 9)] + + +@directory.register +class TYTTHUVF1Radio(chirp_common.CloneModeRadio): + """TYT TH-UVF1""" + VENDOR = "TYT" + MODEL = "TH-UVF1" + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 128) + rf.has_bank = False + rf.has_ctone = True + rf.has_tuning_step = False + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_settings = True + rf.can_odd_split = True + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-" + rf.valid_bands = [(136000000, 174000000), + (420000000, 470000000)] + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_modes = ["FM", "NFM"] + rf.valid_name_length = 7 + rf.valid_cross_modes = ["Tone->Tone", "DTCS->DTCS", + "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->"] + + return rf + + def sync_in(self): + try: + self._mmap = uvf1_download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + uvf1_upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + @classmethod + def match_model(cls, filedata, filename): + # TYT TH-UVF1 original + if filedata.startswith("\x13\x60\x17\x40\x40\x00\x48\x00" + + "\x35\x00\x39\x00\x47\x00\x52\x00"): + return True + # TYT TH-UVF1 V2 + elif filedata.startswith("\x14\x40\x14\x80\x43\x00\x45\x00" + + "\x13\x60\x17\x40\x40\x00\x47\x00"): + return True + else: + return False + + def process_mmap(self): + self._memobj = bitwise.parse(THUV1F_MEM_FORMAT, self._mmap) + + def _decode_tone(self, toneval): + pol = "N" + rawval = (toneval[1].get_bits(0xFF) << 8) | toneval[0].get_bits(0xFF) + + if toneval[0].get_bits(0xFF) == 0xFF: + mode = "" + val = 0 + elif toneval[1].get_bits(0xC0) == 0xC0: + mode = "DTCS" + val = int("%x" % (rawval & 0x3FFF)) + pol = "R" + elif toneval[1].get_bits(0x80): + mode = "DTCS" + val = int("%x" % (rawval & 0x3FFF)) + else: + mode = "Tone" + val = int(toneval) / 10.0 + + return mode, val, pol + + def _encode_tone(self, _toneval, mode, val, pol): + toneval = 0 + if mode == "Tone": + toneval = int("%i" % (val * 10), 16) + elif mode == "DTCS": + toneval = int("%i" % val, 16) + toneval |= 0x8000 + if pol == "R": + toneval |= 0x4000 + else: + toneval = 0xFFFF + + _toneval[0].set_raw(toneval & 0xFF) + _toneval[1].set_raw((toneval >> 8) & 0xFF) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.tx_freq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + if _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"): + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + txfreq = int(_mem.tx_freq) * 10 + if self._is_txinh(_mem): + mem.duplex = "off" + mem.offset = 0 + elif txfreq == mem.freq: + mem.duplex = "" + elif abs(txfreq - mem.freq) > 70000000: + mem.duplex = "split" + mem.offset = txfreq + elif txfreq < mem.freq: + mem.duplex = "-" + mem.offset = mem.freq - txfreq + elif txfreq > mem.freq: + mem.duplex = "+" + mem.offset = txfreq - mem.freq + + txmode, txval, txpol = self._decode_tone(_mem.tx_tone) + rxmode, rxval, rxpol = self._decode_tone(_mem.rx_tone) + + chirp_common.split_tone_decode( + mem, (txmode, txval, txpol), (rxmode, rxval, rxpol)) + + mem.name = str(self._memobj.names[number - 1].name) + mem.name = mem.name.replace("\xFF", " ").rstrip() + + mem.skip = not _mem.scan and "S" or "" + mem.mode = _mem.isnarrow and "NFM" or "FM" + mem.power = POWER_LEVELS[1 - _mem.ishighpower] + + mem.extra = RadioSettingGroup("extra", "Extra Settings") + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("vox", "VOX", + RadioSettingValueBoolean(_mem.vox)) + mem.extra.append(rs) + + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueList(BCL_LIST, + BCL_LIST[_mem.bcl])) + mem.extra.append(rs) + + rs = RadioSetting("scramble_code", "Scramble Code", + RadioSettingValueList( + CODES_LIST, CODES_LIST[_mem.scramble_code])) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + if mem.empty: + _mem.set_raw("\xFF" * 16) + return + + if _mem.get_raw() == ("\xFF" * 16): + LOG.debug("Initializing empty memory") + _mem.set_raw("\x00" * 16) + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "off": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.tx_freq = mem.offset / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + (txmode, txval, txpol), (rxmode, rxval, rxpol) = \ + chirp_common.split_tone_encode(mem) + + self._encode_tone(_mem.tx_tone, txmode, txval, txpol) + self._encode_tone(_mem.rx_tone, rxmode, rxval, rxpol) + + self._memobj.names[mem.number - 1].name = mem.name.ljust(7, "\xFF") + + _mem.scan = mem.skip == "" + _mem.isnarrow = mem.mode == "NFM" + _mem.ishighpower = mem.power == POWER_LEVELS[0] + + for element in mem.extra: + setattr(_mem, element.get_name(), element.value) + + def get_settings(self): + _settings = self._memobj.settings + + group = RadioSettingGroup("basic", "Basic") + top = RadioSettings(group) + + group.append( + RadioSetting("led", "LED Mode", + RadioSettingValueList(LED_LIST, + LED_LIST[_settings.led]))) + group.append( + RadioSetting("light", "Light Color", + RadioSettingValueList(LIGHT_LIST, + LIGHT_LIST[_settings.light]))) + + group.append( + RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch))) + + group.append( + RadioSetting("vox_level", "VOX Level", + RadioSettingValueList(VOX_LIST, + VOX_LIST[_settings.vox_level]))) + + group.append( + RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep))) + + group.append( + RadioSetting("ani", "ANI", + RadioSettingValueBoolean(_settings.ani))) + + group.append( + RadioSetting("dwait", "D.WAIT", + RadioSettingValueBoolean(_settings.dwait))) + + group.append( + RadioSetting("tot", "Timeout Timer", + RadioSettingValueList(TOT_LIST, + TOT_LIST[_settings.tot]))) + + group.append( + RadioSetting("roger", "Roger Beep", + RadioSettingValueBoolean(_settings.roger))) + + group.append( + RadioSetting("dw", "Dual Watch", + RadioSettingValueBoolean(_settings.dw))) + + group.append( + RadioSetting("rxsave", "RX Save", + RadioSettingValueBoolean(_settings.rxsave))) + + group.append( + RadioSetting("scans", "Scans", + RadioSettingValueList(SCANS_LIST, + SCANS_LIST[_settings.scans]))) + + group.append( + RadioSetting("autolk", "Auto Lock", + RadioSettingValueBoolean(_settings.autolk))) + + group.append( + RadioSetting("voice", "Voice", + RadioSettingValueBoolean(_settings.voice))) + + group.append( + RadioSetting("opnmsg", "Opening Message", + RadioSettingValueList(OPNMSG_LIST, + OPNMSG_LIST[_settings.opnmsg]))) + + group.append( + RadioSetting("disnm", "Display Name", + RadioSettingValueBoolean(_settings.disnm))) + + def _filter(name): + LOG.debug(repr(str(name))) + return str(name).rstrip("\xFF").rstrip() + + group.append( + RadioSetting("ponmsg", "Power-On Message", + RadioSettingValueString(0, 6, + _filter(_settings.ponmsg)))) + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + setattr(_settings, element.get_name(), element.value) diff --git a/chirp/drivers/tk270.py b/chirp/drivers/tk270.py new file mode 100644 index 0000000..b7927d2 --- /dev/null +++ b/chirp/drivers/tk270.py @@ -0,0 +1,944 @@ +# Copyright 2016 Pavel Milanes CO7WT, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging + +LOG = logging.getLogger(__name__) + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettings +from textwrap import dedent + +MEM_FORMAT = """ +#seekto 0x0010; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rx_tone[2]; + lbcd tx_tone[2]; + u8 unknown41:1, + unknown42:1, + power:1, // high power set (1=off) + shift:1, // Shift (1=off) + busy:1, // Busy lock (1=off) + unknown46:1, + unknown47:1, + unknown48:1; + u8 rxen; // xff if off, x00 if enabled (if chan sel = 00) + u8 txen; // xff if off, x00 if enabled + u8 unknown7; +} memory[32]; + +#seekto 0x0338; +u8 scan[4]; // 4 bytes / bit LSBF for the channel + +#seekto 0x033C; +u8 active[4]; // 4 bytes / bit LSBF for the active cha + // active = 0 + +#seekto 0x0340; +struct { + u8 kMoni; // monitor key funcion + u8 kScan; // scan key funcion + u8 kDial; // dial key funcion + u8 kTa; // ta key funcion + u8 kLo; // low key funcion + u8 unknown40[7]; + // 0x034c + u8 tot; // TOT val * 30 steps (x00-0xa) + u8 tot_alert; // TOT pre-alert val * 10 steps, (x00-x19) + u8 tot_rekey; // TOT rekey val, 0-60, (x00-x3c) + u8 tot_reset; // TOT reset val, 0-15, (x00-x0f) + // 0x0350 + u8 sql; // SQL level val, 0-9 (default 6) + u8 unknown50[12]; + u8 unknown30:1, + unknown31:1, + dealer:1, // dealer & test mode (1=on) + add:1, // add/del from the scan (1=on) + unknown34:1, + batt_save:1, // Battery save (1=on) + unknown36:1, + beep:1; // beep on tone (1=on) + u8 unknown51[2]; +} settings; + +#seekto 0x03f0; +struct { + u8 batt_level; // inverted (ff-val) + u8 sq_tight; // sq tight (ff-val) + u8 sq_open; // sq open (ff-val) + u8 high_power; // High power + u8 qt_dev; // QT deviation + u8 dqt_dev; // DQT deviation + u8 low_power; // low power +} tune; + +""" + +MEM_SIZE = 0x400 +BLOCK_SIZE = 8 +MEM_BLOCKS = range(0, (MEM_SIZE / BLOCK_SIZE)) +ACK_CMD = "\x06" +TIMEOUT = 0.05 # from 0.03 up it' s safe, we set in 0.05 for a margin + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), + chirp_common.PowerLevel("High", watts=5)] +SKIP_VALUES = ["", "S"] +TONES = chirp_common.TONES +#TONES.remove(254.1) +DTCS_CODES = chirp_common.DTCS_CODES + +# some vars for the UI +off = ["off"] +TOT = off + ["%s" % x for x in range(30, 330, 30)] +TOT_A = off + ["%s" % x for x in range(10, 260, 10)] +TOT_RK = off + ["%s" % x for x in range(1, 61)] +TOT_RS = off + ["%s" % x for x in range(1, 16)] +SQL = off + ["%s" % x for x in range(1, 10)] + +# keys +MONI = off + ["Monitor momentary", "Monitor lock", "SQ off momentary"] +SCAN = off + ["Carrier operated (COS)", "Time operated (TOS)"] +YESNO = ["Enabled", "Disabled"] +TA = off + ["Turn around", "Reverse"] + +def rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + #print("<= %02i: %s" % (len(data), util.hexprint(data))) + except: + raise errors.RadioError("Error reading data from radio") + + return data + + +def rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + #print("=> %02i: %s" % (len(data), util.hexprint(data))) + except: + raise errors.RadioError("Error sending data from radio") + + +def send(radio, frame): + """Generic send data to the radio""" + rawsend(radio, frame) + + +def make_frame(cmd, addr, data=""): + """Pack the info in the format it likes""" + ts = struct.pack(">BHB", ord(cmd), addr, 8) + if data == "": + return ts + else: + if len(data) == 8: + return ts + data + else: + raise errors.InvalidValueError("Data of unexpected length to send") + + +def handshake(radio, msg="", full=False): + """Make a full handshake, if not full just hals""" + # send ACK if commandes + if full is True: + rawsend(radio, ACK_CMD) + # receive ACK + ack = rawrecv(radio, 1) + # check ACK + if ack != ACK_CMD: + #close_radio(radio) + mesg = "Handshake failed: " + msg + raise Exception(mesg) + + +def recv(radio): + """Receive data from the radio, 12 bytes, 4 in the header, 8 as data""" + rxdata = rawrecv(radio, 12) + + if len(rxdata) != 12: + raise errors.RadioError( + "Received a length of data that is not possible") + return + + cmd, addr, length = struct.unpack(">BHB", rxdata[0:4]) + data = "" + if length == 8: + data = rxdata[4:] + + return data + + +def open_radio(radio): + """Open the radio into program mode and check if it's the correct model""" + # Set serial discipline + try: + radio.pipe.parity = "N" + radio.pipe.timeout = TIMEOUT + radio.pipe.flush() + except: + msg = "Serial error: Can't set serial line discipline" + raise errors.RadioError(msg) + + # we will try to open the radio 5 times, this is an improved mechanism + magic = "PROGRAM" + exito = False + for i in range(0, 5): + for i in range(0, len(magic)): + ack = rawrecv(radio, 1) + time.sleep(0.05) + send(radio, magic[i]) + + try: + handshake(radio, "Radio not entering Program mode") + exito = True + break + except: + LOG.debug("Attempt #%s, failed, trying again" % i) + pass + + # check if we had EXITO + if exito is False: + msg = "The radio did not accept program mode after five tries.\n" + msg += "Check you interface cable and power cycle your radio." + raise errors.RadioError(msg) + + rawsend(radio, "\x02") + ident = rawrecv(radio, 8) + handshake(radio, "Comm error after ident", True) + + if not (radio.TYPE in ident): + LOG.debug("Incorrect model ID, got %s" % util.hexprint(ident)) + msg = "Incorrect model ID, got %s, it not contains %s" % \ + (ident[0:5], radio.TYPE) + raise errors.RadioError(msg) + + +def do_download(radio): + """This is your download function""" + open_radio(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in MEM_BLOCKS: + send(radio, make_frame("R", addr * BLOCK_SIZE)) + data += recv(radio) + handshake(radio, "Rx error in block %03i" % addr, True) + # DEBUG + #print("Block: %04x, Pos: %06x" % (addr, addr * BLOCK_SIZE)) + + # UI Update + status.cur = addr + status.msg = "Cloning from radio..." + radio.status_fn(status) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + """Upload info to radio""" + open_radio(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + count = 0 + + for addr in MEM_BLOCKS: + # UI Update + status.cur = addr + status.msg = "Cloning to radio..." + radio.status_fn(status) + + block = addr * BLOCK_SIZE + # Beyond 0x03d0 the data is not writable + if block > 0x3d0: + continue + + data = radio.get_mmap()[block:block + BLOCK_SIZE] + send(radio, make_frame("W", block, data)) + + time.sleep(0.02) + handshake(radio, "Rx error in block %03i" % addr) + + +def get_radio_id(data): + """Extract the radio identification from the firmware""" + # Reverse the radio id string. MemoryMap does not support the step/stride + # slice argument, so it is first sliced to a str then reversed. + return data[0x03d0:0x03d8][::-1] + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = get_radio_id(data) + + # DEBUG + #print("Full ident string is %s" % util.hexprint(rid)) + + if (rid in cls.VARIANTS): + # correct model + return True + else: + return False + + +class Kenwood_P60_Radio(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): + """Kenwood Mobile Family 60 Radios""" + VENDOR = "Kenwood" + _range = [350000000, 512000000] # don't mind, it will be overited + _upper = 32 + VARIANT = "" + MODEL = "" + _kind = "" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental; not all features have been ' + 'implemented, but it has those features most used by hams.\n' + '\n' + 'This radios are able to work slightly outside the OEM ' + 'frequency limits. After testing, the limit in Chirp has ' + 'been set 4% outside the OEM limit. This allows you to use ' + 'some models on the ham bands.\n' + '\n' + 'Nevertheless, each radio has its own hardware limits and ' + 'your mileage may vary.\n' + ) + rp.pre_download = _(dedent("""\ + Follow this instructions to read your radio: + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to write your radio: + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.has_name = False + rf.has_offset = True + rf.has_mode = False + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = ["FM"] + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = SKIP_VALUES + rf.valid_dtcs_codes = DTCS_CODES + rf.valid_bands = [self._range] + rf.memory_bounds = (1, self._upper) + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0] + return rf + + def sync_in(self): + """Download from radio""" + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + # Get the data ready for upload + try: + self._prep_data() + except: + raise errors.RadioError("Error processing the radio data") + + # do the upload + try: + do_upload(self) + except: + raise errors.RadioError("Error uploading data to radio") + + def set_variant(self): + """Select and set the correct variables for the class acording + to the correct variant of the radio""" + rid = get_radio_id(self._mmap) + + # indentify the radio variant and set the enviroment to it's values + try: + self._upper, low, high, self._kind = self.VARIANTS[rid] + + # Frequency ranges: some model/variants are able to work the near + # ham bands, even if they are outside the OEM ranges. + # By experimentation we found that a +/- 4% at the edges is in most + # cases safe and will cover the near ham band in full + self._range = [low * 1000000 * 0.96, high * 1000000 * 1.04] + + # put the VARIANT in the class, clean the model / CHs / Type + # in the same layout as the KPG program + self._VARIANT = self.MODEL + " [" + str(self._upper) + "CH]: " + # In the OEM string we show the real OEM ranges + self._VARIANT += self._kind + ", %d - %d MHz" % (low, high) + + # DEBUG + #print self._VARIANT + + except KeyError: + LOG.debug("Wrong Kenwood radio, ID or unknown variant") + LOG.debug(util.hexprint(rid)) + raise errors.RadioError( + "Wrong Kenwood radio, ID or unknown variant, see LOG output.") + + def _prep_data(self): + """Prepare the areas in the memmap to do a consistent write + it has to make an update on the x280 flag data""" + achs = 0 + + for i in range(0, self._upper): + if self.get_active(i) is True: + achs += 1 + + # The x0280 area has the settings for the DTMF/2-Tone per channel, + # as we don't support this feature yet, + # we disabled by cleaning the data + #fldata = "\x00\xf0\xff\xff\xff" * achs + \ + #"\xff" * (5 * (self._upper - achs)) + + fldata = "\xFF" * 5 * self._upper + self._fill(0x0280, fldata) + + def _fill(self, offset, data): + """Fill an specified area of the memmap with the passed data""" + for addr in range(0, len(data)): + self._mmap[offset + addr] = data[addr] + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + # to set the vars on the class to the correct ones + self.set_variant() + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_active(self, chan): + """Get the channel active status from the 4 bytes array on the eeprom""" + byte = int(chan/8) + bit = chan % 8 + res = self._memobj.active[byte] & (pow(2, bit)) + res = not bool(res) + + return res + + def set_active(self, chan, value=True): + """Set the channel active status from UI to the mem_map""" + byte = int(chan/8) + bit = chan % 8 + + # DEBUG + #print("SET Chan %s, Byte %s, Bit % s" % (chan, byte, bit)) + + # get the actual value to see if I need to change anything + actual = self.get_active(chan) + if actual != bool(value): + # DEBUG + #print "VALUE %s fliping" % int(not value) + + # I have to flip the value + rbyte = self._memobj.active[byte] + rbyte = rbyte ^ pow(2, bit) + self._memobj.active[byte] = rbyte + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_scan(self, chan): + """Get the channel scan status from the 4 bytes array on the eeprom + then from the bits on the byte, return '' or 'S' as needed""" + result = "S" + byte = int(chan/8) + bit = chan % 8 + res = self._memobj.scan[byte] & (pow(2, bit)) + if res > 0: + result = "" + + return result + + def set_scan(self, chan, value): + """Set the channel scan status from UI to the mem_map""" + byte = int(chan/8) + bit = chan % 8 + + # get the actual value to see if I need to change anything + actual = self.get_scan(chan) + if actual != value: + # I have to flip the value + rbyte = self._memobj.scan[byte] + rbyte = rbyte ^ pow(2, bit) + self._memobj.scan[byte] = rbyte + + def get_memory(self, number): + """Get the mem representation from the radio image""" + _mem = self._memobj.memory[number - 1] + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Memory number + mem.number = number + + if _mem.get_raw()[0] == "\xFF": + mem.empty = True + # but is not enough, you have to clear the memory in the mmap + # to get it ready for the sync_out process, just in case + _mem.set_raw("\xFF" * 16) + # set the channel to inactive state + self.set_active(number - 1, False) + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[4] == "\xFF" or int(_mem.txen) == 255: + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX feq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + # power + mem.power = POWER_LEVELS[int(_mem.power)] + + # skip + mem.skip = self.get_scan(number - 1) + + # tone data + rxtone = txtone = None + txtone = self.decode_tone(_mem.tx_tone) + rxtone = self.decode_tone(_mem.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra + # bank and number in the channel + mem.extra = RadioSettingGroup("extra", "Extra") + + bl = RadioSetting("busy", "Busy Channel lock", + RadioSettingValueBoolean( + not bool(_mem.busy))) + mem.extra.append(bl) + + sf = RadioSetting("shift", "Beat Shift", + RadioSettingValueBoolean( + not bool(_mem.shift))) + mem.extra.append(sf) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI + not ready yet, so it will return as is""" + + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number - 1] + + # Empty memory + if mem.empty: + _mem.set_raw("\xFF" * 16) + self.set_active(mem.number - 1, False) + return + + # freq rx + _mem.rxfreq = mem.freq / 10 + + # rx enabled if valid channel, + # set tx to on, we decide if off after duplex = off + _mem.rxen = 0 + _mem.txen = 0 + + # freq tx + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + # set tx freq on the memap to xff + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + # erase the txen flag + _mem.txen = 255 + else: + _mem.txfreq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.tx_tone, txmode, txtone, txpol) + self.encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol) + + # power, default power is high, as the low is configurable via a key + if mem.power is None: + mem.power = POWER_LEVELS[1] + + _mem.power = POWER_LEVELS.index(mem.power) + + # skip + self.set_scan(mem.number - 1, mem.skip) + + # set as active + self.set_active(mem.number - 1, True) + + # extra settings + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + return mem + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + sett = self._memobj.settings + + # basic features of the radio + basic = RadioSettingGroup("basic", "Basic Settings") + # keys + fkeys = RadioSettingGroup("keys", "Function keys") + + top = RadioSettings(basic, fkeys) + + # Basic + val = RadioSettingValueString(0, 35, self._VARIANT) + val.set_mutable(False) + mod = RadioSetting("not.mod", "Radio version", val) + basic.append(mod) + + beep = RadioSetting("settings.beep", "Beep tone", + RadioSettingValueBoolean( + bool(sett.beep))) + basic.append(beep) + + bsave = RadioSetting("settings.batt_save", "Battery save", + RadioSettingValueBoolean( + bool(sett.batt_save))) + basic.append(bsave) + + deal = RadioSetting("settings.dealer", "Dealer & Test", + RadioSettingValueBoolean( + bool(sett.dealer))) + basic.append(deal) + + add = RadioSetting("settings.add", "Del / Add feature", + RadioSettingValueBoolean( + bool(sett.add))) + basic.append(add) + + # In some cases the values that follows can be 0xFF (HARD RESET) + # so we need to take and validate that + if int(sett.tot) == 0xff: + # 120 sec + sett.tot = 4 + if int(sett.tot_alert) == 0xff: + # 10 secs + sett.tot_alert = 1 + if int(sett.tot_rekey) == 0xff: + # off + sett.tot_rekey = 0 + if int(sett.tot_reset) == 0xff: + # off + sett.tot_reset = 0 + if int(sett.sql) == 0xff: + # a confortable level ~6 + sett.sql = 6 + + tot = RadioSetting("settings.tot", "Time Out Timer (TOT)", + RadioSettingValueList(TOT, TOT[int(sett.tot)])) + basic.append(tot) + + tota = RadioSetting("settings.tot_alert", "TOT pre-plert", + RadioSettingValueList(TOT_A, TOT_A[int(sett.tot_alert)])) + basic.append(tota) + + totrk = RadioSetting("settings.tot_rekey", "TOT rekey time", + RadioSettingValueList(TOT_RK, TOT_RK[int(sett.tot_rekey)])) + basic.append(totrk) + + totrs = RadioSetting("settings.tot_reset", "TOT reset time", + RadioSettingValueList(TOT_RS, TOT_RS[int(sett.tot_reset)])) + basic.append(totrs) + + sql = RadioSetting("settings.sql", "Squelch level", + RadioSettingValueList(SQL, SQL[int(sett.sql)])) + basic.append(sql) + + # front keys + m = int(sett.kMoni) + if m > 3: + m = 1 + mon = RadioSetting("settings.kMoni", "Monitor", + RadioSettingValueList(MONI, MONI[m])) + fkeys.append(mon) + + s = int(sett.kScan) + if s > 3: + s = 1 + scn = RadioSetting("settings.kScan", "Scan", + RadioSettingValueList(SCAN, SCAN[s])) + fkeys.append(scn) + + d = int(sett.kDial) + if d > 1: + d = 0 + dial = RadioSetting("settings.kDial", "Dial", + RadioSettingValueList(YESNO, YESNO[d])) + fkeys.append(dial) + + t = int(sett.kTa) + if t > 2: + t = 2 + ta = RadioSetting("settings.kTa", "Ta", + RadioSettingValueList(TA, TA[t])) + fkeys.append(ta) + + l = int(sett.kLo) + if l > 1: + l = 0 + low = RadioSetting("settings.kLo", "Low", + RadioSettingValueList(YESNO, YESNO[l])) + fkeys.append(low) + + return top + + def set_settings(self, settings): + """Translate the settings in the UI into bit in the mem_struct + I don't understand well the method used in many drivers + so, I used mine, ugly but works ok""" + + mobj = self._memobj + + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + + # Let's roll the ball + if "." in element.get_name(): + inter, setting = element.get_name().split(".") + # you must ignore the settings with "not" + # this are READ ONLY attributes + if inter == "not": + continue + + obj = getattr(mobj, inter) + value = element.value + + # integers case + special case + if setting in ["tot", "tot_alert", "tot_rekey", \ + "tot_reset", "sql", "kMoni", "kScan", \ + "kDial", "kTa", "kLo"]: + # catching the "off" values as zero + try: + value = int(value) + except: + value = 0 + + # Bool types + inverted + if setting in ["beep", "batt_save", "dealer", "add"]: + value = bool(value) + + # Apply al configs done + # DEBUG + #print("%s: %s" % (setting, value)) + setattr(obj, setting, value) + + +# This are the oldest family 60 models just portables support here +# Info striped from a hexdump inside the preogram and hack over a +# tk-270 + +@directory.register +class TK260_Radio(Kenwood_P60_Radio): + """Kenwood TK-260 Radios""" + MODEL = "TK-260" + TYPE = "P0260" + VARIANTS = { + "P0260\x20\x00\x00": (4, 136, 150, "F2"), + "P0260\x21\x00\x00": (4, 150, 174, "F1"), + } + + +@directory.register +class TK270_Radio(Kenwood_P60_Radio): + """Kenwood TK-270 Radios""" + MODEL = "TK-270" + TYPE = "P0270" + VARIANTS = { + "P0270\x10\x00\x00": (32, 136, 150, "F2"), + "P0270\x11\x00\x00": (32, 150, 174, "F1"), + } + + +@directory.register +class TK272_Radio(Kenwood_P60_Radio): + """Kenwood TK-272 Radios""" + MODEL = "TK-272" + TYPE = "P0272" + VARIANTS = { + "P0272\x10\x00\x00": (10, 136, 150, "F2"), + "P0272\x11\x00\x00": (10, 150, 174, "F1"), + } + + +@directory.register +class TK278_Radio(Kenwood_P60_Radio): + """Kenwood TK-278 Radios""" + MODEL = "TK-278" + TYPE = "P0278" + VARIANTS = { + "P0278\x00\x00\x00": (32, 136, 150, "F2"), + "P0278\x01\x00\x00": (32, 150, 174, "F1"), + } + + +@directory.register +class TK360_Radio(Kenwood_P60_Radio): + """Kenwood TK-360 Radios""" + MODEL = "TK-360" + TYPE = "P0360" + VARIANTS = { + "P0360\x24\x00\x00": (4, 450, 470, "F1"), + "P0360\x25\x00\x00": (4, 470, 490, "F2"), + "P0360\x26\x00\x00": (4, 490, 512, "F3"), + "P0360\x23\x00\x00": (4, 406, 430, "F4"), + } + + +@directory.register +class TK370_Radio(Kenwood_P60_Radio): + """Kenwood TK-370 Radios""" + MODEL = "TK-370" + TYPE = "P0370" + VARIANTS = { + "P0370\x14\x00\x00": (32, 450, 470, "F1"), + "P0370\x15\x00\x00": (32, 470, 490, "F2"), + "P0370\x16\x00\x00": (32, 490, 512, "F3"), + "P0370\x13\x00\x00": (32, 406, 430, "F4"), + } + + +@directory.register +class TK372_Radio(Kenwood_P60_Radio): + """Kenwood TK-372 Radios""" + MODEL = "TK-372" + TYPE = "P0372" + VARIANTS = { + "P0372\x14\x00\x00": (10, 450, 470, "F1"), + "P0372\x15\x00\x00": (10, 470, 490, "F2"), + } + + +@directory.register +class TK378_Radio(Kenwood_P60_Radio): + """Kenwood TK-378 Radios""" + MODEL = "TK-378" + TYPE = "P0378" + VARIANTS = { + "P0378\x04\x00\x00": (32, 370, 470, "SP1"), + "P0378\x02\x00\x00": (32, 350, 427, "SP2"), + } diff --git a/chirp/drivers/tk760.py b/chirp/drivers/tk760.py new file mode 100644 index 0000000..c9a2ff1 --- /dev/null +++ b/chirp/drivers/tk760.py @@ -0,0 +1,881 @@ +# Copyright 2016 Pavel Milanes CO7WT, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging + +LOG = logging.getLogger(__name__) + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettings +from textwrap import dedent + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; +} memory[32]; + +#seekto 0x0100; +struct { + lbcd rx_tone[2]; + lbcd tx_tone[2]; +} tone[32]; + +#seekto 0x0180; +struct { + u8 unknown0:1, + unknown1:1, + wide:1, // wide: 1 = wide, 0 = narrow + power:1, // power: 1 = high, 0 = low + busy_lock:1, // busy lock: 1 = off, 0 = on + pttid:1, // ptt id: 1 = off, 0 = on + dtmf:1, // dtmf signaling: 1 = off, 0 = on + twotone:1; // 2-tone signaling: 1 = off, 0 = on +} ch_settings[32]; + +#seekto 0x02B0; +struct { + u8 unknown10[16]; // x02b0 + u8 unknown11[16]; // x02c0 + u8 active[4]; // x02d0 + u8 scan[4]; // x02d4 + u8 unknown12[8]; // x02d8 + u8 unknown13; // x02e0 + u8 kMON; // 0x02d1 MON Key + u8 kA; // 0x02d2 A Key + u8 kSCN; // 0x02d3 SCN Key + u8 kDA; // 0x02d4 D/A Key + u8 unknown14; // x02e5 + u8 min_vol; // x02e6 byte 0-31 0 = off + u8 poweron_tone; // x02e7 power on tone 0 = off, 1 = on + u8 tot; // x02e8 Time out Timer 0 = off, 1 = 30s (max 300) + u8 unknown15[3]; // x02e9-x02eb + u8 dealer_tuning; // x02ec ? bit 0? 0 = off, 1 = on + u8 clone; // x02ed ? bit 0? 0 = off, 1 = on + u8 unknown16[2]; // x02ee-x2ef + u8 unknown17[16]; // x02f0 + u8 unknown18[5]; // x0300 + u8 clear2transpond; // x0305 byte 0 = off, 1 = on + u8 off_hook_decode; // x0306 byte 0 = off, 1 = on + u8 off_hook_hornalert; // x0307 byte 0 = off, 1 = on + u8 unknown19[8]; // x0308-x030f + u8 unknown20[16]; // x0310 +} settings; +""" + +KEYS = { + 0x00: "Disabled", + 0x01: "Monitor", + 0x02: "Talk Around", + 0x03: "Horn Alert", + 0x04: "Public Adress", + 0x05: "Auxiliary", + 0x06: "Scan", + 0x07: "Scan Del/Add", + 0x08: "Home Channel", + 0x09: "Operator Selectable Tone" +} + +MEM_SIZE = 0x400 +BLOCK_SIZE = 8 +MEM_BLOCKS = range(0, (MEM_SIZE / BLOCK_SIZE)) +ACK_CMD = "\x06" +# from 0.03 up it' s safe +# I have to turn it up, some users reported problems with this, was 0.05 +TIMEOUT = 0.1 + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), + chirp_common.PowerLevel("High", watts=5)] +MODES = ["NFM", "FM"] +SKIP_VALUES = ["", "S"] +TONES = chirp_common.TONES +#TONES.remove(254.1) +DTCS_CODES = chirp_common.DTCS_CODES + +TOT = ["off"] + ["%s" % x for x in range(30, 330, 30)] +VOL = ["off"] + ["%s" % x for x in range(1, 32)] + + +def rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + #print("<= %02i: %s" % (len(data), util.hexprint(data))) + except: + raise errors.RadioError("Error reading data from radio") + + return data + + +def rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + #print("=> %02i: %s" % (len(data), util.hexprint(data))) + except: + raise errors.RadioError("Error sending data from radio") + + +def send(radio, frame): + """Generic send data to the radio""" + rawsend(radio, frame) + + +def make_frame(cmd, addr, data=""): + """Pack the info in the format it likes""" + ts = struct.pack(">BHB", ord(cmd), addr, 8) + if data == "": + return ts + else: + if len(data) == 8: + return ts + data + else: + raise errors.InvalidValueError("Data length of unexpected length") + + +def handshake(radio, msg="", full=False): + """Make a full handshake, if not full just hals""" + # send ACK if commandes + if full is True: + rawsend(radio, ACK_CMD) + # receive ACK + ack = rawrecv(radio, 1) + # check ACK + if ack != ACK_CMD: + #close_radio(radio) + mesg = "Handshake failed: " + msg + raise errors.RadioError(mesg) + + +def recv(radio): + """Receive data from the radio, 12 bytes, 4 in the header, 8 as data""" + rxdata = rawrecv(radio, 12) + + if len(rxdata) != 12: + raise errors.RadioError( + "Received a length of data that is not possible") + return + + cmd, addr, length = struct.unpack(">BHB", rxdata[0:4]) + data = "" + if length == 8: + data = rxdata[4:] + + return data + + +def open_radio(radio): + """Open the radio into program mode and check if it's the correct model""" + # Set serial discipline + try: + radio.pipe.parity = "N" + radio.pipe.timeout = TIMEOUT + radio.pipe.flush() + LOG.debug("Serial port open successful") + except: + msg = "Serial error: Can't set serial line discipline" + raise errors.RadioError(msg) + + magic = "PROGRAM" + LOG.debug("Sending MAGIC") + exito = False + + # it appears that some buggy interfaces/serial devices keep sending + # data in the RX line, we will try to catch this garbage here + devnull = rawrecv(radio, 256) + + for i in range(0, 5): + LOG.debug("Try %i" % i) + for i in range(0, len(magic)): + ack = rawrecv(radio, 1) + time.sleep(0.05) + send(radio, magic[i]) + + try: + handshake(radio, "Radio not entering Program mode") + LOG.debug("Radio opened for programming") + exito = True + break + except: + LOG.debug("No go, next try") + pass + + # validate the success + if exito is False: + msg = "Radio refuse to enter into program mode after a few tries" + raise errors.RadioError(msg) + + rawsend(radio, "\x02") + ident = rawrecv(radio, 8) + + # validate the input + if len(ident) != 8: + LOG.debug("Wrong ID, get only %s bytes, we expect 8" % len(ident)) + LOG.debug(hexprint(ident)) + msg = "Bad ID received, just %s bytes, we want 8" % len(ident) + raise errors.RadioError(msg) + + handshake(radio, "Comm error after ident", True) + LOG.debug("Correct get ident and hanshake") + + if not (radio.TYPE in ident): + LOG.debug("Incorrect model ID:") + LOG.debug(util.hexprint(ident)) + msg = "Incorrect model ID, got %s, it not contains %s" % \ + (ident[0:5], radio.TYPE) + raise errors.RadioError(msg) + + LOG.debug("Full ident string is:") + LOG.debug(util.hexprint(ident)) + + +def do_download(radio): + """This is your download function""" + open_radio(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + LOG.debug("Starting the downolad") + for addr in MEM_BLOCKS: + send(radio, make_frame("R", addr * BLOCK_SIZE)) + data += recv(radio) + handshake(radio, "Rx error in block %03i" % addr, True) + LOG.debug("Block: %04x, Pos: %06x" % (addr, addr * BLOCK_SIZE)) + + # UI Update + status.cur = addr + status.msg = "Cloning from radio..." + radio.status_fn(status) + + return memmap.MemoryMap(data) + + +def do_upload(radio): + """Upload info to radio""" + open_radio(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + count = 0 + + for addr in MEM_BLOCKS: + # UI Update + status.cur = addr + status.msg = "Cloning to radio..." + radio.status_fn(status) + + pos = addr * BLOCK_SIZE + if pos > 0x0378: + # it seems that from this point forward is read only !?!?!? + continue + + data = radio.get_mmap()[pos:pos + BLOCK_SIZE] + send(radio, make_frame("W", pos, data)) + LOG.debug("Block: %04x, Pos: %06x" % (addr, pos)) + + time.sleep(0.1) + handshake(radio, "Rx error in block %04x" % addr) + + +def get_rid(data): + """Extract the radio identification from the firmware""" + rid = data[0x0378:0x0380] + # we have to invert rid + nrid = "" + for i in range(1, len(rid) + 1): + nrid += rid[-i] + rid = nrid + + return rid + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = get_rid(data) + + # DEBUG + #print("Full ident string is %s" % util.hexprint(rid)) + + if (rid in cls.VARIANTS): + # correct model + return True + else: + return False + + +class Kenwood_M60_Radio(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): + """Kenwood Mobile Family 60 Radios""" + VENDOR = "Kenwood" + _range = [136000000, 500000000] # don't mind, it will be overwritten + _upper = 32 + VARIANT = "" + MODEL = "" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental; not all features have been ' + 'implemented, but it has those features most used by hams.\n' + '\n' + 'This radios are able to work slightly outside the OEM ' + 'frequency limits. After testing, the limit in Chirp has ' + 'been set 4% outside the OEM limit. This allows you to use ' + 'some models on the ham bands.\n' + '\n' + 'Nevertheless, each radio has its own hardware limits and ' + 'your mileage may vary.\n' + ) + rp.pre_download = _(dedent("""\ + Follow this instructions to read your radio: + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to write your radio: + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.has_name = False + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = MODES + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = SKIP_VALUES + rf.valid_dtcs_codes = DTCS_CODES + rf.valid_bands = [self._range] + rf.memory_bounds = (1, self._upper) + return rf + + def sync_in(self): + """Download from radio""" + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + # Get the data ready for upload + try: + self._prep_data() + except: + raise errors.RadioError("Error processing the radio data") + + # do the upload + try: + do_upload(self) + except: + raise errors.RadioError("Error uploading data to radio") + + def set_variant(self): + """Select and set the correct variables for the class acording + to the correct variant of the radio""" + rid = get_rid(self._mmap) + + # indentify the radio variant and set the enviroment to it's values + try: + self._upper, low, high, self._kind = self.VARIANTS[rid] + + # Frequency ranges: some model/variants are able to work the near + # ham bands, even if they are outside the OEM ranges. + # By experimentation we found that a +/- 4% at the edges is in most + # cases safe and will cover the near ham band in full + self._range = [low * 1000000 * 0.96, high * 1000000 * 1.04] + + # put the VARIANT in the class, clean the model / CHs / Type + # in the same layout as the KPG program + self._VARIANT = self.MODEL + " [" + str(self._upper) + "CH]: " + # In the OEM string we show the real OEM ranges + self._VARIANT += self._kind + ", %d - %d MHz" % (low, high) + + except KeyError: + LOG.debug("Wrong Kenwood radio, ID or unknown variant") + LOG.debug(util.hexprint(rid)) + raise errors.RadioError( + "Wrong Kenwood radio, ID or unknown variant, see LOG output.") + + def _prep_data(self): + """Prepare the areas in the memmap to do a consistend write + it has to make an update on the x200 flag data""" + achs = 0 + + for i in range(0, self._upper): + if self.get_active(i) is True: + achs += 1 + + # The x0200 area has the settings for the DTMF/2-Tone per channel, + # as by default any of this radios has the DTMF IC installed; + # we clean this areas + fldata = "\x00\xf0\xff\xff\xff" * achs + \ + "\xff" * (5 * (self._upper - achs)) + self._fill(0x0200, fldata) + + def _fill(self, offset, data): + """Fill an specified area of the memmap with the passed data""" + for addr in range(0, len(data)): + self._mmap[offset + addr] = data[addr] + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + # to set the vars on the class to the correct ones + self.set_variant() + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def get_scan(self, chan): + """Get the channel scan status from the 4 bytes array on the eeprom + then from the bits on the byte, return '' or 'S' as needed""" + result = "S" + byte = int(chan/8) + bit = chan % 8 + res = self._memobj.settings.scan[byte] & (pow(2, bit)) + if res > 0: + result = "" + + return result + + def set_scan(self, chan, value): + """Set the channel scan status from UI to the mem_map""" + byte = int(chan/8) + bit = chan % 8 + + # get the actual value to see if I need to change anything + actual = self.get_scan(chan) + if actual != value: + # I have to flip the value + rbyte = self._memobj.settings.scan[byte] + rbyte = rbyte ^ pow(2, bit) + self._memobj.settings.scan[byte] = rbyte + + def get_active(self, chan): + """Get the channel active status from the 4 bytes array on the eeprom + then from the bits on the byte, return True/False""" + byte = int(chan/8) + bit = chan % 8 + res = self._memobj.settings.active[byte] & (pow(2, bit)) + return bool(res) + + def set_active(self, chan, value=True): + """Set the channel active status from UI to the mem_map""" + byte = int(chan/8) + bit = chan % 8 + + # get the actual value to see if I need to change anything + actual = self.get_active(chan) + if actual != bool(value): + # I have to flip the value + rbyte = self._memobj.settings.active[byte] + rbyte = rbyte ^ pow(2, bit) + self._memobj.settings.active[byte] = rbyte + + def get_memory(self, number): + """Get the mem representation from the radio image""" + _mem = self._memobj.memory[number - 1] + _tone = self._memobj.tone[number - 1] + _ch = self._memobj.ch_settings[number - 1] + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Memory number + mem.number = number + + if _mem.get_raw()[0] == "\xFF" or not self.get_active(number - 1): + mem.empty = True + # but is not enough, you have to crear the memory in the mmap + # to get it ready for the sync_out process + _mem.set_raw("\xFF" * 8) + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[4] == "\xFF": + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX feq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + # power + mem.power = POWER_LEVELS[_ch.power] + + # wide/marrow + mem.mode = MODES[_ch.wide] + + # skip + mem.skip = self.get_scan(number - 1) + + # tone data + rxtone = txtone = None + txtone = self.decode_tone(_tone.tx_tone) + rxtone = self.decode_tone(_tone.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra + # bank and number in the channel + mem.extra = RadioSettingGroup("extra", "Extra") + + bl = RadioSetting("busy_lock", "Busy Channel lock", + RadioSettingValueBoolean( + not bool(_ch.busy_lock))) + mem.extra.append(bl) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI + not ready yet, so it will return as is""" + + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[mem.number - 1] + _tone = self._memobj.tone[mem.number - 1] + _ch = self._memobj.ch_settings[mem.number - 1] + + # Empty memory + if mem.empty: + _mem.set_raw("\xFF" * 8) + # empty the active bit + self.set_active(mem.number - 1, False) + return + + # freq rx + _mem.rxfreq = mem.freq / 10 + + # freq tx + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + for byte in _mem.txfreq: + byte.set_raw("\xFF") + else: + _mem.txfreq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_tone.tx_tone, txmode, txtone, txpol) + self.encode_tone(_tone.rx_tone, rxmode, rxtone, rxpol) + + # power, default power is low + if mem.power is None: + mem.power = POWER_LEVELS[0] + + _ch.power = POWER_LEVELS.index(mem.power) + + # wide/marrow + _ch.wide = MODES.index(mem.mode) + + # skip + self.set_scan(mem.number - 1, mem.skip) + + # extra settings + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + # set the mem a active in the _memmap + self.set_active(mem.number - 1) + + return mem + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + sett = self._memobj.settings + + # basic features of the radio + basic = RadioSettingGroup("basic", "Basic Settings") + # buttons + fkeys = RadioSettingGroup("keys", "Front keys config") + + top = RadioSettings(basic, fkeys) + + # Basic + val = RadioSettingValueString(0, 35, self._VARIANT) + val.set_mutable(False) + mod = RadioSetting("not.mod", "Radio version", val) + basic.append(mod) + + tot = RadioSetting("settings.tot", "Time Out Timer (TOT)", + RadioSettingValueList(TOT, TOT[int(sett.tot)])) + basic.append(tot) + + minvol = RadioSetting("settings.min_vol", "Minimum volume", + RadioSettingValueList(VOL, + VOL[int(sett.min_vol)])) + basic.append(minvol) + + ptone = RadioSetting("settings.poweron_tone", "Power On tone", + RadioSettingValueBoolean( + bool(sett.poweron_tone))) + basic.append(ptone) + + sprog = RadioSetting("settings.dealer_tuning", "Dealer Tuning", + RadioSettingValueBoolean( + bool(sett.dealer_tuning))) + basic.append(sprog) + + clone = RadioSetting("settings.clone", "Allow clone", + RadioSettingValueBoolean( + bool(sett.clone))) + basic.append(clone) + + # front keys + mon = RadioSetting("settings.kMON", "MON", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(sett.kMON))])) + fkeys.append(mon) + + a = RadioSetting("settings.kA", "A", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(sett.kA))])) + fkeys.append(a) + + scn = RadioSetting("settings.kSCN", "SCN", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(sett.kSCN))])) + fkeys.append(scn) + + da = RadioSetting("settings.kDA", "D/A", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(sett.kDA))])) + fkeys.append(da) + + return top + + def set_settings(self, settings): + """Translate the settings in the UI into bit in the mem_struct + I don't understand well the method used in many drivers + so, I used mine, ugly but works ok""" + + mobj = self._memobj + + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + + # Let's roll the ball + if "." in element.get_name(): + inter, setting = element.get_name().split(".") + # you must ignore the settings with "not" + # this are READ ONLY attributes + if inter == "not": + continue + + obj = getattr(mobj, inter) + value = element.value + + # case keys, with special config + if setting[0] == "k": + value = KEYS.keys()[KEYS.values().index(str(value))] + + # integers case + special case + if setting in ["tot", "min_vol"]: + # catching the "off" values as zero + try: + value = int(value) + except: + value = 0 + + # Bool types + inverted + if setting in ["poweron_tone", "dealer_tuning", "clone"]: + value = bool(value) + + # Apply al configs done + # DEBUG + #print("%s: %s" % (setting, value)) + setattr(obj, setting, value) + + +# This are the oldest family 60 models (Black keys), just mobiles support here + +@directory.register +class TK760_Radio(Kenwood_M60_Radio): + """Kenwood TK-760 Radios""" + MODEL = "TK-760" + TYPE = "M0760" + VARIANTS = { + "M0760\x01\x00\x00": (32, 136, 156, "K2"), + "M0760\x00\x00\x00": (32, 148, 174, "K") + } + + +@directory.register +class TK762_Radio(Kenwood_M60_Radio): + """Kenwood TK-762 Radios""" + MODEL = "TK-762" + TYPE = "M0762" + VARIANTS = { + "M0762\x01\x00\x00": (2, 136, 156, "K2"), + "M0762\x00\x00\x00": (2, 148, 174, "K") + } + + +@directory.register +class TK768_Radio(Kenwood_M60_Radio): + """Kenwood TK-768 Radios""" + MODEL = "TK-768" + TYPE = "M0768" + VARIANTS = { + "M0768\x21\x00\x00": (32, 136, 156, "K2"), + "M0768\x20\x00\x00": (32, 148, 174, "K") + } + + +@directory.register +class TK860_Radio(Kenwood_M60_Radio): + """Kenwood TK-860 Radios""" + MODEL = "TK-860" + TYPE = "M0860" + VARIANTS = { + "M0860\x05\x00\x00": (32, 406, 430, "F4"), + "M0860\x04\x00\x00": (32, 488, 512, "F3"), + "M0860\x03\x00\x00": (32, 470, 496, "F2"), + "M0860\x02\x00\x00": (32, 450, 476, "F1") + } + + +@directory.register +class TK862_Radio(Kenwood_M60_Radio): + """Kenwood TK-862 Radios""" + MODEL = "TK-862" + TYPE = "M0862" + VARIANTS = { + "M0862\x05\x00\x00": (2, 406, 430, "F4"), + "M0862\x04\x00\x00": (2, 488, 512, "F3"), + "M0862\x03\x00\x00": (2, 470, 496, "F2"), + "M0862\x02\x00\x00": (2, 450, 476, "F1") + } + + +@directory.register +class TK868_Radio(Kenwood_M60_Radio): + """Kenwood TK-868 Radios""" + MODEL = "TK-868" + TYPE = "M0868" + VARIANTS = { + "M0868\x25\x00\x00": (32, 406, 430, "F4"), + "M0868\x24\x00\x00": (32, 488, 512, "F3"), + "M0868\x23\x00\x00": (32, 470, 496, "F2"), + "M0868\x22\x00\x00": (32, 450, 476, "F1") + } diff --git a/chirp/drivers/tk760g.py b/chirp/drivers/tk760g.py new file mode 100644 index 0000000..0f90d5e --- /dev/null +++ b/chirp/drivers/tk760g.py @@ -0,0 +1,1748 @@ +# Copyright 2016 Pavel Milanes, CO7WT, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import struct +import time +import sys + +from chirp import chirp_common, directory, memmap, errors, util, bitwise +from textwrap import dedent +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +##### IMPORTANT DATA ########################################## +# This radios have a span of +# 0x00000 - 0x08000 => Radio Memory / Settings data +# 0x08000 - 0x10000 => FIRMWARE... hum... +############################################################### + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + u8 unknown0[14]; // x00-x0d unknown + u8 banks; // x0e how many banks are programmed + u8 channels; // x0f how many total channels are programmed + // -- + ul16 tot; // x10 TOT value: range(15, 600, 15); x04b0 = off + u8 tot_rekey; // x12 TOT Re-key value range(0, 60); off= 0 + u8 unknown1; // x13 unknown + u8 tot_reset; // x14 TOT Re-key value range(0, 60); off= 0 + u8 unknown2; // x15 unknows + u8 tot_alert; // x16 TOT pre alert: range(0,10); 0 = off + u8 unknown3[7]; // x17-x1d unknown + u8 sql_level; // x1e SQ reference level + u8 battery_save; // Only for portable: FF = off, x32 = on + // -- + u8 unknown4[10]; // x20 + u8 unknown5:3, // x2d + c2t:1, // 1 bit clear to transpond: 1-off + // This is relative to DTMF / 2-Tone settings + unknown6:4; + u8 unknown7[5]; // x2b-x2f + // -- + u8 unknown8[16]; // x30 ? + u8 unknown9[16]; // x40 ? + u8 unknown10[16]; // x50 ? + u8 unknown11[16]; // x60 ? + // -- + u8 add[16]; // x70-x7f 128 bits corresponding add/skip values + // -- + u8 unknown12:4, // x80 + off_hook_decode:1, // 1 bit off hook decode enabled: 1-off + off_hook_horn_alert:1, // 1 bit off hook horn alert: 1-off + unknown13:2; + u8 unknown14; // x81 + u8 unknown15:3, // x82 + self_prog:1, // 1 bit Self programming enabled: 1-on + clone:1, // 1 bit clone enabled: 1-on + firmware_prog:1, // 1 bit firmware programming enabled: 1-on + unknown16:1, + panel_test:1; // 1 bit panel test enabled + u8 unknown17; // x83 + u8 unknown18:5, // x84 + warn_tone:1, // 1 bit warning tone, enabled: 1-on + control_tone:1, // 1 bit control tone (key tone), enabled: 1-on + poweron_tone:1; // 1 bit power on tone, enabled: 1-on + u8 unknown19[5]; // x85-x89 + u8 min_vol; // minimum volume posible: range(0,32); 0 = off + u8 tone_vol; // minimum tone volume posible: + // xff = continous, range(0, 31) + u8 unknown20[4]; // x8c-x8f + // -- + u8 unknown21[4]; // x90-x93 + char poweronmesg[8]; // x94-x9b power on mesg 8 bytes, off is "\FF" * 8 + u8 unknown22[4]; // x9c-x9f + // -- + u8 unknown23[7]; // xa0-xa6 + char ident[8]; // xa7-xae radio identification string + u8 unknown24; // xaf + // -- + u8 unknown26[11]; // xaf-xba + char lastsoftversion[5]; // software version employed to program the radio +} settings; + +#seekto 0xd0; +struct { + u8 unknown[4]; + char radio[6]; + char data[6]; +} passwords; + +#seekto 0x0110; +struct { + u8 kA; // Portable > Closed circle + u8 kDA; // Protable > Triangle to Left + u8 kGROUP_DOWN; // Protable > Triangle to Right + u8 kGROUP_UP; // Protable > Side 1 + u8 kSCN; // Portable > Open Circle + u8 kMON; // Protable > Side 2 + u8 kFOOT; + u8 kCH_UP; + u8 kCH_DOWN; + u8 kVOL_UP; + u8 kVOL_DOWN; + u8 unknown30[5]; + // -- + u8 unknown31[4]; + u8 kP_KNOB; // Just portable: channel knob + u8 unknown32[11]; +} keys; + +#seekto 0x0140; +struct { + lbcd tf01_rx[4]; + lbcd tf01_tx[4]; + u8 tf01_u_rx; + u8 tf01_u_tx; + lbcd tf02_rx[4]; + lbcd tf02_tx[4]; + u8 tf02_u_rx; + u8 tf02_u_tx; + lbcd tf03_rx[4]; + lbcd tf03_tx[4]; + u8 tf03_u_rx; + u8 tf03_u_tx; + lbcd tf04_rx[4]; + lbcd tf04_tx[4]; + u8 tf04_u_rx; + u8 tf04_u_tx; + lbcd tf05_rx[4]; + lbcd tf05_tx[4]; + u8 tf05_u_rx; + u8 tf05_u_tx; + lbcd tf06_rx[4]; + lbcd tf06_tx[4]; + u8 tf06_u_rx; + u8 tf06_u_tx; + lbcd tf07_rx[4]; + lbcd tf07_tx[4]; + u8 tf07_u_rx; + u8 tf07_u_tx; + lbcd tf08_rx[4]; + lbcd tf08_tx[4]; + u8 tf08_u_rx; + u8 tf08_u_tx; + lbcd tf09_rx[4]; + lbcd tf09_tx[4]; + u8 tf09_u_rx; + u8 tf09_u_tx; + lbcd tf10_rx[4]; + lbcd tf10_tx[4]; + u8 tf10_u_rx; + u8 tf10_u_tx; + lbcd tf11_rx[4]; + lbcd tf11_tx[4]; + u8 tf11_u_rx; + u8 tf11_u_tx; + lbcd tf12_rx[4]; + lbcd tf12_tx[4]; + u8 tf12_u_rx; + u8 tf12_u_tx; + lbcd tf13_rx[4]; + lbcd tf13_tx[4]; + u8 tf13_u_rx; + u8 tf13_u_tx; + lbcd tf14_rx[4]; + lbcd tf14_tx[4]; + u8 tf14_u_rx; + u8 tf14_u_tx; + lbcd tf15_rx[4]; + lbcd tf15_tx[4]; + u8 tf15_u_rx; + u8 tf15_u_tx; + lbcd tf16_rx[4]; + lbcd tf16_tx[4]; + u8 tf16_u_rx; + u8 tf16_u_tx; +} test_freq; + +#seekto 0x200; +struct { + char line1[32]; + char line2[32]; +} message; + +#seekto 0x2000; +struct { + u8 bnumb; // mem number + u8 bank; // to which bank it belongs + char name[8]; // name 8 chars + u8 unknown20[2]; // unknown yet + lbcd rxfreq[4]; // rx freq + // -- + lbcd txfreq[4]; // tx freq + u8 rx_unkw; // unknown yet + u8 tx_unkw; // unknown yet + ul16 rx_tone; // rx tone + ul16 tx_tone; // tx tone + u8 unknown23[5]; // unknown yet + u8 signaling; // xFF = off, x30 DTMF, x31 2-Tone + // See the zone on x7000 + // -- + u8 ptt_id:2, // ??? BOT = 0, EOT = 1, Both = 2, NONE = 3 + beat_shift:1, // 1 = off + unknown26:2 // ??? + power:1, // power: 0 low / 1 high + compander:1, // 1 = off + wide:1; // wide 1 / 0 narrow + u8 unknown27:6, // ??? + busy_lock:1, // 1 = off + unknown28:1; // ??? + u8 unknown29[14]; // unknown yet +} memory[128]; + +#seekto 0x5900; +struct { + char model[8]; + u8 unknown50[4]; + char type[2]; + u8 unknown51[2]; + // -- + char serial[8]; + u8 unknown52[8]; +} id; + +#seekto 0x6000; +struct { + u8 code[8]; + u8 unknown60[7]; + u8 count; +} bot[128]; + +#seekto 0x6800; +struct { + u8 code[8]; + u8 unknown61[7]; + u8 count; +} eot[128]; + +#seekto 0x7000; +struct { + lbcd dt2_id[5]; // DTMF lbcd ID (000-9999999999) + // 2-Tone = "11 f1 ff ff ff" ??? + // None = "00 f0 ff ff ff" +} dtmf; +""" + +MEM_SIZE = 0x8000 # 32,768 bytes +BLOCK_SIZE = 256 +BLOCKS = MEM_SIZE / BLOCK_SIZE +MEM_BLOCKS = range(0, BLOCKS) + +# define and empty block of data, as it will be used a lot in this code +EMPTY_BLOCK = "\xFF" * 256 + +RO_BLOCKS = range(0x10, 0x1F) + range(0x59, 0x5f) +ACK_CMD = "\x06" + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), + chirp_common.PowerLevel("High", watts=5)] + +MODES = ["NFM", "FM"] # 12.5 / 25 Khz +VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "_-*()/\-+=)" +SKIP_VALUES = ["", "S"] + +TONES = chirp_common.TONES +# TONES.remove(254.1) +DTCS_CODES = chirp_common.DTCS_CODES + +TOT = ["off"] + ["%s" % x for x in range(15, 615, 15)] +TOT_PRE = ["off"] + ["%s" % x for x in range(1, 11)] +TOT_REKEY = ["off"] + ["%s" % x for x in range(1, 61)] +TOT_RESET = ["off"] + ["%s" % x for x in range(1, 16)] +VOL = ["off"] + ["%s" % x for x in range(1, 32)] +TVOL = ["%s" % x for x in range(0, 33)] +TVOL[32] = "Continous" +SQL = ["off"] + ["%s" % x for x in range(1, 10)] + +## BOT = 0, EOT = 1, Both = 2, NONE = 3 +#PTTID = ["BOT", "EOT", "Both", "none"] + +# For debugging purposes +debug = False + +KEYS = { + 0x33: "Display character", + 0x35: "Home Channel", # Posible portable only, chek it + 0x37: "CH down", + 0x38: "CH up", + 0x39: "Key lock", + 0x3a: "Lamp", # Portable only + 0x3b: "Public address", + 0x3c: "Reverse", # Just in updated firmwares (768G) + 0x3d: "Horn alert", + 0x3e: "Selectable QT", # Just in updated firmwares (768G) + 0x3f: "2-tone encode", + 0x40: "Monitor A: open mommentary", + 0x41: "Monitor B: Open Toggle", + 0x42: "Monitor C: Carrier mommentary", + 0x43: "Monitor D: Carrier toogle", + 0x44: "Operator selectable tone", + 0x45: "Redial", + 0x46: "RF Power Low", # portable only ? + 0x47: "Scan", + 0x48: "Scan del/add", + 0x4a: "GROUP down", + 0x4b: "GROUP up", + #0x4e: "Tone off (Experimental)", # undocumented !!!! + 0x4f: "None", + 0x50: "VOL down", + 0x51: "VOL up", + 0x52: "Talk around", + 0x5d: "AUX", + 0xa1: "Channel Up/Down" # Knob for portables only + } + + +def _raw_recv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + raise errors.RadioError("Error reading data from radio") + + # DEBUG + if debug is True: + LOG.debug("<== (%d) bytes:\n\n%s" % (len(data), util.hexprint(data))) + + return data + + +def _raw_send(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + # DEBUG + if debug is True: + LOG.debug("==> (%d) bytes:\n\n%s" % (len(data), util.hexprint(data))) + + +def _close_radio(radio): + """Get the radio out of program mode""" + _raw_send(radio, "\x45") + + +def _checksum(data): + """the radio block checksum algorithm""" + cs = 0 + for byte in data: + cs += ord(byte) + return cs % 256 + + +def _send(radio, frame): + """Generic send data to the radio""" + _raw_send(radio, frame) + + +def _make_frame(cmd, addr): + """Pack the info in the format it likes""" + return struct.pack(">BH", ord(cmd), addr) + + +def _handshake(radio, msg=""): + """Make a full handshake""" + # send ACK + _raw_send(radio, ACK_CMD) + # receive ACK + ack = _raw_recv(radio, 1) + # check ACK + if ack != ACK_CMD: + _close_radio(radio) + mesg = "Handshake failed " + msg + # DEBUG + LOG.debug(mesg) + raise Exception(mesg) + + +def _check_write_ack(r, ack, addr): + """Process the ack from the write process + this is half handshake needed in tx data block""" + # all ok + if ack == ACK_CMD: + return True + + # Explicit BAD checksum + if ack == "\x15": + _close_radio(r) + raise errors.RadioError( + "Bad checksum in block %02x write" % addr) + + # everything else + _close_radio(r) + raise errors.RadioError( + "Problem with the ack to block %02x write, ack %03i" % + (addr, int(ack))) + + +def _recv(radio): + """Receive data from the radio, 258 bytes split in (cmd, data, checksum) + checking the checksum to be correct, and returning just + 256 bytes of data or false if short empty block""" + rxdata = _raw_recv(radio, BLOCK_SIZE + 2) + # when the RX block has two bytes and the first is \x5A + # then the block is all \xFF + if len(rxdata) == 2 and rxdata[0] == "\x5A": + # fast work in linux has to make the handshake, slow windows don't + if not sys.platform in ["win32", "cygwin"]: + _handshake(radio, "short block") + return False + elif len(rxdata) != 258: + # not the amount of data we want + msg = "The radio send %d bytes, we need 258" % len(rxdata) + # DEBUG + LOG.error(msg) + raise errors.RadioError(msg) + else: + rcs = ord(rxdata[-1]) + data = rxdata[1:-1] + ccs = _checksum(data) + + if rcs != ccs: + _close_radio(radio) + raise errors.RadioError( + "Block Checksum Error! real %02x, calculated %02x" % + (rcs, ccs)) + + _handshake(radio, "after checksum") + return data + + +def _open_radio(radio, status): + """Open the radio into program mode and check if it's the correct model""" + # linux min is 0.13, win min is 0.25; set to bigger to be safe + radio.pipe.timeout = 0.4 + radio.pipe.parity = "E" + + # DEBUG + LOG.debug("Entering program mode.") + # max tries + tries = 10 + + # UI + status.cur = 0 + status.max = tries + status.msg = "Entering program mode..." + + # try a few times to get the radio into program mode + exito = False + for i in range(0, tries): + _raw_send(radio, "PROGRAM") + ack = _raw_recv(radio, 1) + + if ack != ACK_CMD: + # DEBUG + LOG.debug("Try %s failed, traying again..." % i) + time.sleep(0.25) + else: + exito = True + break + + status.cur += 1 + radio.status_fn(status) + + + if exito is False: + _close_radio(radio) + LOG.debug("Radio did not accepted PROGRAM command in %s atempts" % tries) + raise errors.RadioError("The radio doesn't accept program mode") + + # DEBUG + LOG.debug("Received ACK to the PROGRAM command, send ID query.") + + _raw_send(radio, "\x02") + rid = _raw_recv(radio, 8) + + if not (radio.TYPE in rid): + # bad response, properly close the radio before exception + _close_radio(radio) + + # DEBUG + LOG.debug("Incorrect model ID:") + LOG.debug(util.hexprint(rid)) + + raise errors.RadioError( + "Incorrect model ID, got %s, it not contains %s" % + (rid.strip("\xff"), radio.TYPE)) + + # DEBUG + LOG.debug("Full ident string is:") + LOG.debug(util.hexprint(rid)) + _handshake(radio) + + status.msg = "Radio ident success!" + radio.status_fn(status) + # a pause + time.sleep(1) + + +def do_download(radio): + """ The download function """ + # UI progress + status = chirp_common.Status() + data = "" + count = 0 + + # open the radio + _open_radio(radio, status) + + # reset UI data + status.cur = 0 + status.max = MEM_SIZE / 256 + status.msg = "Cloning from radio..." + radio.status_fn(status) + + # set the timeout and if windows keep it bigger + if sys.platform in ["win32", "cygwin"]: + # bigger timeout + radio.pipe.timeout = 0.55 + else: + # Linux can keep up, MAC? + radio.pipe.timeout = 0.05 + + # DEBUG + LOG.debug("Starting the download from radio") + + for addr in MEM_BLOCKS: + # send request, but before flush the rx buffer + radio.pipe.flush() + _send(radio, _make_frame("R", addr)) + + # now we get the data + d = _recv(radio) + # if empty block, it return false + # aka we asume a empty 256 xFF block + if d is False: + d = EMPTY_BLOCK + + data += d + + # UI Update + status.cur = count + radio.status_fn(status) + + count += 1 + + _close_radio(radio) + return memmap.MemoryMap(data) + + +def do_upload(radio): + """ The upload function """ + # UI progress + status = chirp_common.Status() + data = "" + count = 0 + + # open the radio + _open_radio(radio, status) + + # update UI + status.cur = 0 + status.max = MEM_SIZE / 256 + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # the default for the original soft as measured + radio.pipe.timeout = 0.5 + + # DEBUG + LOG.debug("Starting the upload to the radio") + + count = 0 + raddr = 0 + for addr in MEM_BLOCKS: + # this is the data block to write + data = radio.get_mmap()[raddr:raddr+BLOCK_SIZE] + + # The blocks from x59-x5F are NOT programmable + # The blocks from x11-x1F are writed only if not empty + if addr in RO_BLOCKS: + # checking if in the range of optional blocks + if addr >= 0x10 and addr <= 0x1F: + # block is empty ? + if data == EMPTY_BLOCK: + # no write of this block + # but we have to continue updating the counters + count += 1 + raddr = count * 256 + continue + else: + count += 1 + raddr = count * 256 + continue + + if data == EMPTY_BLOCK: + frame = _make_frame("Z", addr) + "\xFF" + else: + cs = _checksum(data) + frame = _make_frame("W", addr) + data + chr(cs) + + _send(radio, frame) + + # get the ACK + ack = _raw_recv(radio, 1) + _check_write_ack(radio, ack, addr) + + # DEBUG + LOG.debug("Sending block %02x" % addr) + + # UI Update + status.cur = count + radio.status_fn(status) + + count += 1 + raddr = count * 256 + + _close_radio(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0xA7:0xAE] + if (rid in cls.VARIANTS): + # correct model + return True + else: + return False + + +class Kenwood60GBankModel(chirp_common.BankModel): + """Testing the bank model on kennwood""" + channelAlwaysHasBank = True + + def get_num_mappings(self): + return self._radio._num_banks + + def get_mappings(self): + banks = [] + for i in range(0, self._radio._num_banks): + bindex = i + 1 + bank = self._radio._bclass(self, i, "%03i" % bindex) + bank.index = i + banks.append(bank) + return banks + + def add_memory_to_mapping(self, memory, bank): + self._radio._set_bank(memory.number, bank.index) + + def remove_memory_from_mapping(self, memory, bank): + if self._radio._get_bank(memory.number) != bank.index: + raise Exception("Memory %i not in bank %s. Cannot remove." % + (memory.number, bank)) + + # We can't "Remove" it for good + # the kenwood paradigm don't allow it + # instead we move it to bank 0 + self._radio._set_bank(memory.number, 0) + + def get_mapping_memories(self, bank): + memories = [] + for i in range(0, self._radio._upper): + if self._radio._get_bank(i) == bank.index: + memories.append(self._radio.get_memory(i)) + return memories + + def get_memory_mappings(self, memory): + index = self._radio._get_bank(memory.number) + return [self.get_mappings()[index]] + + +class memBank(chirp_common.Bank): + """A bank model for kenwood""" + # Integral index of the bank (not to be confused with per-memory + # bank indexes + index = 0 + + +class Kenwood_Serie_60G(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio): + """Kenwood Serie 60G Radios base class""" + VENDOR = "Kenwood" + BAUD_RATE = 9600 + _memsize = MEM_SIZE + NAME_LENGTH = 8 + _range = [136000000, 162000000] + _upper = 128 + _chs_progs = 0 + _num_banks = 128 + _bclass = memBank + _kind = "" + VARIANT = "" + MODEL = "" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental; not all features have been ' + 'implemented, but it has those features most used by hams.\n' + '\n' + 'This radios are able to work slightly outside the OEM ' + 'frequency limits. After testing, the limit in Chirp has ' + 'been set 4% outside the OEM limit. This allows you to use ' + 'some models on the ham bands.\n' + '\n' + 'Nevertheless, each radio has its own hardware limits and ' + 'your mileage may vary.\n' + ) + rp.pre_download = _(dedent("""\ + Follow this instructions to download your info: + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio (unblock it if password protected) + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio (unblock it if password protected) + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + """Return information about this radio's features""" + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = True + rf.has_tuning_step = False + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = MODES + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = VALID_CHARS + rf.valid_skips = SKIP_VALUES + rf.valid_dtcs_codes = DTCS_CODES + rf.valid_bands = [self._range] + rf.valid_name_length = 8 + rf.memory_bounds = (1, self._upper) + return rf + + def _fill(self, offset, data): + """Fill an specified area of the memmap with the passed data""" + for addr in range(0, len(data)): + self._mmap[offset + addr] = data[addr] + + def _prep_data(self): + """Prepare the areas in the memmap to do a consistend write + it has to make an update on the x300 area with banks and channel + info; other in the x1000 with banks and channel counts + and a last one in x7000 with flag data""" + rchs = 0 + data = dict() + + # sorting the data + for ch in range(0, self._upper): + mem = self._memobj.memory[ch] + bnumb = int(mem.bnumb) + bank = int(mem.bank) + if bnumb != 255 and (bank != 255 and bank != 0): + try: + data[bank].append(ch) + except: + data[bank] = list() + data[bank].append(ch) + data[bank].sort() + # counting the real channels + rchs = rchs + 1 + + # updating the channel/bank count + self._memobj.settings.channels = rchs + self._chs_progs = rchs + self._memobj.settings.banks = len(data) + + # building the data for the memmap + fdata = "" + + for k, v in data.iteritems(): + # posible bad data + if k == 0: + k = 1 + raise errors.InvalidValueError( + "Invalid bank value '%k', bad data in the image? \ + Trying to fix this, review your bank data!" % k) + c = 1 + for i in v: + fdata += chr(k) + chr(c) + chr(k - 1) + chr(i) + c = c + 1 + + # fill to match a full 256 bytes block + fdata += (len(fdata) % 256) * "\xFF" + + # updating the data in the memmap [x300] + self._fill(0x300, fdata) + + # update the info in x1000; it has 2 bytes with + # x00 = bank , x01 = bank's channel count + # the rest of the 14 bytes are \xff + bdata = "" + for i in range(1, len(data) + 1): + line = chr(i) + chr(len(data[i])) + line += "\xff" * 14 + bdata += line + + # fill to match a full 256 bytes block + bdata += (256 - (len(bdata)) % 256) * "\xFF" + + # fill to match the whole area + bdata += (16 - len(bdata) / 256) * EMPTY_BLOCK + + # updating the data in the memmap [x1000] + self._fill(0x1000, bdata) + + # DTMF id for each channel, 5 bytes lbcd at x7000 + # ############## TODO ################### + fldata = "\x00\xf0\xff\xff\xff" * self._chs_progs + \ + "\xff" * (5 * (self._upper - self._chs_progs)) + + # write it + # updating the data in the memmap [x7000] + self._fill(0x7000, fldata) + + def _set_variant(self): + """Select and set the correct variables for the class acording + to the correct variant of the radio""" + rid = self._mmap[0xA7:0xAE] + + # indentify the radio variant and set the enviroment to it's values + try: + self._upper, low, high, self._kind = self.VARIANTS[rid] + + # Frequency ranges: some model/variants are able to work the near + # ham bands, even if they are outside the OEM ranges. + # By experimentation we found that 4% at the edges is in most + # cases safe and will cover the near ham bands in full + self._range = [low * 1000000 * 0.96, high * 1000000 * 1.04] + + # setting the bank data in the features, 8 & 16 CH dont have banks + if self._upper < 32: + rf = chirp_common.RadioFeatures() + rf.has_bank = False + + # put the VARIANT in the class, clean the model / CHs / Type + # in the same layout as the KPG program + self._VARIANT = self.MODEL + " [" + str(self._upper) + "CH]: " + # In the OEM string we show the real OEM ranges + self._VARIANT += self._kind + ", %d - %d MHz" % (low, high) + + except KeyError: + LOG.debug("Wrong Kenwood radio, ID or unknown variant") + LOG.debug(util.hexprint(rid)) + raise errors.RadioError( + "Wrong Kenwood radio, ID or unknown variant, see LOG output.") + return False + + def sync_in(self): + """Do a download of the radio eeprom""" + self._mmap = do_download(self) + self.process_mmap() + + def sync_out(self): + """Do an upload to the radio eeprom""" + + # chirp signature on the eprom ;-) + sign = "Chirp" + self._fill(0xbb, sign) + + try: + self._prep_data() + do_upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + """Process the memory object""" + # how many channels are programed + self._chs_progs = ord(self._mmap[15]) + + # load the memobj + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + # to set the vars on the class to the correct ones + self._set_variant() + + def get_raw_memory(self, number): + """Return a raw representation of the memory object, which + is very helpful for development""" + return repr(self._memobj.memory[number]) + + def _decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + val = int(val) + if val == 65535: + return '', None, None + elif val >= 0x2800: + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return 'DTCS', code, pol + else: + a = val / 10.0 + return 'Tone', a, None + + def _encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval.set_raw("\xff\xff") + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + val = int("%i" % value, 8) + 0x2800 + if pol == "R": + val += 0xA000 + memval.set_value(val) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def _get_scan(self, chan): + """Get the channel scan status from the 16 bytes array on the eeprom + then from the bits on the byte, return '' or 'S' as needed""" + result = "S" + byte = int(chan/8) + bit = chan % 8 + res = self._memobj.settings.add[byte] & (pow(2, bit)) + if res > 0: + result = "" + + return result + + def _set_scan(self, chan, value): + """Set the channel scan status from UI to the mem_map""" + byte = int(chan/8) + bit = chan % 8 + + # get the actual value to see if I need to change anything + actual = self._get_scan(chan) + if actual != value: + # I have to flip the value + rbyte = self._memobj.settings.add[byte] + rbyte = rbyte ^ pow(2, bit) + self._memobj.settings.add[byte] = rbyte + + def get_memory(self, number): + # Get a low-level memory object mapped to the image + _mem = self._memobj.memory[number - 1] + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Memory number + mem.number = number + + # this radio has a setting about the amount of real chans of the 128 + # olso in the channel has xff on the Rx freq it's empty + if (number > (self._chs_progs + 1)) or (_mem.get_raw()[0] == "\xFF"): + mem.empty = True + # but is not enough, you have to crear the memory in the mmap + # to get it ready for the sync_out process + _mem.set_raw("\xFF" * 48) + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[16] == "\xFF": + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX feq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + # name TAG of the channel + mem.name = str(_mem.name).rstrip() + + # power + mem.power = POWER_LEVELS[_mem.power] + + # wide/marrow + mem.mode = MODES[_mem.wide] + + # skip + mem.skip = self._get_scan(number - 1) + + # tone data + rxtone = txtone = None + txtone = self._decode_tone(_mem.tx_tone) + rxtone = self._decode_tone(_mem.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra + # bank and number in the channel + mem.extra = RadioSettingGroup("extra", "Extra") + + # validate bank + b = int(_mem.bank) + if b > 127 or b == 0: + _mem.bank = b = 1 + + bank = RadioSetting("bank", "Bank it belongs", + RadioSettingValueInteger(1, 128, b)) + mem.extra.append(bank) + + # validate bnumb + if int(_mem.bnumb) > 127: + _mem.bank = mem.number + + bnumb = RadioSetting("bnumb", "Ch number in the bank", + RadioSettingValueInteger(0, 127, _mem.bnumb)) + mem.extra.append(bnumb) + + bs = RadioSetting("beat_shift", "Beat shift", + RadioSettingValueBoolean( + not bool(_mem.beat_shift))) + mem.extra.append(bs) + + cp = RadioSetting("compander", "Compander", + RadioSettingValueBoolean( + not bool(_mem.compander))) + mem.extra.append(cp) + + bl = RadioSetting("busy_lock", "Busy Channel lock", + RadioSettingValueBoolean( + not bool(_mem.busy_lock))) + mem.extra.append(bl) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI + not ready yet, so it will return as is""" + + # get the eprom representation of this channel + _mem = self._memobj.memory[mem.number - 1] + + # if empty memmory + if mem.empty: + _mem.set_raw("\xFF" * 48) + return + + # frequency + _mem.rxfreq = mem.freq / 10 + + # this are a mistery yet, but so falr there is no impact + # whit this default values for new channels + if int(_mem.rx_unkw) == 0xff: + _mem.rx_unkw = 0x35 + _mem.tx_unkw = 0x32 + + # duplex + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + for byte in _mem.txfreq: + byte.set_raw("\xFF") + else: + _mem.txfreq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.tx_tone, txmode, txtone, txpol) + self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol) + + # name TAG of the channel + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _mem.name[i] = mem.name[i] + except IndexError: + _mem.name[i] = "\x20" + + # power + # default power is low + if mem.power is None: + mem.power = POWER_LEVELS[0] + + _mem.power = POWER_LEVELS.index(mem.power) + + # wide/marrow + _mem.wide = MODES.index(mem.mode) + + # scan add property + self._set_scan(mem.number - 1, mem.skip) + + # bank and number in the channel + if int(_mem.bnumb) == 0xff: + _mem.bnumb = mem.number - 1 + _mem.bank = 1 + + # extra settings + for setting in mem.extra: + if setting != "bank" or setting != "bnumb": + setattr(_mem, setting.get_name(), not bool(setting.value)) + + # all data get sync after channel mod + self._prep_data() + + return mem + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + sett = self._memobj.settings + mess = self._memobj.message + keys = self._memobj.keys + idm = self._memobj.id + passwd = self._memobj.passwords + + # basic features of the radio + basic = RadioSettingGroup("basic", "Basic Settings") + # dealer settings + dealer = RadioSettingGroup("dealer", "Dealer Settings") + # buttons + fkeys = RadioSettingGroup("keys", "Front keys config") + + # TODO / PLANED + # adjust feqs + #freqs = RadioSettingGroup("freqs", "Adjust Frequencies") + + top = RadioSettings(basic, dealer, fkeys) + + # Basic + tot = RadioSetting("settings.tot", "Time Out Timer (TOT)", + RadioSettingValueList(TOT, TOT[ + TOT.index(str(int(sett.tot)))])) + basic.append(tot) + + totalert = RadioSetting("settings.tot_alert", "TOT pre alert", + RadioSettingValueList(TOT_PRE, + TOT_PRE[int(sett.tot_alert)])) + basic.append(totalert) + + totrekey = RadioSetting("settings.tot_rekey", "TOT re-key time", + RadioSettingValueList(TOT_REKEY, + TOT_REKEY[int(sett.tot_rekey)])) + basic.append(totrekey) + + totreset = RadioSetting("settings.tot_reset", "TOT reset time", + RadioSettingValueList(TOT_RESET, + TOT_RESET[int(sett.tot_reset)])) + basic.append(totreset) + + # this feature is for mobile only + if self.TYPE[0] == "M": + minvol = RadioSetting("settings.min_vol", "Minimum volume", + RadioSettingValueList(VOL, + VOL[int(sett.min_vol)])) + basic.append(minvol) + + tv = int(sett.tone_vol) + if tv == 255: + tv = 32 + tvol = RadioSetting("settings.tone_vol", "Minimum tone volume", + RadioSettingValueList(TVOL, TVOL[tv])) + basic.append(tvol) + + sql = RadioSetting("settings.sql_level", "SQL Ref Level", + RadioSettingValueList( + SQL, SQL[int(sett.sql_level)])) + basic.append(sql) + + #c2t = RadioSetting("settings.c2t", "Clear to Transpond", + #RadioSettingValueBoolean(not sett.c2t)) + #basic.append(c2t) + + ptone = RadioSetting("settings.poweron_tone", "Power On tone", + RadioSettingValueBoolean(sett.poweron_tone)) + basic.append(ptone) + + ctone = RadioSetting("settings.control_tone", "Control (key) tone", + RadioSettingValueBoolean(sett.control_tone)) + basic.append(ctone) + + wtone = RadioSetting("settings.warn_tone", "Warning tone", + RadioSettingValueBoolean(sett.warn_tone)) + basic.append(wtone) + + # Save Battery only for portables? + if self.TYPE[0] == "P": + bs = int(sett.battery_save) == 0x32 and True or False + bsave = RadioSetting("settings.battery_save", "Battery Saver", + RadioSettingValueBoolean(bs)) + basic.append(bsave) + + ponm = str(sett.poweronmesg).strip("\xff") + pom = RadioSetting("settings.poweronmesg", "Power on message", + RadioSettingValueString(0, 8, ponm, False)) + basic.append(pom) + + # dealer + valid_chars = ",-/:[]" + chirp_common.CHARSET_ALPHANUMERIC + mstr = "".join([c for c in self._VARIANT if c in valid_chars]) + + val = RadioSettingValueString(0, 35, mstr) + val.set_mutable(False) + mod = RadioSetting("not.mod", "Radio Version", val) + dealer.append(mod) + + sn = str(idm.serial).strip(" \xff") + val = RadioSettingValueString(0, 8, sn) + val.set_mutable(False) + serial = RadioSetting("not.serial", "Serial number", val) + dealer.append(serial) + + svp = str(sett.lastsoftversion).strip(" \xff") + val = RadioSettingValueString(0, 5, svp) + val.set_mutable(False) + sver = RadioSetting("not.softver", "Software Version", val) + dealer.append(sver) + + l1 = str(mess.line1).strip(" \xff") + line1 = RadioSetting("message.line1", "Comment 1", + RadioSettingValueString(0, 32, l1)) + dealer.append(line1) + + l2 = str(mess.line2).strip(" \xff") + line2 = RadioSetting("message.line2", "Comment 2", + RadioSettingValueString(0, 32, l2)) + dealer.append(line2) + + sprog = RadioSetting("settings.self_prog", "Self program", + RadioSettingValueBoolean(sett.self_prog)) + dealer.append(sprog) + + clone = RadioSetting("settings.clone", "Allow clone", + RadioSettingValueBoolean(sett.clone)) + dealer.append(clone) + + panel = RadioSetting("settings.panel_test", "Panel Test", + RadioSettingValueBoolean(sett.panel_test)) + dealer.append(panel) + + fmw = RadioSetting("settings.firmware_prog", "Firmware program", + RadioSettingValueBoolean(sett.firmware_prog)) + dealer.append(fmw) + + # front keys + # The Mobile only parameters are wraped here + if self.TYPE[0] == "M": + vu = RadioSetting("keys.kVOL_UP", "VOL UP", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kVOL_UP))])) + fkeys.append(vu) + + vd = RadioSetting("keys.kVOL_DOWN", "VOL DOWN", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kVOL_DOWN))])) + fkeys.append(vd) + + chu = RadioSetting("keys.kCH_UP", "CH UP", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kCH_UP))])) + fkeys.append(chu) + + chd = RadioSetting("keys.kCH_DOWN", "CH DOWN", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kCH_DOWN))])) + fkeys.append(chd) + + foot = RadioSetting("keys.kFOOT", "Foot switch", + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kCH_DOWN))])) + fkeys.append(foot) + + # this is the common buttons for all + + # 260G model don't have the front keys + if not "P2600" in self.TYPE: + scn_name = "SCN" + if self.TYPE[0] == "P": + scn_name = "Open Circle" + + scn = RadioSetting("keys.kSCN", scn_name, + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kSCN))])) + fkeys.append(scn) + + a_name = "A" + if self.TYPE[0] == "P": + a_name = "Closed circle" + + a = RadioSetting("keys.kA", a_name, + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kA))])) + fkeys.append(a) + + da_name = "D/A" + if self.TYPE[0] == "P": + da_name = "< key" + + da = RadioSetting("keys.kDA", da_name, + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kDA))])) + fkeys.append(da) + + gu_name = "Triangle up" + if self.TYPE[0] == "P": + gu_name = "Side 1" + + gu = RadioSetting("keys.kGROUP_UP", gu_name, + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kGROUP_UP))])) + fkeys.append(gu) + + # Side keys on portables + gd_name = "Triangle Down" + if self.TYPE[0] == "P": + gd_name = "> key" + + gd = RadioSetting("keys.kGROUP_DOWN", gd_name, + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kGROUP_DOWN))])) + fkeys.append(gd) + + mon_name = "MON" + if self.TYPE[0] == "P": + mon_name = "Side 2" + + mon = RadioSetting("keys.kMON", mon_name, + RadioSettingValueList(KEYS.values(), + KEYS.values()[KEYS.keys().index( + int(keys.kMON))])) + fkeys.append(mon) + + return top + + def set_settings(self, settings): + """Translate the settings in the UI into bit in the mem_struct + I don't understand well the method used in many drivers + so, I used mine, ugly but works ok""" + + mobj = self._memobj + + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + + # Let's roll the ball + if "." in element.get_name(): + inter, setting = element.get_name().split(".") + # you must ignore the settings with "not" + # this are READ ONLY attributes + if inter == "not": + continue + + obj = getattr(mobj, inter) + value = element.value + + # integers case + special case + if setting in ["tot", "tot_alert", "min_vol", "tone_vol", + "sql_level", "tot_rekey", "tot_reset"]: + # catching the "off" values as zero + try: + value = int(value) + except: + value = 0 + + # tot case step 15 + if setting == "tot": + value = value * 15 + # off is special + if value == 0: + value = 0x4b0 + + # Caso tone_vol + if setting == "tone_vol": + # off is special + if value == 32: + value = 0xff + + # Bool types + inverted + if setting in ["c2t", "poweron_tone", "control_tone", + "warn_tone", "battery_save", "self_prog", + "clone", "panel_test"]: + value = bool(value) + + # this cases are inverted + if setting == "c2t": + value = not value + + # case battery save is special + if setting == "battery_save": + if bool(value) is True: + value = 0x32 + else: + value = 0xff + + # String cases + if setting in ["poweronmesg", "line1", "line2"]: + # some vars + value = str(value) + just = 8 + # lines with 32 + if "line" in setting: + just = 32 + + # empty case + if len(value) == 0: + value = "\xff" * just + else: + value = value.ljust(just) + + # case keys, with special config + if inter == "keys": + value = KEYS.keys()[KEYS.values().index(str(value))] + + # Apply al configs done + setattr(obj, setting, value) + + def get_bank_model(self): + """Pass the bank model to the UI part""" + rf = self.get_features() + if rf.has_bank is True: + return Kenwood60GBankModel(self) + else: + return None + + def _get_bank(self, loc): + """Get the bank data for a specific channel""" + mem = self._memobj.memory[loc - 1] + bank = int(mem.bank) - 1 + + if bank > self._num_banks or bank < 1: + # all channels must belong to a bank, even with just 1 bank + return 0 + else: + return bank + + def _set_bank(self, loc, bank): + """Set the bank data for a specific channel""" + try: + b = int(bank) + if b > 127: + b = 0 + mem = self._memobj.memory[loc - 1] + mem.bank = b + 1 + except: + msg = "You can't have a channel without a bank, click another bank" + raise errors.InvalidDataError(msg) + + +# This kenwwood family is known as "60-G Serie" +# all this radios ending in G are compatible: +# +# Portables VHF TK-260G/270G/272G/278G +# Portables UHF TK-360G/370G/372G/378G/388G +# +# Mobiles VHF TK-760G/762G/768G +# Mobiles VHF TK-860G/862G/868G +# +# WARNING !!!! Radios With Password in the data section ############### +# +# When a radio has a data password (aka to program it) the last byte (#8) +# in the id code change from \xf1 to \xb1; so we remove this last byte +# from the identification procedures and variants. +# +# This effectively render the data password USELESS even if set. +# Translation: Chirps will read and write password protected radios +# with no problem. + + +@directory.register +class TK868G_Radios(Kenwood_Serie_60G): + """Kenwood TK-868G Radio M/C""" + MODEL = "TK-868G" + TYPE = "M8680" + VARIANTS = { + "M8680\x18\xff": (8, 400, 490, "M"), + "M8680;\xff": (128, 350, 390, "C1"), + "M86808\xff": (128, 400, 430, "C2"), + "M86806\xff": (128, 450, 490, "C3"), + } + + +@directory.register +class TK862G_Radios(Kenwood_Serie_60G): + """Kenwood TK-862G Radio K/E/(N)E""" + MODEL = "TK-862G" + TYPE = "M8620" + VARIANTS = { + "M8620\x06\xff": (8, 450, 490, "K"), + "M8620\x07\xff": (8, 485, 512, "K2"), + "M8620&\xff": (8, 440, 470, "E"), + "M8620V\xff": (8, 440, 470, "(N)E"), + } + + +@directory.register +class TK860G_Radios(Kenwood_Serie_60G): + """Kenwood TK-860G Radio K""" + MODEL = "TK-860G" + TYPE = "M8600" + VARIANTS = { + "M8600\x08\xff": (128, 400, 430, "K"), + "M8600\x06\xff": (128, 450, 490, "K1"), + "M8600\x07\xff": (128, 485, 512, "K2"), + "M8600\x18\xff": (128, 400, 430, "M"), + "M8600\x16\xff": (128, 450, 490, "M1"), + "M8600\x17\xff": (128, 485, 520, "M2"), + } + + +@directory.register +class TK768G_Radios(Kenwood_Serie_60G): + """Kenwood TK-768G Radios [M/C]""" + MODEL = "TK-768G" + TYPE = "M7680" + # Note that 8 CH don't have banks + VARIANTS = { + "M7680\x15\xff": (8, 136, 162, "M2"), + "M7680\x14\xff": (8, 148, 174, "M"), + "M76805\xff": (128, 136, 162, "C2"), + "M76804\xff": (128, 148, 174, "C"), + } + + +@directory.register +class TK762G_Radios(Kenwood_Serie_60G): + """Kenwood TK-762G Radios [K/E/NE]""" + MODEL = "TK-762G" + TYPE = "M7620" + # Note that 8 CH don't have banks + VARIANTS = { + "M7620\x05\xff": (8, 136, 162, "K2"), + "M7620\x04\xff": (8, 148, 172, "K"), + "M7620$\xff": (8, 148, 172, "E"), + "M7620T\xff": (8, 148, 172, "NE"), + } + + +@directory.register +class TK760G_Radios(Kenwood_Serie_60G): + """Kenwood TK-760G Radios [K/M/(N)E]""" + MODEL = "TK-760G" + TYPE = "M7600" + VARIANTS = { + "M7600\x05\xff": (128, 136, 162, "K2"), + "M7600\x04\xff": (128, 148, 174, "K"), + "M7600\x14\xff": (128, 148, 174, "M"), + "M7600T\xff": (128, 148, 174, "NE") + } + + +@directory.register +class TK388G_Radios(Kenwood_Serie_60G): + """Kenwood TK-388 Radio [K/E/M/NE]""" + MODEL = "TK-388G" + TYPE = "P3880" + VARIANTS = { + "P3880\x1b\xff": (128, 350, 370, "M") + } + + +@directory.register +class TK378G_Radios(Kenwood_Serie_60G): + """Kenwood TK-378 Radio [K/E/M/NE]""" + MODEL = "TK-378G" + TYPE = "P3780" + VARIANTS = { + "P3780\x16\xff": (16, 450, 470, "M"), + "P3780\x17\xff": (16, 400, 420, "M1"), + "P3780\x36\xff": (128, 490, 512, "C"), + "P3780\x39\xff": (128, 403, 430, "C1") + } + + +@directory.register +class TK372G_Radios(Kenwood_Serie_60G): + """Kenwood TK-372 Radio [K/E/M/NE]""" + MODEL = "TK-372G" + TYPE = "P3720" + VARIANTS = { + "P3720\x06\xff": (32, 450, 470, "K"), + "P3720\x07\xff": (32, 470, 490, "K1"), + "P3720\x08\xff": (32, 490, 512, "K2"), + "P3720\x09\xff": (32, 403, 430, "K3") + } + + +@directory.register +class TK370G_Radios(Kenwood_Serie_60G): + """Kenwood TK-370 Radio [K/E/M/NE]""" + MODEL = "TK-370G" + TYPE = "P3700" + VARIANTS = { + "P3700\x06\xff": (128, 450, 470, "K"), + "P3700\x07\xff": (128, 470, 490, "K1"), + "P3700\x08\xff": (128, 490, 512, "K2"), + "P3700\x09\xff": (128, 403, 430, "K3"), + "P3700\x16\xff": (128, 450, 470, "M"), + "P3700\x17\xff": (128, 470, 490, "M1"), + "P3700\x18\xff": (128, 490, 520, "M2"), + "P3700\x19\xff": (128, 403, 430, "M3"), + "P3700&\xff": (128, 440, 470, "E"), + "P3700V\xff": (128, 440, 470, "NE") + } + + +@directory.register +class TK360G_Radios(Kenwood_Serie_60G): + """Kenwood TK-360 Radio [K/E/M/NE]""" + MODEL = "TK-360G" + TYPE = "P3600" + VARIANTS = { + "P3600\x06\xff": (8, 450, 470, "K"), + "P3600\x07\xff": (8, 470, 490, "K1"), + "P3600\x08\xff": (8, 490, 512, "K2"), + "P3600\x09\xff": (8, 403, 430, "K3"), + "P3600&\xff": (8, 440, 470, "E"), + "P3600)\xff": (8, 406, 430, "E1"), + "P3600\x16\xff": (8, 450, 470, "M"), + "P3600\x17\xff": (8, 470, 490, "M1"), + "P3600\x19\xff": (8, 403, 430, "M2"), + "P3600V\xff": (8, 440, 470, "NE"), + "P3600Y\xff": (8, 403, 430, "NE1") + } + + +@directory.register +class TK278G_Radios(Kenwood_Serie_60G): + """Kenwood TK-278G Radio C/C1/M/M1""" + MODEL = "TK-278G" + TYPE = "P2780" + # Note that 16 CH don't have banks + VARIANTS = { + "P27805\xff": (128, 136, 150, "C1"), + "P27804\xff": (128, 150, 174, "C"), + "P2780\x15\xff": (16, 136, 150, "M1"), + "P2780\x14\xff": (16, 150, 174, "M") + } + + +@directory.register +class TK272G_Radios(Kenwood_Serie_60G): + """Kenwood TK-272G Radio K/K1""" + MODEL = "TK-272G" + TYPE = "P2720" + VARIANTS = { + "P2720\x05\xfb": (32, 136, 150, "K1"), + "P2720\x04\xfb": (32, 150, 174, "K") + } + + +@directory.register +class TK270G_Radios(Kenwood_Serie_60G): + """Kenwood TK-270G Radio K/K1/M/E/NE/NT""" + MODEL = "TK-270G" + TYPE = "P2700" + VARIANTS = { + "P2700T\xff": (128, 146, 174, "NE/NT"), + "P2700$\xff": (128, 146, 174, "E"), + "P2700\x14\xff": (128, 150, 174, "M"), + "P2700\x05\xff": (128, 136, 150, "K1"), + "P2700\x04\xff": (128, 150, 174, "K") + } + + +@directory.register +class TK260G_Radios(Kenwood_Serie_60G): + """Kenwood TK-260G Radio K/K1/M/E/NE/NT""" + MODEL = "TK-260G" + _hasbanks = False + TYPE = "P2600" + VARIANTS = { + "P2600U\xff": (8, 136, 150, "N1"), + "P2600T\xff": (8, 146, 174, "N"), + "P2600$\xff": (8, 150, 174, "E"), + "P2600\x14\xff": (8, 150, 174, "M"), + "P2600\x05\xff": (8, 136, 150, "K1"), + "P2600\x04\xff": (8, 150, 174, "K") + } diff --git a/chirp/drivers/tk8102.py b/chirp/drivers/tk8102.py new file mode 100644 index 0000000..f28a219 --- /dev/null +++ b/chirp/drivers/tk8102.py @@ -0,0 +1,456 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes +import os +import logging +import struct + +from chirp import chirp_common, directory, memmap, errors, util +from chirp import bitwise +from chirp.settings import RadioSettingGroup, RadioSetting +from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList +from chirp.settings import RadioSettingValueString, RadioSettings + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0030; +struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 signaling:2, + unknown1:3, + bcl:1, + wide:1, + beatshift:1; + u8 pttid:2, + highpower:1, + scan:1 + unknown2:4; + u8 unknown3[2]; +} memory[8]; + +#seekto 0x0310; +struct { + char line1[32]; + char line2[32]; +} messages; + +""" + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("High", watts=50)] +MODES = ["NFM", "FM"] +PTTID = ["", "BOT", "EOT", "Both"] +SIGNAL = ["", "DTMF"] + + +def make_frame(cmd, addr, length, data=b""): + return struct.pack(">BHB", ord(cmd), addr, length) + bytes(data) + + +def send(radio, frame): + # LOG.debug("%04i P>R: %s" % (len(frame), util.hexprint(frame))) + radio.pipe.write(frame) + + +def recv(radio, readdata=True): + hdr = radio.pipe.read(4) + cmd, addr, length = struct.unpack(">BHB", hdr) + if readdata: + data = radio.pipe.read(length) + # LOG.debug(" PTone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = ["", "S"] + rf.valid_bands = [self._range] + rf.memory_bounds = (1, self._upper) + return rf + + def sync_in(self): + try: + self._mmap = do_download(self) + except errors.RadioError: + self.pipe.write(b"\x45") + raise + except Exception as e: + raise errors.RadioError("Failed to download from radio: %s" % e) + self.process_mmap() + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def sync_out(self): + try: + do_upload(self) + except errors.RadioError: + self.pipe.write(b"\x45") + raise + except Exception as e: + raise errors.RadioError("Failed to upload to radio: %s" % e) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + if mem.tmode == "DTCS": + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[:4] == "\xFF\xFF\xFF\xFF": + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + offset = (int(_mem.tx_freq) * 10) - mem.freq + if offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + self._get_tone(_mem, mem) + mem.power = POWER_LEVELS[_mem.highpower] + mem.mode = MODES[_mem.wide] + mem.skip = not _mem.scan and "S" or "" + + mem.extra = RadioSettingGroup("all", "All Settings") + + bcl = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueBoolean(bool(_mem.bcl))) + mem.extra.append(bcl) + + beat = RadioSetting("beatshift", "Beat Shift", + RadioSettingValueBoolean(bool(_mem.beatshift))) + mem.extra.append(beat) + + pttid = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(PTTID, + PTTID[_mem.pttid])) + mem.extra.append(pttid) + + signal = RadioSetting("signaling", "Signaling", + RadioSettingValueList(SIGNAL, + SIGNAL[ + _mem.signaling & 0x01])) + mem.extra.append(signal) + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0xA000 + return val + + rx_mode = tx_mode = None + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + tx_tone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = int(mem.ctone * 10) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.tx_tone, rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xFF" * 16) + return + + _mem.unknown3[0] = 0x07 + _mem.unknown3[1] = 0x22 + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + self._set_tone(mem, _mem) + + _mem.highpower = mem.power == POWER_LEVELS[1] + _mem.wide = mem.mode == "FM" + _mem.scan = mem.skip != "S" + + for setting in mem.extra: + if setting.get_name == "signaling": + if setting.value == "DTMF": + _mem.signaling = 0x03 + else: + _mem.signaling = 0x00 + else: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic") + top = RadioSettings(basic) + + def _f(val): + string = "" + for char in str(val): + if char == "\xFF": + break + string += char + return string + + line1 = RadioSetting("messages.line1", "Message Line 1", + RadioSettingValueString(0, 32, + _f(_mem.messages.line1), + autopad=False)) + basic.append(line1) + + line2 = RadioSetting("messages.line2", "Message Line 2", + RadioSettingValueString(0, 32, + _f(_mem.messages.line2), + autopad=False)) + basic.append(line2) + + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if "line" in setting: + value = str(element.value).ljust(32, "\xFF") + else: + value = element.value + setattr(obj, setting, value) + + @classmethod + def match_model(cls, filedata, filename): + model = filedata[0x03D1:0x03D5] + LOG.debug(model) + return model == cls.MODEL.split("-")[1] + + +@directory.register +class KenwoodTK7102Radio(KenwoodTKx102Radio): + MODEL = "TK-7102" + _range = (136000000, 174000000) + _upper = 4 + + +@directory.register +class KenwoodTK8102Radio(KenwoodTKx102Radio): + MODEL = "TK-8102" + _range = (400000000, 500000000) + _upper = 4 + + +@directory.register +class KenwoodTK7108Radio(KenwoodTKx102Radio): + MODEL = "TK-7108" + _range = (136000000, 174000000) + _upper = 8 + + +@directory.register +class KenwoodTK8108Radio(KenwoodTKx102Radio): + MODEL = "TK-8108" + _range = (400000000, 500000000) + _upper = 8 diff --git a/chirp/drivers/tk8180.py b/chirp/drivers/tk8180.py new file mode 100644 index 0000000..6efa9b2 --- /dev/null +++ b/chirp/drivers/tk8180.py @@ -0,0 +1,1215 @@ +# Copyright 2019 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import os +import time +import logging +from collections import OrderedDict + +from chirp import chirp_common, directory, memmap, errors, util +from chirp import bitwise +from chirp.settings import RadioSettingGroup, RadioSetting +from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList +from chirp.settings import RadioSettingValueString, RadioSettingValueInteger +from chirp.settings import RadioSettings + +LOG = logging.getLogger(__name__) + +# Gross hack to handle missing future module on un-updatable +# platforms like MacOS. Just avoid registering these radio +# classes for now. +try: + from builtins import bytes + has_future = True +except ImportError: + has_future = False + LOG.warning('python-future package is not ' + 'available; %s requires it' % __name__) + + +HEADER_FORMAT = """ +#seekto 0x0100; +struct { + char sw_name[7]; + char sw_ver[5]; + u8 unknown1[4]; + char sw_key[12]; + u8 unknown2[4]; + char model[5]; + u8 variant; + u8 unknown3[10]; +} header; + +#seekto 0x0140; +struct { + // 0x0140 + u8 unknown1; + u8 sublcd; + u8 unknown2[30]; + + // 0x0160 + char pon_msgtext[12]; + u8 min_volume; + u8 max_volume; + u8 lo_volume; + u8 hi_volume; + + // 0x0170 + u8 tone_volume_offset; + u8 poweron_tone; + u8 control_tone; + u8 warning_tone; + u8 alert_tone; + u8 sidetone; + u8 locator_tone; + u8 unknown3[2]; + u8 ignition_mode; + u8 ignition_time; // In tens of minutes (6 = 1h) + u8 micsense; + ul16 modereset; + u8 min_vol_preset; + u8 unknown4; + + // 0x0180 + u8 unknown5[16]; + + // 0x0190 + u8 unknown6[3]; + u8 pon_msgtype; + u8 unknown7[8]; + u8 unknown8_1:2, + ssi:1, + busy_led:1, + power_switch_memory:1, + scrambler_memory:1, + unknown8_2:1, + off_hook_decode:1; + u8 unknown9_1:5, + clockfmt:1, + datefmt:1, + ignition_sense:1; + u8 unknownA[2]; + + // 0x01A0 + u8 unknownB[8]; + u8 ptt_timer; + u8 unknownB2[3]; + u8 ptt_proceed:1, + unknownC_1:3, + tone_off:1, + ost_memory:1, + unknownC_2:1, + ptt_release:1; + u8 unknownD[3]; +} settings; + +#seekto 0x01E0; +struct { + char name[12]; + ul16 rxtone; + ul16 txtone; +} ost_tones[40]; + +#seekto 0x0A00; +ul16 zone_starts[128]; + +struct zoneinfo { + u8 number; + u8 zonetype; + u8 unknown1[2]; + u8 count; + char name[12]; + u8 unknown2[2]; + ul16 timeout; // 15-1200 + ul16 tot_alert; // 10 + ul16 tot_rekey; // 60 + ul16 tot_reset; // 15 + u8 unknown3[3]; + u8 unknown21:2, + bcl_override:1, + unknown22:5; + u8 unknown5; +}; + +struct memory { + u8 number; + lbcd rx_freq[4]; + lbcd tx_freq[4]; + u8 unknown1[2]; + ul16 rx_tone; + ul16 tx_tone; + char name[12]; + u8 unknown2[19]; + u8 unknown3_1:4, + highpower:1, + unknown3_2:1, + wide:1, + unknown3_3:1; + u8 unknown4; +}; + +#seekto 0xC570; // Fixme +u8 skipflags[64]; +""" + + +SYSTEM_MEM_FORMAT = """ +#seekto 0x%(addr)x; +struct { + struct zoneinfo zoneinfo; + struct memory memories[%(count)i]; +} zone%(index)i; +""" + +STARTUP_MODES = ['Text', 'Clock'] + +VOLUMES = OrderedDict([(str(x), x) for x in range(0, 30)]) +VOLUMES.update({'Selectable': 0x30, + 'Current': 0xFF}) +VOLUMES_REV = {v: k for k, v in VOLUMES.items()} + +MIN_VOL_PRESET = {'Preset': 0x30, + 'Lowest Limit': 0x31} +MIN_VOL_PRESET_REV = {v: k for k, v in MIN_VOL_PRESET.items()} + +SUBLCD = ['Zone Number', 'CH/GID Number', 'OSD List Number'] +CLOCKFMT = ['12H', '24H'] +DATEFMT = ['Day/Month', 'Month/Day'] +MICSENSE = ['On'] +ONLY_MOBILE_SETTINGS = ['power_switch_memory', 'off_hook_decode', + 'ignition_sense', 'mvp', 'it', 'ignition_mode'] + + +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("High", watts=50)] + + +def set_choice(setting, obj, key, choices, default='Off'): + settingstr = str(setting.value) + if settingstr == default: + val = 0xFF + else: + val = choices.index(settingstr) + 0x30 + setattr(obj, key, val) + + +def get_choice(obj, key, choices, default='Off'): + val = getattr(obj, key) + if val == 0xFF: + return default + else: + return choices[val - 0x30] + + +def make_frame(cmd, addr, data=b""): + return struct.pack(">BH", ord(cmd), addr) + data + + +def send(radio, frame): + # LOG.debug("%04i P>R:\n%s" % (len(frame), util.hexprint(frame))) + radio.pipe.write(frame) + + +def do_ident(radio): + radio.pipe.baudrate = 9600 + radio.pipe.stopbits = 2 + radio.pipe.timeout = 1 + send(radio, b'PROGRAM') + ack = radio.pipe.read(1) + LOG.debug('Read %r from radio' % ack) + if ack != b'\x16': + raise errors.RadioError('Radio refused hi-speed program mode') + radio.pipe.baudrate = 19200 + ack = radio.pipe.read(1) + if ack != b'\x06': + raise errors.RadioError('Radio refused program mode') + radio.pipe.write(b'\x02') + ident = radio.pipe.read(8) + LOG.debug('Radio ident is %r' % ident) + radio.pipe.write(b'\x06') + ack = radio.pipe.read(1) + if ack != b'\x06': + raise errors.RadioError('Radio refused program mode') + if ident[:6] not in (radio._model,): + model = ident[:5].decode() + variants = {b'\x06': 'K, K1, K3 (450-520MHz)', + b'\x07': 'K2, K4 (400-470MHz)'} + if model == 'P3180': + model += ' ' + variants.get(ident[5], '(Unknown)') + raise errors.RadioError('Unsupported radio model %s' % model) + + +def checksum_data(data): + _chksum = 0 + for byte in data: + _chksum = (_chksum + byte) & 0xFF + return _chksum + + +def do_download(radio): + do_ident(radio) + + data = bytes() + + def status(): + status = chirp_common.Status() + status.cur = len(data) + status.max = radio._memsize + status.msg = "Cloning from radio" + radio.status_fn(status) + LOG.debug('Radio address 0x%04x' % len(data)) + + # Addresses 0x0000-0xBF00 pulled by block number (divide by 0x100) + for block in range(0, 0xBF + 1): + send(radio, make_frame('R', block)) + cmd = radio.pipe.read(1) + chunk = b'' + if cmd == b'Z': + data += bytes(b'\xff' * 256) + LOG.debug('Radio reports empty block %02x' % block) + elif cmd == b'W': + chunk = bytes(radio.pipe.read(256)) + if len(chunk) != 256: + LOG.error('Received %i for block %02x' % (len(chunk), block)) + raise errors.RadioError('Radio did not send block') + data += chunk + else: + LOG.error('Radio sent %r (%02x), expected W(0x57)' % (cmd, + chr(cmd))) + raise errors.RadioError('Radio sent unexpected response') + + LOG.debug('Read block index %02x' % block) + status() + + chksum = radio.pipe.read(1) + if len(chksum) != 1: + LOG.error('Checksum was %r' % chksum) + raise errors.RadioError('Radio sent invalid checksum') + _chksum = checksum_data(chunk) + + if chunk and _chksum != ord(chksum): + LOG.error( + 'Checksum failed for %i byte block 0x%02x: %02x != %02x' % ( + len(chunk), block, _chksum, ord(chksum))) + raise errors.RadioError('Checksum failure while reading block. ' + 'Check serial cable.') + + radio.pipe.write(b'\x06') + if radio.pipe.read(1) != b'\x06': + raise errors.RadioError('Post-block exchange failed') + + # Addresses 0xC000 - 0xD1F0 pulled by address + for block in range(0x0100, 0x1200, 0x40): + send(radio, make_frame('S', block, b'\x40')) + x = radio.pipe.read(1) + if x != b'X': + raise errors.RadioError('Radio did not send block') + chunk = radio.pipe.read(0x40) + data += chunk + + LOG.debug('Read memory address %04x' % block) + status() + + radio.pipe.write(b'\x06') + if radio.pipe.read(1) != b'\x06': + raise errors.RadioError('Post-block exchange failed') + + radio.pipe.write(b'E') + if radio.pipe.read(1) != b'\x06': + raise errors.RadioError('Radio failed to acknowledge completion') + + LOG.debug('Read %i bytes total' % len(data)) + return data + + +def do_upload(radio): + do_ident(radio) + + def status(addr): + status = chirp_common.Status() + status.cur = addr + status.max = radio._memsize + status.msg = "Cloning to radio" + radio.status_fn(status) + + for block in range(0, 0xBF + 1): + addr = block * 0x100 + chunk = bytes(radio._mmap[addr:addr + 0x100]) + if all(byte == b'\xff' for byte in chunk): + LOG.debug('Sending zero block %i, range 0x%04x' % (block, addr)) + send(radio, make_frame('Z', block, b'\xFF')) + else: + checksum = checksum_data(chunk) + send(radio, make_frame('W', block, chunk + chr(checksum))) + + ack = radio.pipe.read(1) + if ack != b'\x06': + LOG.error('Radio refused block 0x%02x with %r' % (block, ack)) + raise errors.RadioError('Radio refused data block') + + status(addr) + + addr_base = 0xC000 + for addr in range(addr_base, radio._memsize, 0x40): + block_addr = addr - addr_base + 0x0100 + chunk = radio._mmap[addr:addr + 0x40] + send(radio, make_frame('X', block_addr, b'\x40' + chunk)) + + ack = radio.pipe.read(1) + if ack != b'\x06': + LOG.error('Radio refused address 0x%02x with %r' % (block_addr, + ack)) + raise errors.RadioError('Radio refused data block') + + status(addr) + + radio.pipe.write(b'E') + if radio.pipe.read(1) != b'\x06': + raise errors.RadioError('Radio failed to acknowledge completion') + + +def reset(self): + try: + self.pipe.baudrate = 9600 + self.pipe.write(b'E') + time.sleep(0.5) + self.pipe.baudrate = 19200 + self.pipe.write(b'E') + except Exception: + LOG.error('Unable to send reset sequence') + + +class KenwoodTKx180Radio(chirp_common.CloneModeRadio): + """Kenwood TK-x180""" + VENDOR = 'Kenwood' + MODEL = 'TK-x180' + BAUD_RATE = 9600 + NEEDS_COMPAT_SERIAL = False + + _system_start = 0x0B00 + _memsize = 0xD100 + + def __init__(self, *a, **k): + self._zones = [] + chirp_common.CloneModeRadio.__init__(self, *a, **k) + + def sync_in(self): + try: + data = do_download(self) + self._mmap = memmap.MemoryMapBytes(data) + except errors.RadioError: + reset(self) + raise + except Exception as e: + reset(self) + LOG.exception('General failure') + raise errors.RadioError('Failed to download from radio: %s' % e) + self.process_mmap() + + def sync_out(self): + try: + do_upload(self) + except Exception as e: + reset(self) + LOG.exception('General failure') + raise errors.RadioError('Failed to upload to radio: %s' % e) + + @property + def is_portable(self): + return self._model.startswith(b'P') + + def probe_layout(self): + start_addrs = [] + tmp_format = '#seekto 0x0A00; ul16 zone_starts[128];' + mem = bitwise.parse(tmp_format, self._mmap) + zone_format = """struct zoneinfo { + u8 number; + u8 zonetype; + u8 unknown1[2]; + u8 count; + char name[12]; + u8 unknown2[15]; + };""" + + zone_addresses = [] + for i in range(0, 128): + if mem.zone_starts[i] == 0xFFFF: + break + zone_addresses.append(mem.zone_starts[i]) + zone_format += '#seekto 0x%x; struct zoneinfo zone%i;' % ( + mem.zone_starts[i], i) + + zoneinfo = bitwise.parse(zone_format, self._mmap) + zones = [] + for i, addr in enumerate(zone_addresses): + zone = getattr(zoneinfo, 'zone%i' % i) + if zone.zonetype != 0x31: + LOG.error('Zone %i is type 0x%02x; ' + 'I only support 0x31 (conventional)') + raise errors.RadioError( + 'Unsupported non-conventional zone found in radio; ' + 'Refusing to load to safeguard your data!') + zones.append((addr, zone.count)) + + LOG.debug('Zones: %s' % zones) + return zones + + def process_mmap(self): + self._zones = self.probe_layout() + + mem_format = HEADER_FORMAT + for index, (addr, count) in enumerate(self._zones): + mem_format += '\n\n' + ( + SYSTEM_MEM_FORMAT % { + 'addr': addr, + 'count': max(count, 2), # bitwise bug, one-element array + 'index': index}) + + self._memobj = bitwise.parse(mem_format, self._mmap) + + def expand_mmap(self, zone_sizes): + """Remap memory into zones of the specified sizes, copying things + around to keep the contents, as appropriate.""" + old_zones = self._zones + old_memobj = self._memobj + + self._mmap = memmap.MemoryMapBytes(bytes(self._mmap.get_packed())) + + new_format = HEADER_FORMAT + addr = self._system_start + self._zones = [] + for index, count in enumerate(zone_sizes): + new_format += SYSTEM_MEM_FORMAT % { + 'addr': addr, + 'count': max(count, 2), # bitwise bug + 'index': index} + self._zones.append((addr, count)) + addr += 0x20 + (count * 0x30) + + self._memobj = bitwise.parse(new_format, self._mmap) + + # Set all known zone addresses and clear the rest + for index in range(0, 128): + try: + self._memobj.zone_starts[index] = self._zones[index][0] + except IndexError: + self._memobj.zone_starts[index] = 0xFFFF + + for zone_number, count in enumerate(zone_sizes): + dest_zone = getattr(self._memobj, 'zone%i' % zone_number) + dest = dest_zone.memories + dest_zoneinfo = dest_zone.zoneinfo + + if zone_number < len(old_zones): + LOG.debug('Copying existing zone %i' % zone_number) + _, old_count = old_zones[zone_number] + source_zone = getattr(old_memobj, 'zone%i' % zone_number) + source = source_zone.memories + source_zoneinfo = source_zone.zoneinfo + + if old_count != count: + LOG.debug('Zone %i going from %i to %i' % (zone_number, + old_count, + count)) + + # Copy the zone record from the source, but then update + # the count + dest_zoneinfo.set_raw(source_zoneinfo.get_raw()) + dest_zoneinfo.count = count + + source_i = 0 + for dest_i in range(0, min(count, old_count)): + dest[dest_i].set_raw(source[dest_i].get_raw()) + else: + LOG.debug('New zone %i' % zone_number) + dest_zone.zoneinfo.number = zone_number + 1 + dest_zone.zoneinfo.zonetype = 0x31 + dest_zone.zoneinfo.count = count + dest_zone.zoneinfo.name = ( + 'Zone %i' % (zone_number + 1)).ljust(12) + + def shuffle_zone(self): + """Sort the memories in the zone according to logical channel number""" + # FIXME: Move this to the zone + raw_memories = self.raw_memories + memories = [(i, raw_memories[i].number) + for i in range(0, self.raw_zoneinfo.count)] + current = memories[:] + memories.sort(key=lambda t: t[1]) + if current == memories: + LOG.debug('Shuffle not required') + return + raw_data = [raw_memories[i].get_raw() for i, n in memories] + for i, raw_mem in enumerate(raw_data): + raw_memories[i].set_raw(raw_mem) + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = ('This radio is zone-based, which is different from how ' + 'most radios work (that CHIRP supports). The zone count ' + 'can be adjusted in the Settings tab, but you must save ' + 'and re-load the file after changing that value in order ' + 'to be able to add/edit memories there.') + rp.experimental = ('This driver is very experimental. Every attempt ' + 'has been made to be overly pedantic to avoid ' + 'destroying data. However, you should use caution, ' + 'maintain backups, and proceed at your own risk.') + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_ctone = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_settings = True + rf.has_bank = False + rf.has_sub_devices = True + rf.has_rx_dtcs = True + rf.can_odd_split = True + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS', + 'DTCS->Tone', '->Tone', 'DTCS->DTCS'] + rf.valid_bands = self.VALID_BANDS + rf.valid_modes = ['FM', 'NFM'] + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 12.5, 10.0, 15.0, 20.0, + 25.0, 50.0, 100.0] + rf.valid_duplexes = ['', '-', '+', 'split', 'off'] + rf.valid_power_levels = POWER_LEVELS + rf.valid_name_length = 12 + rf.valid_characters = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789' + '!"#$%&\'()~+-,./:;<=>?@[\\]^`{}*| ') + rf.memory_bounds = (1, 512) + return rf + + @property + def raw_zone(self): + return getattr(self._memobj, 'zone%i' % self._zone) + + @property + def raw_zoneinfo(self): + return self.raw_zone.zoneinfo + + @property + def raw_memories(self): + return self.raw_zone.memories + + @property + def max_mem(self): + return self.raw_memories[self.raw_zoneinfo.count].number + + def _get_raw_memory(self, number): + for i in range(0, self.raw_zoneinfo.count): + if self.raw_memories[i].number == number: + return self.raw_memories[i] + return None + + def get_raw_memory(self, number): + return repr(self._get_raw_memory(number)) + + @staticmethod + def _decode_tone(toneval): + # DCS examples: + # D024N - 2814 - 0010 1000 0001 0100 + # ^--DCS + # D024I - A814 - 1010 1000 0001 0100 + # ^----inverted + # D754I - A9EC - 1010 1001 1110 1100 + # code in octal-------^^^^^^^^^^^ + + pol = toneval & 0x8000 and 'R' or 'N' + if toneval == 0xFFFF: + return '', None, None + elif toneval & 0x2000: + # DTCS + code = int('%o' % (toneval & 0x1FF)) + return 'DTCS', code, pol + else: + return 'Tone', toneval / 10.0, None + + @staticmethod + def _encode_tone(mode, val, pol): + if not mode: + return 0xFFFF + elif mode == 'Tone': + return int(val * 10) + elif mode == 'DTCS': + code = int('%i' % val, 8) + code |= 0x2000 + if pol == 'R': + code |= 0x8000 + return code + else: + raise errors.RadioError('Unsupported tone mode %r' % mode) + + def get_memory(self, number): + mem = chirp_common.Memory() + mem.number = number + _mem = self._get_raw_memory(number) + if _mem is None: + mem.empty = True + return mem + + mem.name = str(_mem.name).rstrip('\x00') + mem.freq = int(_mem.rx_freq) * 10 + chirp_common.split_tone_decode(mem, + self._decode_tone(_mem.tx_tone), + self._decode_tone(_mem.rx_tone)) + if _mem.wide: + mem.mode = 'FM' + else: + mem.mode = 'NFM' + + mem.power = POWER_LEVELS[_mem.highpower] + + offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10 + if offset == 0: + mem.duplex = '' + elif abs(offset) < 10000000: + mem.duplex = offset < 0 and '-' or '+' + mem.offset = abs(offset) + else: + mem.duplex = 'split' + mem.offset = int(_mem.tx_freq) * 10 + + skipbyte = self._memobj.skipflags[(mem.number - 1) // 8] + skipbit = skipbyte & (1 << (mem.number - 1) % 8) + mem.skip = skipbit and 'S' or '' + + return mem + + def set_memory(self, mem): + _mem = self._get_raw_memory(mem.number) + if _mem is None: + LOG.debug('Need to expand zone %i' % self._zone) + + # Calculate the new zone sizes and remap memory + new_zones = [x[1] for x in self._parent._zones] + new_zones[self._zone] = new_zones[self._zone] + 1 + self._parent.expand_mmap(new_zones) + + # Assign the new memory (at the end) to the desired + # number + _mem = self.raw_memories[self.raw_zoneinfo.count - 1] + _mem.number = mem.number + + # Sort the memory into place + self.shuffle_zone() + + # Now find it in the right spot + _mem = self._get_raw_memory(mem.number) + if _mem is None: + raise errors.RadioError('Internal error after ' + 'memory allocation') + + # Default values for unknown things + _mem.unknown1[0] = 0x36 + _mem.unknown1[1] = 0x36 + _mem.unknown2 = [0xFF for i in range(0, 19)] + _mem.unknown3_1 = 0xF + _mem.unknown3_2 = 0x1 + _mem.unknown3_3 = 0x0 + _mem.unknown4 = 0xFF + + if mem.empty: + LOG.debug('Need to shrink zone %i' % self._zone) + # Make the memory sort to the end, and sort the zone + _mem.number = 0xFF + self.shuffle_zone() + + # Calculate the new zone sizes and remap memory + new_zones = [x[1] for x in self._parent._zones] + new_zones[self._zone] = new_zones[self._zone] - 1 + self._parent.expand_mmap(new_zones) + return + + _mem.name = mem.name[:12].encode().rstrip().ljust(12, b'\x00') + _mem.rx_freq = mem.freq // 10 + + txtone, rxtone = chirp_common.split_tone_encode(mem) + _mem.tx_tone = self._encode_tone(*txtone) + _mem.rx_tone = self._encode_tone(*rxtone) + + _mem.wide = mem.mode == 'FM' + _mem.highpower = mem.power == POWER_LEVELS[1] + + if mem.duplex == '': + _mem.tx_freq = mem.freq // 10 + elif mem.duplex == 'split': + _mem.tx_freq = mem.offset // 10 + elif mem.duplex == 'off': + _mem.tx_freq.set_raw(b'\xff\xff\xff\xff') + elif mem.duplex == '-': + _mem.tx_freq = (mem.freq - mem.offset) // 10 + elif mem.duplex == '+': + _mem.tx_freq = (mem.freq + mem.offset) // 10 + else: + raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex) + + skipbyte = self._memobj.skipflags[(mem.number - 1) // 8] + if mem.skip == 'S': + skipbyte |= (1 << (mem.number - 1) % 8) + else: + skipbyte &= ~(1 << (mem.number - 1) % 8) + + def _pure_choice_setting(self, settings_key, name, choices, default='Off'): + if default is not None: + ui_choices = [default] + choices + else: + ui_choices = choices + s = RadioSetting( + settings_key, name, + RadioSettingValueList( + ui_choices, + get_choice(self._memobj.settings, settings_key, + choices, default))) + s.set_apply_callback(set_choice, self._memobj.settings, + settings_key, choices, default) + return s + + def _inverted_flag_setting(self, key, name, obj=None): + if obj is None: + obj = self._memobj.settings + + def apply_inverted(setting, key): + setattr(obj, key, not int(setting.value)) + + v = not getattr(obj, key) + s = RadioSetting( + key, name, + RadioSettingValueBoolean(v)) + s.set_apply_callback(apply_inverted, key) + return s + + def _get_common1(self): + settings = self._memobj.settings + common1 = RadioSettingGroup('common1', 'Common 1') + + common1.append(self._pure_choice_setting('sublcd', + 'Sub LCD Display', + SUBLCD, + default='None')) + + def apply_clockfmt(setting): + settings.clockfmt = CLOCKFMT.index(str(setting.value)) + + clockfmt = RadioSetting( + 'clockfmt', 'Clock Format', + RadioSettingValueList(CLOCKFMT, + CLOCKFMT[settings.clockfmt])) + clockfmt.set_apply_callback(apply_clockfmt) + common1.append(clockfmt) + + def apply_datefmt(setting): + settings.datefmt = DATEFMT.index(str(setting.value)) + + datefmt = RadioSetting( + 'datefmt', 'Date Format', + RadioSettingValueList(DATEFMT, + DATEFMT[settings.datefmt])) + datefmt.set_apply_callback(apply_datefmt) + common1.append(datefmt) + + common1.append(self._pure_choice_setting('micsense', + 'Mic Sense High', + MICSENSE)) + + def apply_modereset(setting): + val = int(setting.value) + if val == 0: + val = 0xFFFF + settings.modereset = val + + _modereset = int(settings.modereset) + if _modereset == 0xFFFF: + _modereset = 0 + modereset = RadioSetting( + 'modereset', 'Mode Reset Timer', + RadioSettingValueInteger(0, 300, _modereset)) + modereset.set_apply_callback(apply_modereset) + common1.append(modereset) + + inverted_flags = [('power_switch_memory', 'Power Switch Memory'), + ('scrambler_memory', 'Scrambler Memory'), + ('off_hook_decode', 'Off-Hook Decode'), + ('ssi', 'Signal Strength Indicator'), + ('ignition_sense', 'Ingnition Sense')] + for key, name in inverted_flags: + if self.is_portable and key in ONLY_MOBILE_SETTINGS: + # Skip settings that are not valid for portables + continue + common1.append(self._inverted_flag_setting(key, name)) + + if not self.is_portable and 'ignition_mode' in ONLY_MOBILE_SETTINGS: + common1.append(self._pure_choice_setting('ignition_mode', + 'Ignition Mode', + ['Ignition & SW', + 'Ignition Only'], + None)) + + def apply_it(setting): + settings.ignition_time = int(setting.value) / 600 + + _it = int(settings.ignition_time) * 600 + it = RadioSetting( + 'it', 'Ignition Timer (s)', + RadioSettingValueInteger(10, 28800, _it)) + it.set_apply_callback(apply_it) + if not self.is_portable and 'it' in ONLY_MOBILE_SETTINGS: + common1.append(it) + + return common1 + + def _get_common2(self): + settings = self._memobj.settings + common2 = RadioSettingGroup('common2', 'Common 2') + + def apply_ponmsgtext(setting): + settings.pon_msgtext = ( + str(setting.value)[:12].strip().ljust(12, '\x00')) + + common2.append( + self._pure_choice_setting('pon_msgtype', 'Power On Message Type', + STARTUP_MODES)) + + _text = str(settings.pon_msgtext).rstrip('\x00') + text = RadioSetting('settings.pon_msgtext', + 'Power On Text', + RadioSettingValueString( + 0, 12, _text)) + text.set_apply_callback(apply_ponmsgtext) + common2.append(text) + + def apply_volume(setting, key): + setattr(settings, key, VOLUMES[str(setting.value)]) + + volumes = {'poweron_tone': 'Power-on Tone', + 'control_tone': 'Control Tone', + 'warning_tone': 'Warning Tone', + 'alert_tone': 'Alert Tone', + 'sidetone': 'Sidetone', + 'locator_tone': 'Locator Tone'} + for value, name in volumes.items(): + setting = getattr(settings, value) + volume = RadioSetting('settings.%s' % value, name, + RadioSettingValueList( + VOLUMES.keys(), + VOLUMES_REV.get(int(setting), 0))) + volume.set_apply_callback(apply_volume, value) + common2.append(volume) + + def apply_vol_level(setting, key): + setattr(settings, key, int(setting.value)) + + levels = {'lo_volume': 'Low Volume Level (Fixed Volume)', + 'hi_volume': 'High Volume Level (Fixed Volume)', + 'min_volume': 'Minimum Audio Volume', + 'max_volume': 'Maximum Audio Volume'} + for value, name in levels.items(): + setting = getattr(settings, value) + if 'Audio' in name: + minimum = 0 + else: + minimum = 1 + volume = RadioSetting( + 'settings.%s' % value, name, + RadioSettingValueInteger(minimum, 31, int(setting))) + volume.set_apply_callback(apply_vol_level, value) + common2.append(volume) + + def apply_vo(setting): + val = int(setting.value) + if val < 0: + val = abs(val) | 0x80 + settings.tone_volume_offset = val + + _voloffset = int(settings.tone_volume_offset) + if _voloffset & 0x80: + _voloffset = abs(_voloffset & 0x7F) * -1 + voloffset = RadioSetting( + 'tvo', 'Tone Volume Offset', + RadioSettingValueInteger( + -5, 5, + _voloffset)) + voloffset.set_apply_callback(apply_vo) + common2.append(voloffset) + + def apply_mvp(setting): + settings.min_vol_preset = MIN_VOL_PRESET[str(setting.value)] + + _volpreset = int(settings.min_vol_preset) + volpreset = RadioSetting( + 'mvp', 'Minimum Volume Type', + RadioSettingValueList(MIN_VOL_PRESET.keys(), + MIN_VOL_PRESET_REV[_volpreset])) + volpreset.set_apply_callback(apply_mvp) + if not self.is_portable and 'mvp' in ONLY_MOBILE_SETTINGS: + common2.append(volpreset) + + return common2 + + def _get_conventional(self): + settings = self._memobj.settings + + conv = RadioSettingGroup('conv', 'Conventional') + inverted_flags = [('busy_led', 'Busy LED'), + ('ost_memory', 'OST Status Memory'), + ('tone_off', 'Tone Off'), + ('ptt_release', 'PTT Release tone'), + ('ptt_proceed', 'PTT Proceed Tone')] + for key, name in inverted_flags: + conv.append(self._inverted_flag_setting(key, name)) + + def apply_pttt(setting): + settings.ptt_timer = int(setting.value) + + pttt = RadioSetting( + 'pttt', 'PTT Proceed Tone Timer (ms)', + RadioSettingValueInteger(0, 6000, int(settings.ptt_timer))) + pttt.set_apply_callback(apply_pttt) + conv.append(pttt) + + self._get_ost(conv) + + return conv + + def _get_zones(self): + zones = RadioSettingGroup('zones', 'Zones') + + zone_count = RadioSetting('_zonecount', + 'Number of Zones', + RadioSettingValueInteger( + 1, 128, len(self._zones))) + zone_count.set_doc('Number of zones in the radio. ' + 'Requires a save and re-load of the ' + 'file to take effect. Reducing this number ' + 'will DELETE memories in affected zones!') + zones.append(zone_count) + + for i in range(len(self._zones)): + zone = RadioSettingGroup('zone%i' % i, 'Zone %i' % (i + 1)) + + _zone = getattr(self._memobj, 'zone%i' % i).zoneinfo + _name = str(_zone.name).rstrip('\x00') + name = RadioSetting('name%i' % i, 'Name', + RadioSettingValueString(0, 12, _name)) + zone.append(name) + + def apply_timer(setting, key): + val = int(setting.value) + if val == 0: + val = 0xFFFF + setattr(_zone, key, val) + + def collapse(val): + val = int(val) + if val == 0xFFFF: + val = 0 + return val + + timer = RadioSetting( + 'timeout', 'Time-out Timer', + RadioSettingValueInteger(15, 1200, collapse(_zone.timeout))) + timer.set_apply_callback(apply_timer, 'timeout') + zone.append(timer) + + timer = RadioSetting( + 'tot_alert', 'TOT Pre-Alert', + RadioSettingValueInteger(0, 10, collapse(_zone.tot_alert))) + timer.set_apply_callback(apply_timer, 'tot_alert') + zone.append(timer) + + timer = RadioSetting( + 'tot_rekey', 'TOT Re-Key Time', + RadioSettingValueInteger(0, 60, collapse(_zone.tot_rekey))) + timer.set_apply_callback(apply_timer, 'tot_rekey') + zone.append(timer) + + timer = RadioSetting( + 'tot_reset', 'TOT Reset Time', + RadioSettingValueInteger(0, 15, collapse(_zone.tot_reset))) + timer.set_apply_callback(apply_timer, 'tot_reset') + zone.append(timer) + + zone.append(self._inverted_flag_setting( + 'bcl_override', 'BCL Override', + _zone)) + + zones.append(zone) + + return zones + + def _get_ost(self, parent): + tones = chirp_common.TONES[:] + + def apply_tone(setting, index, which): + if str(setting.value) == 'Off': + val = 0xFFFF + else: + val = int(float(str(setting.value)) * 10) + setattr(self._memobj.ost_tones[index], '%stone' % which, val) + + def _tones(): + return ['Off'] + [str(x) for x in tones] + + for i in range(0, 40): + _ost = self._memobj.ost_tones[i] + ost = RadioSettingGroup('ost%i' % i, + 'OST %i' % (i + 1)) + + cur = str(_ost.name).rstrip('\x00') + name = RadioSetting('name%i' % i, 'Name', + RadioSettingValueString(0, 12, cur)) + ost.append(name) + + if _ost.rxtone == 0xFFFF: + cur = 'Off' + else: + cur = round(int(_ost.rxtone) / 10.0, 1) + if cur not in tones: + LOG.debug('Non-standard OST rx tone %i %s' % (i, cur)) + tones.append(cur) + tones.sort() + rx = RadioSetting('rxtone%i' % i, 'RX Tone', + RadioSettingValueList(_tones(), + str(cur))) + rx.set_apply_callback(apply_tone, i, 'rx') + ost.append(rx) + + if _ost.txtone == 0xFFFF: + cur = 'Off' + else: + cur = round(int(_ost.txtone) / 10.0, 1) + if cur not in tones: + LOG.debug('Non-standard OST tx tone %i %s' % (i, cur)) + tones.append(cur) + tones.sort() + tx = RadioSetting('txtone%i' % i, 'TX Tone', + RadioSettingValueList(_tones(), + str(cur))) + tx.set_apply_callback(apply_tone, i, 'tx') + ost.append(tx) + + parent.append(ost) + + def get_settings(self): + settings = self._memobj.settings + + zones = self._get_zones() + common1 = self._get_common1() + common2 = self._get_common2() + conv = self._get_conventional() + top = RadioSettings(zones, common1, common2, conv) + return top + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + elif element.get_name() == '_zonecount': + new_zone_count = int(element.value) + zone_sizes = [x[1] for x in self._zones[:new_zone_count]] + if len(self._zones) > new_zone_count: + self.expand_mmap(zone_sizes[:new_zone_count]) + elif len(self._zones) < new_zone_count: + self.expand_mmap(zone_sizes + ( + [0] * (new_zone_count - len(self._zones)))) + elif element.has_apply_callback(): + element.run_apply_callback() + + def get_sub_devices(self): + zones = [] + for i, _ in enumerate(self._zones): + zone = getattr(self._memobj, 'zone%i' % i) + + class _Zone(KenwoodTKx180RadioZone): + VENDOR = self.VENDOR + MODEL = self.MODEL + VALID_BANDS = self.VALID_BANDS + VARIANT = 'Zone %s' % ( + str(zone.zoneinfo.name).rstrip('\x00').rstrip()) + _model = self._model + + zones.append(_Zone(self, i)) + return zones + + +class KenwoodTKx180RadioZone(KenwoodTKx180Radio): + _zone = None + + def __init__(self, parent, zone=0): + if isinstance(parent, KenwoodTKx180Radio): + self._parent = parent + else: + LOG.warning('Parent was not actually our parent, expect failure') + self._zone = zone + + @property + def _zones(self): + return self._parent._zones + + @property + def _memobj(self): + return self._parent._memobj + + def load_mmap(self, filename): + self._parent.load_mmap(filename) + + def get_features(self): + rf = KenwoodTKx180Radio.get_features(self) + rf.has_sub_devices = False + rf.memory_bounds = (1, 250) + return rf + + def get_sub_devices(self): + return [] + + +if has_future: + @directory.register + class KenwoodTK7180Radio(KenwoodTKx180Radio): + MODEL = 'TK-7180' + VALID_BANDS = [(136000000, 174000000)] + _model = b'M7180\x04' + + @directory.register + class KenwoodTK8180Radio(KenwoodTKx180Radio): + MODEL = 'TK-8180' + VALID_BANDS = [(400000000, 520000000)] + _model = b'M8180\x06' + + @directory.register + class KenwoodTK2180Radio(KenwoodTKx180Radio): + MODEL = 'TK-2180' + VALID_BANDS = [(136000000, 174000000)] + _model = b'P2180\x04' + + # K1,K3 are technically 450-470 (K3 == keypad) + @directory.register + class KenwoodTK3180K1Radio(KenwoodTKx180Radio): + MODEL = 'TK-3180K' + VALID_BANDS = [(400000000, 520000000)] + _model = b'P3180\x06' + + # K2,K4 are technically 400-470 (K4 == keypad) + @directory.register + class KenwoodTK3180K2Radio(KenwoodTKx180Radio): + MODEL = 'TK-3180K2' + VALID_BANDS = [(400000000, 520000000)] + _model = b'P3180\x07' diff --git a/chirp/drivers/tmv71.py b/chirp/drivers/tmv71.py new file mode 100644 index 0000000..03e0ef3 --- /dev/null +++ b/chirp/drivers/tmv71.py @@ -0,0 +1,79 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, errors, util +from chirp.drivers import tmv71_ll +import logging + +LOG = logging.getLogger(__name__) + + +class TMV71ARadio(chirp_common.CloneModeRadio): + BAUD_RATE = 9600 + VENDOR = "Kenwood" + MODEL = "TM-V71A" + + mem_upper_limit = 1022 + _memsize = 32512 + _model = "" # FIXME: REMOVE + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, 999) + return rf + + def _detect_baud(self): + for baud in [9600, 19200, 38400, 57600]: + self.pipe.baudrate = baud + self.pipe.write("\r\r") + self.pipe.read(32) + try: + id = tmv71_ll.get_id(self.pipe) + LOG.info("Radio %s at %i baud" % (id, baud)) + return True + except errors.RadioError: + pass + + raise errors.RadioError("No response from radio") + + def get_raw_memory(self, number): + return util.hexprint(tmv71_ll.get_raw_mem(self._mmap, number)) + + def get_special_locations(self): + return sorted(tmv71_ll.V71_SPECIAL.keys()) + + def get_memory(self, number): + if isinstance(number, str): + try: + number = tmv71_ll.V71_SPECIAL[number] + except KeyError: + raise errors.InvalidMemoryLocation("Unknown channel %s" % + number) + + return tmv71_ll.get_memory(self._mmap, number) + + def set_memory(self, mem): + return tmv71_ll.set_memory(self._mmap, mem) + + def erase_memory(self, number): + tmv71_ll.set_used(self._mmap, number, 0) + + def sync_in(self): + self._detect_baud() + self._mmap = tmv71_ll.download(self) + + def sync_out(self): + self._detect_baud() + tmv71_ll.upload(self) diff --git a/chirp/drivers/tmv71_ll.py b/chirp/drivers/tmv71_ll.py new file mode 100644 index 0000000..50a100b --- /dev/null +++ b/chirp/drivers/tmv71_ll.py @@ -0,0 +1,391 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import struct +import time +import logging + +from chirp import memmap, chirp_common, errors + +LOG = logging.getLogger(__name__) + +POS_MODE = 5 +POS_DUP = 6 +POS_TMODE = 6 +POS_RTONE = 7 +POS_CTONE = 8 +POS_DTCS = 9 +POS_OFFSET = 10 + +MEM_LOC_BASE = 0x1700 +MEM_LOC_SIZE = 16 +MEM_TAG_BASE = 0x5800 +MEM_FLG_BASE = 0x0E00 + +V71_SPECIAL = {} + +for i in range(0, 10): + V71_SPECIAL["L%i" % i] = 1000 + (i * 2) + V71_SPECIAL["U%i" % i] = 1000 + (i * 2) + 1 +for i in range(0, 10): + V71_SPECIAL["WX%i" % (i + 1)] = 1020 + i +V71_SPECIAL["C VHF"] = 1030 +V71_SPECIAL["C UHF"] = 1031 + +V71_SPECIAL_REV = {} +for k, v in V71_SPECIAL.items(): + V71_SPECIAL_REV[v] = k + + +def command(s, cmd, timeout=0.5): + start = time.time() + + data = "" + LOG.debug("PC->V71: %s" % cmd) + s.write(cmd + "\r") + while not data.endswith("\r") and (time.time() - start) < timeout: + data += s.read(1) + LOG.debug("V71->PC: %s" % data.strip()) + return data.strip() + + +def get_id(s): + r = command(s, "ID") + if r.startswith("ID "): + return r.split(" ")[1] + else: + raise errors.RadioError("No response to ID command") + +EXCH_R = "R\x00\x00\x00" +EXCH_W = "W\x00\x00\x00" + + +def read_block(s, block, count=256): + s.write(struct.pack(" (max(V71_SPECIAL.values()) + 1): + raise errors.InvalidMemoryLocation("Number must be between 0 and 999") + + mem = chirp_common.Memory() + mem.number = number + + if number > 999: + mem.extd_number = V71_SPECIAL_REV[number] + if not get_used(map, number): + mem.empty = True + return mem + + mmap = get_raw_mem(map, number) + + mem.freq = get_freq(mmap) + mem.name = get_name(map, number) + mem.tmode = get_tmode(mmap) + mem.rtone = get_tone(mmap, POS_RTONE) + mem.ctone = get_tone(mmap, POS_CTONE) + mem.dtcs = get_dtcs(mmap) + mem.duplex = get_duplex(mmap) + mem.offset = get_offset(mmap) + mem.mode = get_mode(mmap) + + if number < 999: + mem.skip = get_skip(map, number) + + if number > 999: + mem.immutable = ["number", "bank", "extd_number", "name"] + if number > 1020 and number < 1030: + mem.immutable += ["freq"] # FIXME: ALL + + return mem + + +def initialize(mmap): + mmap[0] = \ + "\x80\xc8\xb3\x08\x00\x01\x00\x08" + \ + "\x08\x00\xc0\x27\x09\x00\x00\xff" + + +def set_memory(map, mem): + if mem.number < 0 or mem.number > (max(V71_SPECIAL.values()) + 1): + raise errors.InvalidMemoryLocation("Number must be between 0 and 999") + + mmap = memmap.MemoryMap(get_raw_mem(map, mem.number)) + + if not get_used(map, mem.number): + initialize(mmap) + + set_freq(mmap, mem.freq) + if mem.number < 999: + set_name(map, mem.number, mem.name) + set_tmode(mmap, mem.tmode) + set_tone(mmap, mem.rtone, POS_RTONE) + set_tone(mmap, mem.ctone, POS_CTONE) + set_dtcs(mmap, mem.dtcs) + set_duplex(mmap, mem.duplex) + set_offset(mmap, mem.offset) + set_mode(mmap, mem.mode) + + base = get_mem_offset(mem.number) + map[base] = mmap.get_packed() + + set_used(map, mem.number, mem.freq) + if mem.number < 999: + set_skip(map, mem.number, mem.skip) + + return map + + +if __name__ == "__main__": + import sys + import serial + s = serial.Serial(port=sys.argv[1], baudrate=9600, dsrdtr=True, + timeout=0.25) + # s.write("\r\r") + # print get_id(s) + data = download(s) + file(sys.argv[2], "wb").write(data) diff --git a/chirp/drivers/ts2000.py b/chirp/drivers/ts2000.py new file mode 100644 index 0000000..f2160ad --- /dev/null +++ b/chirp/drivers/ts2000.py @@ -0,0 +1,296 @@ +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re +from chirp import chirp_common, directory, util, errors +from chirp.drivers import kenwood_live +from chirp.drivers.kenwood_live import KenwoodLiveRadio, \ + command, iserr, NOCACHE + +TS2000_SSB_STEPS = [1.0, 2.5, 5.0, 10.0] +TS2000_FM_STEPS = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] +TS2000_DUPLEX = dict(kenwood_live.DUPLEX) +TS2000_DUPLEX[3] = "=" +TS2000_DUPLEX[4] = "split" +TS2000_MODES = ["?", "LSB", "USB", "CW", "FM", "AM", + "FSK", "CWR", "?", "FSKR"] +TS2000_TMODES = ["", "Tone", "TSQL", "DTCS"] +TS2000_TONES = list(chirp_common.OLD_TONES) +TS2000_TONES.remove(69.3) + + +@directory.register +class TS2000Radio(KenwoodLiveRadio): + """Kenwood TS-2000""" + MODEL = "TS-2000" + + _upper = 289 + _kenwood_split = True + _kenwood_valid_tones = list(TS2000_TONES) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.has_bank = False + rf.can_odd_split = True + rf.valid_modes = ["LSB", "USB", "CW", "FM", "AM"] + rf.valid_tmodes = list(TS2000_TMODES) + rf.valid_tuning_steps = list(TS2000_SSB_STEPS + TS2000_FM_STEPS) + rf.valid_bands = [(1000, 1300000000)] + rf.valid_skips = ["", "S"] + rf.valid_duplexes = TS2000_DUPLEX.values() + + # TS-2000 uses ";" as a message separator even though it seems to + # allow you to to use all printable ASCII characters at the manual + # controls. The radio doesn't send the name after the ";" if you + # input one from the manual controls. + rf.valid_characters = chirp_common.CHARSET_ASCII.replace(';', '') + rf.valid_name_length = 7 # 7 character channel names + rf.memory_bounds = (0, self._upper) + return rf + + def _cmd_set_memory(self, number, spec): + return "MW0%03i%s" % (number, spec) + + def _cmd_set_split(self, number, spec): + return "MW1%03i%s" % (number, spec) + + def _cmd_get_memory(self, number): + return "MR0%03i" % number + + def _cmd_get_split(self, number): + return "MR1%03i" % number + + def _cmd_recall_memory(self, number): + return "MC%03i" % (number) + + def _cmd_cur_memory(self, number): + return "MC" + + def _cmd_erase_memory(self, number): + # write a memory channel that's effectively zeroed except + # for the channel number + return "MW%04i%035i" % (number, 0) + + def erase_memory(self, number): + if number not in self._memcache: + return + + resp = command(self.pipe, *self._cmd_erase_memory(number)) + if iserr(resp): + raise errors.RadioError("Radio refused delete of %i" % number) + del self._memcache[number] + + def get_memory(self, number): + if number < 0 or number > self._upper: + raise errors.InvalidMemoryLocation( + "Number must be between 0 and %i" % self._upper) + if number in self._memcache and not NOCACHE: + return self._memcache[number] + + result = command(self.pipe, *self._cmd_get_memory(number)) + if result == "N": + mem = chirp_common.Memory() + mem.number = number + mem.empty = True + self._memcache[mem.number] = mem + return mem + + mem = self._parse_mem_spec(result) + self._memcache[mem.number] = mem + + # check for split frequency operation + if mem.duplex == "" and self._kenwood_split: + result = command(self.pipe, *self._cmd_get_split(number)) + self._parse_split_spec(mem, result) + + return mem + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + # pad string so indexes match Kenwood docs + spec = " " + spec + + # use the same variable names as the Kenwood docs + # _p1 = spec[3] + _p2 = spec[4] + _p3 = spec[5:7] + _p4 = spec[7:18] + _p5 = spec[18] + _p6 = spec[19] + _p7 = spec[20] + _p8 = spec[21:23] + _p9 = spec[23:25] + _p10 = spec[25:28] + # _p11 = spec[28] + _p12 = spec[29] + _p13 = spec[30:39] + _p14 = spec[39:41] + # _p15 = spec[41] + _p16 = spec[42:49] + + mem.number = int(_p2 + _p3) # concat bank num and chan num + mem.freq = int(_p4) + mem.mode = TS2000_MODES[int(_p5)] + mem.skip = ["", "S"][int(_p6)] + mem.tmode = TS2000_TMODES[int(_p7)] + # PL and T-SQL are 1 indexed, DTCS is 0 indexed + mem.rtone = self._kenwood_valid_tones[int(_p8) - 1] + mem.ctone = self._kenwood_valid_tones[int(_p9) - 1] + mem.dtcs = chirp_common.DTCS_CODES[int(_p10)] + mem.duplex = TS2000_DUPLEX[int(_p12)] + mem.offset = int(_p13) # 9-digit + if mem.mode in ["AM", "FM"]: + mem.tuning_step = TS2000_FM_STEPS[int(_p14)] + else: + mem.tuning_step = TS2000_SSB_STEPS[int(_p14)] + mem.name = _p16 + + return mem + + def _parse_split_spec(self, mem, spec): + + # pad string so indexes match Kenwood docs + spec = " " + spec + + # use the same variable names as the Kenwood docs + split_freq = int(spec[7:18]) + if mem.freq != split_freq: + mem.duplex = "split" + mem.offset = split_freq + + return mem + + def set_memory(self, memory): + if memory.number < 0 or memory.number > self._upper: + raise errors.InvalidMemoryLocation( + "Number must be between 0 and %i" % self._upper) + + spec = self._make_mem_spec(memory) + spec = "".join(spec) + r1 = command(self.pipe, *self._cmd_set_memory(memory.number, spec)) + if not iserr(r1): + memory.name = memory.name.rstrip() + self._memcache[memory.number] = memory + + # if we're tuned to the channel, reload it + r1 = command(self.pipe, *self._cmd_cur_memory(memory.number)) + if not iserr(r1): + pattern = re.compile("MC([0-9]{3})") + match = pattern.search(r1) + if match is not None: + cur_mem = int(match.group(1)) + if cur_mem == memory.number: + cur_mem = \ + command(self.pipe, + *self._cmd_recall_memory(memory.number)) + else: + raise errors.InvalidDataError("Radio refused %i" % memory.number) + + # FIXME + if memory.duplex == "split" and self._kenwood_split: + spec = "".join(self._make_split_spec(memory)) + result = command(self.pipe, *self._cmd_set_split(memory.number, + spec)) + if iserr(result): + raise errors.InvalidDataError("Radio refused %i" % + memory.number) + + def _make_mem_spec(self, mem): + if mem.duplex in " +-": + duplex = util.get_dict_rev(TS2000_DUPLEX, mem.duplex) + offset = mem.offset + elif mem.duplex == "split": + duplex = 0 + offset = 0 + else: + print "Bug: unsupported duplex `%s'" % mem.duplex + if mem.mode in ["AM", "FM"]: + step = TS2000_FM_STEPS.index(mem.tuning_step) + else: + step = TS2000_SSB_STEPS.index(mem.tuning_step) + + # TS-2000 won't accept channels with tone mode off if they have + # tone values + if mem.tmode == "": + rtone = 0 + ctone = 0 + dtcs = 0 + else: + # PL and T-SQL are 1 indexed, DTCS is 0 indexed + rtone = (self._kenwood_valid_tones.index(mem.rtone) + 1) + ctone = (self._kenwood_valid_tones.index(mem.ctone) + 1) + dtcs = (chirp_common.DTCS_CODES.index(mem.dtcs)) + + spec = ( + "%011i" % mem.freq, + "%i" % (TS2000_MODES.index(mem.mode)), + "%i" % (mem.skip == "S"), + "%i" % TS2000_TMODES.index(mem.tmode), + "%02i" % (rtone), + "%02i" % (ctone), + "%03i" % (dtcs), + "0", # REVERSE status + "%i" % duplex, + "%09i" % offset, + "%02i" % step, + "0", # Memory Group number (0-9) + "%s" % mem.name, + ) + + return spec + + def _make_split_spec(self, mem): + if mem.duplex in " +-": + duplex = util.get_dict_rev(TS2000_DUPLEX, mem.duplex) + elif mem.duplex == "split": + duplex = 0 + else: + print "Bug: unsupported duplex `%s'" % mem.duplex + if mem.mode in ["AM", "FM"]: + step = TS2000_FM_STEPS.index(mem.tuning_step) + else: + step = TS2000_SSB_STEPS.index(mem.tuning_step) + + # TS-2000 won't accept channels with tone mode off if they have + # tone values + if mem.tmode == "": + rtone = 0 + ctone = 0 + dtcs = 0 + else: + # PL and T-SQL are 1 indexed, DTCS is 0 indexed + rtone = (self._kenwood_valid_tones.index(mem.rtone) + 1) + ctone = (self._kenwood_valid_tones.index(mem.ctone) + 1) + dtcs = (chirp_common.DTCS_CODES.index(mem.dtcs)) + + spec = ( + "%011i" % mem.offset, + "%i" % (TS2000_MODES.index(mem.mode)), + "%i" % (mem.skip == "S"), + "%i" % TS2000_TMODES.index(mem.tmode), + "%02i" % (rtone), + "%02i" % (ctone), + "%03i" % (dtcs), + "0", # REVERSE status + "%i" % duplex, + "%09i" % 0, + "%02i" % step, + "0", # Memory Group number (0-9) + "%s" % mem.name, + ) + + return spec diff --git a/chirp/drivers/ts480.py b/chirp/drivers/ts480.py new file mode 100644 index 0000000..47b1757 --- /dev/null +++ b/chirp/drivers/ts480.py @@ -0,0 +1,1144 @@ +# Copyright 2019 Rick DeWitt +# Implementing mem as Clone Mode +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re +import math +import threading +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0000; +struct { // 20 bytes per chan + u32 rxfreq; + u32 txfreq; + u8 xmode; // param stored as CAT value + u8 tmode; + u8 rtone; + u8 ctone; + u8 skip; + u8 step; + char name[8]; +} ch_mem[110]; // 100 normal + 10 P-type + +struct { // 5 bytes each + u32 asfreq; + u8 asmode:4, // param stored as CAT value (0 -9) + asdata:2, + asnu:2; +} asf[32]; + +struct { // 10 x 5 4-byte frequencies + u32 ssfreq; +} ssf[50]; + +struct { + u8 ag; + u8 an; + u32 fa; + u32 fb; + u8 mf; + u8 mg; + u8 pc; + u8 rg; + u8 ty; +} settings; + +struct { // Menu A/B settings + char ex000; + u8 ex003; // These params stored as nibbles + u8 ex007; + u8 ex008; + u8 ex009; + u8 ex010; + u8 ex011; + u8 ex012; + u8 ex013; + u8 ex014; + u8 ex021; + u8 ex022; + u8 ex048; + u8 ex049; + u8 ex050; + u8 ex051; + u8 ex052; +} exset[2]; + + char mdl_name[9]; // appended model name, first 9 chars + +""" + +STIMEOUT = 0.6 +LOCK = threading.Lock() +BAUD = 0 # Initial baud rate +MEMSEL = 0 # Default Menu A +BEEPVOL = 5 # Default beep level +W8S = 0.01 # short wait, secs +W8L = 0.05 # long wait + +TS480_DUPLEX = ["", "-", "+"] +TS480_SKIP = ["", "S"] + +# start at 0:LSB +TS480_MODES = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", "FSK-R"] +EX_MODES = ["FSK-R", "CW-R"] +for ix in EX_MODES: + if ix not in chirp_common.MODES: + chirp_common.MODES.append(ix) + +TS480_TONES = list(chirp_common.TONES) +TS480_TONES.append(1750.0) + +TS480_BANDS = [(50000, 24999999), # VFO Rx range. TX has lockouts + (25000000, 59999999)] + +TS480_TUNE_STEPS = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5, + 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + +RADIO_IDS = { # From kenwood_live.py; used to report wrong radio + "ID019;": "TS-2000", + "ID009;": "TS-850", + "ID020:": "TS-480", + "ID021;": "TS-590S", + "ID023;": "TS-590SG" +} + + +def command(ser, cmd, rsplen, w8t=0.01, exts=""): + """Send @cmd to radio via @ser""" + # cmd is output string without ; terminator + # rsplen is expected response char count, including terminator + # If rsplen = 0 then do not read after write + + start = time.time() + # LOCK.acquire() + stx = cmd # preserve cmd for response check + stx = stx + exts + ";" # append arguments + ser.write(stx) + LOG.debug("PC->RADIO [%s]" % stx) + ts = time.time() # implement the wait after command + while (time.time() - ts) < w8t: + ix = 0 # NOP + result = "" + if rsplen > 0: # read response + result = ser.read(rsplen) + LOG.debug("RADIO->PC [%s]" % result) + result = result[:-1] # remove terminator + # LOCK.release() + return result.strip() + + +def _connect_radio(radio): + """Determine baud rate and verify radio on-line""" + global BAUD # Allows modification + bauds = [9600, 115200, 57600, 38400, 19200, 4800] + if BAUD > 0: + bauds.insert(0, BAUD) # Make the detected one first + # Flush the input buffer + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + + for bd in bauds: + radio.pipe.baudrate = bd + BAUD = bd + radio.pipe.write(";") + radio.pipe.write(";") + resp = radio.pipe.read(4) + radio.pipe.write("ID;") + resp = radio.pipe.read(6) + if resp == radio.ID: # Good comms + resp = command(radio.pipe, "AI0", 0, W8L) + return + elif resp in RADIO_IDS.keys(): + msg = "Radio reported as model %s, not %s!" % \ + (RADIO_IDS[resp], radio.MODEL) + raise errors.RadioError(msg) + raise errors.RadioError("No response from radio") + return + + +def read_str(radio, trm=";"): + """ Read chars until terminator """ + stq = "" + ctq = "" + while ctq != trm: + ctq = radio.pipe.read(1) + stq += ctq + LOG.debug(" + [%s]" % stq) + return stq[:-1] # Return without trm + + +def _read_mem(radio): + """Get the memory map""" + global BEEPVOL + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._upper + 10 # 10 P chans + status.msg = "Reading Channel Memory..." + radio.status_fn(status) + + result0 = command(radio.pipe, "EX0120000", 12, W8S) + BEEPVOL = int(result0[6:12]) + result0 = command(radio.pipe, "EX01200000", 0, W8L) # Silence beeps + data = "" + mrlen = 41 # Expected fixed return string length + for chn in range(0, (radio._upper + 11)): # Loop stops at +10 + # Request this mem chn + r0ch = 999 + r1ch = r0ch + # return results can come back out of order + while (r0ch != chn): + # simplex + result0 = command(radio.pipe, "MR0%03i" % chn, + mrlen, W8S) + result0 += read_str(radio) + r0ch = int(result0[3:6]) + while (r1ch != chn): + # split + result1 = command(radio.pipe, "MR1%03i" % chn, + mrlen, W8S) + result1 += read_str(radio) + r1ch = int(result1[3:6]) + data += radio._parse_mem_spec(result0, result1) + # UI Update + status.cur = chn + status.msg = "Reading Channel Memory..." + radio.status_fn(status) + + if len(data) == 0: # To satisfy run_tests + raise errors.RadioError('No data received.') + return data + + +def _make_dat(sx, nb): + """ Split the string sx into nb binary bytes """ + vx = int(sx) + dx = "" + if nb > 3: + dx += chr((vx >> 24) & 0xFF) + if nb > 2: + dx += chr((vx >> 16) & 0xFF) + if nb > 1: + dx += chr((vx >> 8) & 0xFF) + dx += chr(vx & 0xFF) + return dx + + +def _sets_val(stx, nv, nb): + """ Split string stx into nv nb-bit values in 1 byte """ + # Right now: hardcoded for nv:3 values of nb:2 bits each + v1 = int(stx[0]) << 6 + v1 = v1 | (int(stx[1]) << 4) + v1 = v1 | (int(stx[2]) << 2) + return chr(v1) + + +def _sets_asf(stx): + """ Process AS0 auto-mode setting """ + asm = _make_dat(stx[0:11], 4) # 11-bit freq + a1 = int(stx[11]) # 4-bit mode + a2 = 0 # not used in TS-480 + asm += chr((a1 << 4) | (a2 << 2)) + return asm + + +def my_val_list(setting, opts, obj, atrb, fix=0, ndx=-1): + """Callback:from ValueList. Set the integer index.""" + # This function is here to be available to get_mem and get_set + # fix is optional additive offset to the list index + # ndx is optional obj[ndx] array index + value = opts.index(str(setting.value)) + value += fix + if ndx >= 0: # indexed obj + setattr(obj[ndx], atrb, value) + else: + setattr(obj, atrb, value) + return + + +def _read_settings(radio): + """ Continue filling memory map""" + global MEMSEL + # setc: the list of CAT commands for downloaded settings + # Block paramters first. In the exact order of MEM_FORMAT + setc = radio.SETC + setc.extend(radio.EX) # Menu A EX params + setc.extend(radio.EX) # Menu B + status = chirp_common.Status() + status.cur = 0 + status.max = 32 + 50 + 8 + 17 + 17 + status.msg = "Reading Settings..." + radio.status_fn(status) + + setts = "" + nc = 0 + for cmc in setc: + skipme = False + argx = "" # Extended arguments + if cmc == "AS0": + skipme = True # flag to disable further processing + for ix in range(32): # 32 AS params + result0 = command(radio.pipe, cmc, 19, W8S, + "%02i" % ix) + xc = len(cmc) + 2 + result0 = result0[xc:] + setts += _sets_asf(result0) + nc += 1 + status.cur = nc + radio.status_fn(status) + elif cmc == "SS": + skipme = True + for ix in range(10): # 10 chans + for nx in range(5): # 5 spots + result0 = command(radio.pipe, cmc, 16, W8S, + "%1i%1i" % (ix, nx)) + setts += _make_dat(result0[4:], 4) + nc += 1 + status.cur = nc + radio.status_fn(status) + elif (cmc == "MF0") or (cmc == "MF1"): + result0 = command(radio.pipe, cmc, 0, W8S) + skipme = True # cmd only, no response + else: # issue the cmc cmd as-is with argx + if str(cmc).startswith("EX"): + argx = "0000" + result0 = command(radio.pipe, cmc, 0, W8S, argx) + result0 = read_str(radio) # various length responses + # strip the cmd echo + xc = len(cmc) + result0 = result0[xc:] + # Cmd has been sent, process the result + if (cmc == "FA") or (cmc == "FB"): # Response is 11-bit frq + skipme = True + setts += _make_dat(result0, 4) # 11-bit freq + elif (cmc == "MF0") or (cmc == "MF1"): # No stored response + skipme = True + # Generic single byte processing + if not skipme: + setts += chr(int(result0)) + if cmc == "MF": # Save the initial Menu selection + MEMSEL = int(result0) + nc += 1 + status.cur = nc + radio.status_fn(status) + setts += radio.MODEL.ljust(9) + # Now set the initial menu selection back + result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % MEMSEL) + # And the original Beep Volume + result0 = command(radio.pipe, "EX0120000%i" % BEEPVOL, 0, W8L) + return setts + + +def _write_mem(radio): + """ Send MW commands for each channel """ + global BEEPVOL + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._upper + 10 # 10 P chans + status.msg = "Writing Channel Memory" + radio.status_fn(status) + + result0 = command(radio.pipe, "EX0120000", 12, W8S) + BEEPVOL = int(result0[6:12]) + result0 = command(radio.pipe, "EX01200000", 0, W8L) # Silence beeps + + for chn in range(0, (radio._upper + 11)): # Loop stops at +20 + _mem = radio._memobj.ch_mem[chn] + cmx = "MW0%03i" % chn + stm = cmx + radio._make_base_spec(_mem, _mem.rxfreq) + result0 = command(radio.pipe, stm, 0, W8L) # No response + if _mem.txfreq > 0: # Don't write MW1 if empty/deleted + cmx = "MW1%03i" % chn + stm = cmx + radio._make_base_spec(_mem, _mem.txfreq) + result0 = command(radio.pipe, stm, 0, W8L) + status.cur = chn + radio.status_fn(status) + return + + +def _write_sets(radio): + """ Send settings and Menu a/b """ + status = chirp_common.Status() + status.cur = 0 + status.max = 124 # Total to send + status.msg = "Writing Settings" + radio.status_fn(status) + # Define mem struct shortcuts + _sets = radio._memobj.settings + _asf = radio._memobj.asf + _ssf = radio._memobj.ssf + _mex = radio._memobj.exset + snx = 0 # Settings status counter + stlen = 0 # No response count + # Send 32 AS + for ix in range(32): + scm = "AS0%02i%011i%1i%1i" % (ix, _asf[ix].asfreq, + _asf[ix].asmode, _asf[ix].asdata) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 1 + status.cur = snx + radio.status_fn(status) + # Send 50 SS + for ix in range(10): + for kx in range(5): + nx = ix * 5 + kx + scm = "SS%1i%1i%011i" % (ix, kx, _ssf[nx].ssfreq) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 1 + status.cur = snx + radio.status_fn(status) + # Send 8 thingies + scm = "AG0%03i" % _sets.ag + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "AN%1i" % _sets.an + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "FA%011i" % _sets.fa + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "FB%011i" % _sets.fb + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "MG%03i" % _sets.mg + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "PC%03i" % _sets.pc + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "RG%03i" % _sets.rg + result0 = command(radio.pipe, scm, stlen, W8S) + # TY cmd is firmware read-only + scm = "MF0" # Select menu A/B + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 8 + status.cur = snx + radio.status_fn(status) + # Send 17 Menu A EX + setc = radio.EX # list of EX cmds + for ix in range(2): + for cmx in setc: + if str(cmx)[0:2] == "MF": + scm = cmx + else: # The EX cmds + scm = "%s0000%i" % (cmx, getattr(_mex[ix], + cmx.lower())) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 1 + status.cur = snx + radio.status_fn(status) + # Now set the initial menu selection back + result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % _sets.mf) + # And the original Beep Volume + result0 = command(radio.pipe, "EX0120000%i" % BEEPVOL, 0, W8L) + return + + +@directory.register +class TS480Radio(chirp_common.CloneModeRadio): + """Kenwood TS-590""" + VENDOR = "Kenwood" + MODEL = "TS-480_CloneMode" + ID = "ID020;" + # Settings read/write cmd sequence list + SETC = ["AS0", "SS", "AG0", "AN", "FA", "FB", + "MF", "MG", "PC", "RG", "TY", "MF0"] + # This is the TS-590SG MENU A/B read_settings paramter tuple list + # The order is mandatory; to match the Mem_Format sequence + EX = ["EX000", "EX003", "EX007", "EX008", "EX009", "EX010", "EX011", + "EX012", "EX013", "EX014", "EX021", "EX022", "EX048", "EX049", + "EX050", "EX051", "EX052", "MF1"] + # EX menu settings label dictionary. Key is the EX number + EX_LBL = {0: " Display brightness", + 3: " Tuning control adj rate (Hz)", + 12: " Beep volume", + 13: " Sidetone volume", + 14: " Message playback volume", + 7: " Temporary MR Chan freq allowed", + 8: " Program Scan slowdown", + 9: " Program Scan slowdown range (Hz)", + 10: " Program Scan hold", + 11: " Scan Resume method", + 21: " TX Power fine adjust", + 22: " Timeout timer (Secs)", + 48: " Panel PF-A function", + 49: " MIC PF1 function", + 50: " MIC PF2 function", + 51: " MIC PF3 function", + 52: " MIC PF4 function"} + + _upper = 99 + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.can_odd_split = False + rf.has_bank = False + rf.has_ctone = True + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_name = True + rf.has_settings = True + rf.has_offset = True + rf.has_mode = True + rf.has_tuning_step = True + rf.has_nostep_tuning = True # Radio accepts any entered freq + rf.has_cross = False + rf.has_comment = False + rf.memory_bounds = (0, self._upper) + rf.valid_bands = TS480_BANDS + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/" + rf.valid_duplexes = TS480_DUPLEX + rf.valid_modes = TS480_MODES + rf.valid_skips = TS480_SKIP + rf.valid_tuning_steps = TS480_TUNE_STEPS + rf.valid_tmodes = ["", "Tone", "TSQL"] + rf.valid_name_length = 8 # 8 character channel names + + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = _(dedent("""\ + P-VFO channels 100-109 are considered Settings.\n + Only a subset of the over 130 available radio settings + are supported in this release.\n + """)) + rp.pre_download = _(dedent("""\ + Follow these instructions to download the radio memory: + 1 - Connect your interface cable + 2 - Radio > Download from radio: Don't adjust any settings + on the radio head! + 3 - Disconnect your interface cable + """)) + rp.pre_upload = _(dedent("""\ + Follow these instructions to upload the radio memory: + 1 - Connect your interface cable + 2 - Radio > Upload to radio: Don't adjust any settings + on the radio head! + 3 - Disconnect your interface cable + """)) + return rp + + def sync_in(self): + """Download from radio""" + try: + _connect_radio(self) + data = _read_mem(self) + data += _read_settings(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + return + + def sync_out(self): + """Upload to radio""" + try: + _connect_radio(self) + _write_mem(self) + _write_sets(self) + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + return + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + return + + def get_memory(self, number): + """Convert raw channel data (_mem) into UI columns (mem)""" + mem = chirp_common.Memory() + if number > 99 and number < 110: + return # Don't show VFO edges as mem chans + _mem = self._memobj.ch_mem[number] + mem.number = number + mnx = "" + for char in _mem.name: + mnx += chr(char) + mem.name = mnx.strip() + mem.name = mem.name.upper() + if _mem.rxfreq == 0: + mem.empty = True + return mem + mem.empty = False + mem.freq = int(_mem.rxfreq) + mem.duplex = TS480_DUPLEX[0] # None by default + mem.offset = 0 + if _mem.rxfreq < _mem.txfreq: # + shift + mem.duplex = TS480_DUPLEX[2] + mem.offset = _mem.txfreq - _mem.rxfreq + if _mem.rxfreq > _mem.txfreq: # - shift + mem.duplex = TS480_DUPLEX[1] + mem.offset = _mem.rxfreq - _mem.txfreq + if _mem.txfreq == 0: + # leave offset alone, or run_tests will bomb + mem.duplex = TS480_DUPLEX[0] + mx = _mem.xmode - 1 # CAT modes start at 1 + if _mem.xmode == 9: # except there is no xmode 9 + mx = 7 + mem.mode = TS480_MODES[mx] + mem.tmode = "" + mem.cross_mode = "Tone->Tone" + mem.ctone = TS480_TONES[_mem.ctone] + mem.rtone = TS480_TONES[_mem.rtone] + if _mem.tmode == 1: + mem.tmode = "Tone" + elif _mem.tmode == 2: + mem.tmode = "TSQL" + elif _mem.tmode == 3: + mem.tmode = "Cross" + mem.skip = TS480_SKIP[_mem.skip] + # Tuning step depends on mode + options = [0.5, 1.0, 2.5, 5.0, 10.0] # SSB/CS/FSK + if _mem.xmode == 4 or _mem.xmode == 5: # AM/FM + options = TS480_TUNE_STEPS[3:] + mem.tuning_step = options[_mem.step] + return mem + + def set_memory(self, mem): + """Convert UI column data (mem) into MEM_FORMAT memory (_mem)""" + _mem = self._memobj.ch_mem[mem.number] + if mem.empty: + _mem.rxfreq = 0 + _mem.txfreq = 0 + _mem.xmode = 0 + _mem.step = 0 + _mem.tmode = 0 + _mem.rtone = 0 + _mem.ctone = 0 + _mem.skip = 0 + _mem.name = " " + return + + if mem.number > self._upper: # Specials: No Name changes + ix = 0 + # LOG.warning("Special Chan set_mem @ %i" % mem.number) + else: + nx = len(mem.name) + for ix in range(8): + if ix < nx: + _mem.name[ix] = mem.name[ix].upper() + else: + _mem.name[ix] = " " # assignment needs 8 chrs + _mem.rxfreq = mem.freq + _mem.txfreq = 0 + if mem.duplex == "+": + _mem.txfreq = mem.freq + mem.offset + if mem.duplex == "-": + _mem.txfreq = mem.freq - mem.offset + ix = TS480_MODES.index(mem.mode) + _mem.xmode = ix + 1 # stored as CAT values, LSB= 1 + if ix == 7: # FSK-R + _mem.xmode = 9 # There is no CAT 8 + _mem.tmode = 0 + _mem.rtone = TS480_TONES.index(mem.rtone) + _mem.ctone = TS480_TONES.index(mem.ctone) + if mem.tmode == "Tone": + _mem.tmode = 1 + if mem.tmode == "TSQL": + _mem.tmode = 2 + _mem.skip = 0 + if mem.skip == "S": + _mem.skip = 1 + options = [0.5, 1.0, 2.5, 5.0, 10.0] # SSB/CS/FSK steps + if _mem.xmode == 4 or _mem.xmode == 5: # AM/FM + options = TS480_TUNE_STEPS[3:] + _mem.step = options.index(mem.tuning_step) + return + + def _parse_mem_spec(self, spec0, spec1): + """ Extract ascii memory paramters; build data string """ + # spec0 is simplex result, spec1 is split + # pad string so indexes match Kenwood docs + spec0 = "x" + spec0 # match CAT document 1-based description + ix = len(spec0) + # _pxx variables are STRINGS + _p1 = spec0[3] # P1 Split Specification + _p3 = spec0[5:7] # P3 Memory Channel + _p4 = spec0[7:18] # P4 Frequency + _p5 = spec0[18] # P5 Mode + _p6 = spec0[19] # P6 Chan Lockout (Skip) + _p7 = spec0[20] # P7 Tone Mode + _p8 = spec0[21:23] # P8 Tone Frequency Index + if _p8 == "00": + _p8 = "08" + _p9 = spec0[23:25] # P9 CTCSS Frequency Index + if _p9 == "00": + _p9 = "08" + _p14 = spec0[39:41] # P14 Step Size + _p16 = spec0[41:50] # P16 Max 8-Char Name if assigned + + spec1 = "x" + spec1 + _p4s = int(spec1[7:18]) # P4: Offset freq + + datm = "" # Fill in MEM_FORMAT sequence + datm += _make_dat(_p4, 4) # rxreq: u32, 4 bytes/chars + datm += _make_dat(_p4s, 4) # tx freq + datm += chr(int(_p5)) # xmode: 0-9 + datm += chr(int(_p7)) # Tmode: 0-3 + datm += chr(int(_p8)) # rtone: 00-41 + datm += chr(int(_p9)) # ctone: 00-41 + datm += chr(int(_p6)) # skip: 0/1 + datm += chr(int(_p14)) # step: 0-9 + v1 = len(_p16) + for ix in range(8): + if ix < v1: + datm += _p16[ix] + else: + datm += " " + return datm + + def _make_base_spec(self, mem, freq): + spec = "%011i%1i%1i%1i%02i%02i00000000000000%02i0%s" \ + % (freq, mem.xmode, mem.skip, mem.tmode, mem.rtone, + mem.ctone, mem.step, mem.name) + + return spec.strip() + + def get_settings(self): + """Translate the MEM_FORMAT structs into settings in the UI""" + # Define mem struct write-back shortcuts + _sets = self._memobj.settings + _asf = self._memobj.asf + _ssf = self._memobj.ssf + _mex = self._memobj.exset + _chm = self._memobj.ch_mem + basic = RadioSettingGroup("basic", "Basic Settings") + pvfo = RadioSettingGroup("pvfo", "VFO Band Edges") + mena = RadioSettingGroup("mena", "Menu A") + menb = RadioSettingGroup("menb", "Menu B") + amode = RadioSettingGroup("amode", "Auto Mode") + ssc = RadioSettingGroup("ssc", "Slow Scan") + group = RadioSettings(basic, pvfo, mena, menb, amode, ssc) + + mhz1 = 1000000. + + # Callback functions + def _my_readonly(setting, obj, atrb): + """NOP callback, prevents writing the setting""" + vx = 0 + return + + def my_adjraw(setting, obj, atrb, fix=0, ndx=-1): + """Callback for Integer add or subtract fix from value.""" + vx = int(str(setting.value)) + value = vx + int(fix) + if value < 0: + value = 0 + if ndx < 0: + setattr(obj, atrb, value) + else: + setattr(obj[ndx], atrb, value) + return + + def my_mhz_val(setting, obj, atrb, ndx=-1): + """ Callback to set freq back to Htz""" + vx = float(str(setting.value)) + vx = int(vx * mhz1) + if ndx < 0: + setattr(obj, atrb, vx) + else: + setattr(obj[ndx], atrb, vx) + return + + def my_bool(setting, obj, atrb, ndx=-1): + """ Callback to properly set boolean """ + # set_settings is not setting [indexed] booleans??? + vx = 0 + if str(setting.value) == "True": + vx = 1 + if ndx < 0: + setattr(obj, atrb, vx) + else: + setattr(obj[ndx], atrb, vx) + return + + def my_asf_mode(setting, obj, nx=0): + """ Callback to extract mode and create asmode, asdata """ + v1 = TS480_MODES.index(str(setting.value)) + v2 = 0 # asdata + vx = v1 + 1 # stored as CAT values, same as xmode + if v1 == 7: + vx = 9 + if v1 > 7: # a Data mode + v2 = 1 + if v1 == 8: + vx = 1 # LSB + elif v1 == 9: + vx = 2 # USB + elif v1 == 10: + vx = 4 # FM + setattr(obj[nx], "asdata", v2) + setattr(obj[nx], "asmode", vx) + return + + def my_fnctns(setting, obj, ndx, atrb): + """ Filter only valid key function assignments """ + vx = int(str(setting.value)) + if vx > 79: + vx = 99 # Off + setattr(obj[ndx], atrb, vx) + return + + def my_labels(kx): + lbl = "%03i:" % kx # SG EX number + lbl += self.EX_LBL[kx] # and the label to match + return lbl + + # ===== BASIC GROUP ===== + + options = ["TS-480HX (200W)", "TS-480SAT (100W + AT)", + "Japanese 50W type", "Japanese 20W type"] + rx = RadioSettingValueString(14, 22, options[_sets.ty]) + rset = RadioSetting("settings.ty", "FirmwareVersion", rx) + rset.set_apply_callback(_my_readonly, _sets, "ty") + basic.append(rset) + + rx = RadioSettingValueInteger(0, 255, _sets.ag) + rset = RadioSetting("settings.ag", "AF Gain", rx) + # rset.set_apply_callback(my_adjraw, _sets, "ag", -1) + basic.append(rset) + + rx = RadioSettingValueInteger(0, 100, _sets.rg) + rset = RadioSetting("settings.rg", "RF Gain", rx) + # rset.set_apply_callback(my_adjraw, _sets, "rg", -1) + basic.append(rset) + + options = ["ANT1", "ANT2"] + # CAUTION: an has value of 1 or 2 + rx = RadioSettingValueList(options, options[_sets.an - 1]) + rset = RadioSetting("settings.an", "Antenna Selected", rx) + # Add 1 to the changed value. S/b 1/2 + rset.set_apply_callback(my_val_list, options, _sets, "an", 1) + basic.append(rset) + + rx = RadioSettingValueInteger(0, 100, _sets.mg) + rset = RadioSetting("settings.mg", "Microphone gain", rx) + basic.append(rset) + + nx = 5 # Coarse step + if bool(_mex[0].ex021): # Power Fine enabled in menu A + nx = 1 + vx = _sets.pc # Trap invalid values from run_tests.py + if vx < 5: + vx = 5 + options = [200, 100, 50, 20] # subject to firmware + rx = RadioSettingValueInteger(5, options[_sets.ty], vx, nx) + sx = "TX Output power (Watts)" + rset = RadioSetting("settings.pc", sx, rx) + basic.append(rset) + + val = _sets.fa / mhz1 # valid range is for receiver + rx = RadioSettingValueFloat(0.05, 60.0, val, 0.001, 3) + sx = "VFO-A Frequency (MHz)" + rset = RadioSetting("settings.fa", sx, rx) + rset.set_apply_callback(my_mhz_val, _sets, "fa") + basic.append(rset) + + val = _sets.fb / mhz1 + rx = RadioSettingValueFloat(0.05, 60.0, val, 0.001, 3) + sx = "VFO-B Frequency (MHz)" + rset = RadioSetting("settings.fb", sx, rx) + rset.set_apply_callback(my_mhz_val, _sets, "fb") + basic.append(rset) + + options = ["Menu A", "Menu B"] + rx = RadioSettingValueList(options, options[_sets.mf]) + sx = "Menu Selected" + rset = RadioSetting("settings.mf", sx, rx) + rset.set_apply_callback(my_val_list, options, _sets, "mf") + basic.append(rset) + + # ==== VFO Edges Group ================ + + for mx in range(100, 110): + val = _chm[mx].rxfreq / mhz1 + if val < 1.8: # Many operators never use this + val = 1.8 # So default is 0.0 + rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3) + sx = "VFO-Band %i lower limit (MHz)" % (mx - 100) + rset = RadioSetting("ch_mem.rxfreq/%d" % mx, sx, rx) + rset.set_apply_callback(my_mhz_val, _chm, "rxfreq", mx) + pvfo.append(rset) + + val = _chm[mx].txfreq / mhz1 + if val < 1.8: + val = 54.0 + rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3) + sx = " VFO-Band %i upper limit (MHz)" % (mx - 100) + rset = RadioSetting("ch_mem.txfreq/%d" % mx, sx, rx) + rset.set_apply_callback(my_mhz_val, _chm, "txfreq", mx) + pvfo.append(rset) + + kx = _chm[mx].xmode + options = ["None", "LSB", "USB", "CW", "FM", "AM", "FSK", + "CW-R", "N/A", "FSK-R"] + rx = RadioSettingValueList(options, options[kx]) + sx = " VFO-Band %i Tx/Rx Mode" % (mx - 100) + rset = RadioSetting("ch_mem.xmode/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _chm, + "xmode", 0, mx) + pvfo.append(rset) + + # ==== Menu A/B Group ================= + + for mx in range(2): # A/B index + sx = my_labels(0) + rx = RadioSettingValueInteger(0, 4, _mex[mx].ex000) + rset = RadioSetting("exset.ex000", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 9, _mex[mx].ex012) + sx = my_labels(12) + rset = RadioSetting("exset.ex012", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + sx = my_labels(13) + rx = RadioSettingValueInteger(0, 9, _mex[mx].ex013) + rset = RadioSetting("exset.ex013", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + sx = my_labels(14) + rx = RadioSettingValueInteger(0, 9, _mex[mx].ex014) + rset = RadioSetting("exset.ex014", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["250", "500", "1000"] + rx = RadioSettingValueList(options, options[_mex[mx].ex003]) + sx = my_labels(3) + rset = RadioSetting("exset.ex003/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex003", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex007)) + sx = my_labels(7) + rset = RadioSetting("exset.ex007/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex007", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex008)) + sx = my_labels(8) + rset = RadioSetting("exset.ex008/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex008", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["100", "200", "300", "400", "500"] + rx = RadioSettingValueList(options, options[_mex[mx].ex009]) + sx = my_labels(9) + rset = RadioSetting("exset.ex009/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex009", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex010)) + sx = my_labels(10) + rset = RadioSetting("exset.ex010/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex010", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["TO", "CO"] + rx = RadioSettingValueList(options, options[_mex[mx].ex011]) + sx = my_labels(11) + rset = RadioSetting("exset.ex011/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex011", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex021)) + sx = my_labels(21) + rset = RadioSetting("exset.ex021/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex021", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["Off", "3", "5", "10", "20", "30"] + rx = RadioSettingValueList(options, options[_mex[mx].ex022]) + sx = my_labels(22) + rset = RadioSetting("exset.ex022/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex022", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 99, _mex[mx].ex048) + sx = my_labels(48) + rset = RadioSetting("exset.ex048/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex048") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 99, _mex[mx].ex049) + sx = my_labels(49) + rset = RadioSetting("exset.ex049/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex049") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 99, _mex[mx].ex050) + sx = my_labels(50) + rset = RadioSetting("exset.ex050/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex050") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 99, _mex[mx].ex051) + sx = my_labels(51) + rset = RadioSetting("exset.ex051/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex051") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 99, _mex[mx].ex052) + sx = my_labels(52) + rset = RadioSetting("exset.ex052/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex052") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + # End of for mx loop + + # ==== Auto Scan Params (amode) ============== + for ix in range(32): + val = _asf[ix].asfreq / mhz1 + rx = RadioSettingValueFloat(0.03, 60.0, val, 0.001, 3) + rset = RadioSetting("asf.asfreq/%d" % ix, + "Scan %02i Freq (MHz)" % ix, rx) + rset.set_apply_callback(my_mhz_val, _asf, "asfreq", ix) + amode.append(rset) + + mx = _asf[ix].asmode - 1 # Same logic as xmode + if _asf[ix].asmode == 9: + mx = 7 + rx = RadioSettingValueList(TS480_MODES, TS480_MODES[mx]) + rset = RadioSetting("asf.asmode/%d" % ix, " Mode", rx) + rset.set_apply_callback(my_asf_mode, _asf, ix) + amode.append(rset) + + # ==== Slow Scan Settings === + for ix in range(10): # Chans + for nx in range(5): # spots + px = ((ix * 5) + nx) + val = _ssf[px].ssfreq / mhz1 + stx = " - - - Slot %02i Freq (MHz)" % nx + if nx == 0: + stx = "Slow Scan %02i, Slot 0 Freq (MHz" % ix + rx = RadioSettingValueFloat(0, 54.0, val, 0.001, 3) + rset = RadioSetting("ssf.ssfreq/%d" % px, stx, rx) + rset.set_apply_callback(my_mhz_val, _ssf, "ssfreq", px) + ssc.append(rset) + + return group # END get_settings() + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + return diff --git a/chirp/drivers/ts590.py b/chirp/drivers/ts590.py new file mode 100644 index 0000000..497aac3 --- /dev/null +++ b/chirp/drivers/ts590.py @@ -0,0 +1,1684 @@ +# Copyright 2019 Rick DeWitt +# Version 1.0: CatClone- Implementing fake memory image +# Version 2.0: No Live Mode library links. Implementing mem as Clone Mode +# Having fun with Dictionaries +# Version 2.1: Adding match_model function to fix File>New issue #7409 +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re +import math +import threading +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0000; +struct { // 20 bytes per chan + u32 rxfreq; + u32 txfreq; + u8 xmode:4, // param stored as CAT value + data:2, + tmode:2; + u8 rtone; + u8 ctone; + u8 filter:1, + fmnrw:1, + skip:1, + nu:5; + char name[8]; +} ch_mem[120]; // 100 normal + 10 P-type + 10 EXT + +struct { // 5 bytes each + u32 asfreq; + u8 asmode:4, // param stored as CAT value + asdata:2, + asnu:2; +} asf[32]; + +struct { // 10 x 5 4-byte frequencies + u32 ssfreq; +} ssf[50]; + +struct { // 16 bytes + u8 txeq; + u8 rxeq; +} eqx[8]; + +struct { // Common to S and SG models + u8 ag; + u8 an1:2, + an2:2, + an3:2, + anu:2; + u32 fa; + u32 fb; + char fv[4]; + u8 mf; + u8 mg; + u8 pc; + u8 rg; + u8 tp; +} settings; + +struct { // Menu A/B settings by TS-590SG names + char ex001[8]; // 590S values get put in SG equiv + u8 ex002; // These params stored as nibbles + u8 ex003; + u8 ex005; + u8 ex006; + u8 ex007; + u8 ex008; + u8 ex009; + u8 ex010; + u8 ex011; + u8 ex012; + u8 ex013; + u8 ex016; + u8 ex017; + u8 ex018; + u8 ex019; + u8 ex021; + u8 ex022; + u8 ex023; + u8 ex024; + u8 ex025; + u8 ex026; + u8 ex054; + u8 ex055; + u8 ex076; + u8 ex077; + u8 ex087; + u8 ex088; + u8 ex089; + u8 ex090; + u8 ex091; + u8 ex092; + u8 ex093; + u8 ex094; + u8 ex095; + u8 ex096; + u8 ex097; + u8 ex098; + u8 ex099; +} exset[2]; + + char mdl_name[9]; // appended model name, first 9 chars + +""" + +STIMEOUT = 2 +LOCK = threading.Lock() +BAUD = 0 # Initial baud rate +MEMSEL = 0 # Default Menu A +BEEPVOL = 4 # Default beep volume +W8S = 0.01 # short wait, secs +W8L = 0.05 # long wait + +TS590_DUPLEX = ["", "-", "+"] +TS590_SKIP = ["", "S"] + +# start at 0:LSB +TS590_MODES = ["LSB", "USB", "CW", "FM", "AM", "FSK", "CW-R", + "FSK-R", "Data+LSB", "Data+USB", "Data+FM"] +EX_MODES = ["FSK-R", "CW-R", "Data+LSB", "Data+USB", "Data+FM"] +for ix in EX_MODES: + if ix not in chirp_common.MODES: + chirp_common.MODES.append(ix) + +TS590_TONES = list(chirp_common.TONES) +TS590_TONES.append(1750.0) + +RADIO_IDS = { # From kenwood_live.py; used to report wrong radio + "ID019;": "TS-2000", + "ID009;": "TS-850", + "ID020;": "TS-480", + "ID021;": "TS-590S", + "ID023;": "TS-590SG" +} + + +def command(ser, cmd, rsplen, w8t=0.01, exts=""): + """Send @cmd to radio via @ser""" + # cmd is output string without ; terminator + # rsplen is expected response char count, including terminator + # If rsplen = 0 then do not read after write + + start = time.time() + # LOCK.acquire() + stx = cmd # preserve cmd for response check + stx = stx + exts + ";" # append arguments + ser.write(stx) + LOG.debug("PC->RADIO [%s]" % stx) + ts = time.time() # implement the wait after command + while (time.time() - ts) < w8t: + ix = 0 # NOP + result = "" + if rsplen > 0: # read response + result = ser.read(rsplen) + LOG.debug("RADIO->PC [%s]" % result) + result = result[:-1] # remove terminator + # LOCK.release() + return result.strip() + + +def _connect_radio(radio): + """Determine baud rate and verify radio on-line""" + global BAUD # Allows modification + bauds = [115200, 57600, 38400, 19200, 9600, 4800] + if BAUD > 0: + bauds.insert(0, BAUD) # Make the detected one first + # Flush the input buffer + radio.pipe.timeout = 0.005 + radio.pipe.baudrate = 9600 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + + for bd in bauds: + radio.pipe.baudrate = bd + BAUD = bd + radio.pipe.write(";") + radio.pipe.write(";") + resp = radio.pipe.read(4) + radio.pipe.write("ID;") + resp = radio.pipe.read(6) + + if resp == radio.ID: # Good comms + resp = command(radio.pipe, "AI0", 0, W8L) + return + elif resp in RADIO_IDS.keys(): + msg = "Radio reported as model %s, not %s!" % \ + (RADIO_IDS[resp], radio.MODEL) + raise errors.RadioError(msg) + raise errors.RadioError("No response from radio") + return + + +def read_str(radio, trm=";"): + """ Read chars until terminator """ + stq = "" + ctq = "" + while ctq != trm: + ctq = radio.pipe.read(1) + stq += ctq + LOG.debug(" + [%s]" % stq) + return stq[:-1] # Return without trm + + +def _read_mem(radio): + """Get the memory map""" + global BEEPVOL + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._upper + 20 # 10 P chans and 10 EXT + status.msg = "Reading Channel Memory..." + radio.status_fn(status) + + result0 = command(radio.pipe, "EX0050000", 9, W8S) + result0 += read_str(radio) + BEEPVOL = int(result0[6:]) + result0 = command(radio.pipe, "EX005000000", 0, W8L) # Silence beeps + + data = "" + mrlen = 41 # Expected fixed return string length + for chn in range(0, (radio._upper + 21)): # Loop stops at +20 + # Request this mem chn + r0ch = 999 + r1ch = r0ch + # return results can come back out of order + while (r0ch != chn): + # simplex + if chn < 100: + result0 = command(radio.pipe, "MR0 %02i" % chn, + mrlen, W8S) + result0 += read_str(radio) + else: + result0 = command(radio.pipe, "MR0%03i" % chn, + mrlen, W8S) + result0 += read_str(radio) + r0ch = int(result0[3:6]) + while (r1ch != chn): + # split + if chn < 100: + result1 = command(radio.pipe, "MR1 %02i" % chn, + mrlen, W8S) + result1 += read_str(radio) + else: + result1 = command(radio.pipe, "MR1%03i" % chn, + mrlen, W8S) + result1 += read_str(radio) + r1ch = int(result1[3:6]) + data += radio._parse_mem_spec(result0, result1) + # UI Update + status.cur = chn + status.msg = "Reading Channel Memory..." + radio.status_fn(status) + + if len(data) == 0: # To satisfy run_tests + raise errors.RadioError('No data received.') + return data + + +def _make_dat(sx, nb): + """ Split the string sx into nb binary bytes """ + vx = int(sx) + dx = "" + if nb > 3: + dx += chr((vx >> 24) & 0xFF) + if nb > 2: + dx += chr((vx >> 16) & 0xFF) + if nb > 1: + dx += chr((vx >> 8) & 0xFF) + dx += chr(vx & 0xFF) + return dx + + +def _sets_val(stx, nv=3, nb=2): + """ Split string stx into nv nb-bit values in 1 byte """ + # Right now: hardcoded for nv:3 values of nb:2 bits each + v1 = int(stx[0]) << 6 + v1 = v1 | (int(stx[1]) << 4) + v1 = v1 | (int(stx[2]) << 2) + return chr(v1) + + +def _sets_asf(stx): + """ Process AS0 auto-mode setting """ + asm = _make_dat(stx[0:11], 4) # 11-bit freq + a1 = int(stx[11]) # 4-bit mode + a2 = int(stx[12]) # 2-bit data + asm += chr((a1 << 4) | (a2 << 2)) + return asm + + +def my_val_list(setting, opts, obj, atrb, fix=0, ndx=-1): + """Callback:from ValueList. Set the integer index.""" + # This function is here to be available to get_mem and get_set + # fix is optional additive offset to the list index + # ndx is optional obj[ndx] array index + value = opts.index(str(setting.value)) + value += fix + if ndx >= 0: # indexed obj + setattr(obj[ndx], atrb, value) + else: + setattr(obj, atrb, value) + return + + +def _read_settings(radio): + """ Continue filling memory map""" + global MEMSEL + # setc: the list of CAT commands for downloaded settings + # Block paramters first. In the exact order of MEM_FORMAT + setc = radio.SETC + setc.extend(radio.EX) # Menu A EX params + setc.extend(radio.EX) # Menu B + status = chirp_common.Status() + status.cur = 0 + status.max = 32 + 50 + 8 + 11 + 39 + 39 + status.msg = "Reading Settings..." + radio.status_fn(status) + + setts = "" + nc = 0 + for cmc in setc: + skipme = False + argx = "" # Extended arguments + if cmc == "AS0": + skipme = True # flag to disable further processing + for ix in range(32): # 32 AS params + result0 = command(radio.pipe, cmc, 19, W8S, + "%02i" % ix) + xc = len(cmc) + 2 + result0 = result0[xc:] + setts += _sets_asf(result0) + nc += 1 + status.cur = nc + radio.status_fn(status) + elif cmc == "SS": + skipme = True + for ix in range(10): # 10 chans + for nx in range(5): # 5 spots + result0 = command(radio.pipe, cmc, 16, W8S, + "%1i%1i" % (ix, nx)) + setts += _make_dat(result0[4:], 4) + nc += 1 + status.cur = nc + radio.status_fn(status) + elif cmc == "EQ": + skipme = True + for ix in range(8): + result0 = command(radio.pipe, cmc, 6, W8S, "0%1i" + % ix) # Tx eq + setts += chr(int(result0[4:])) # 'EQ13x", want the x + result0 = command(radio.pipe, cmc, 6, W8S, "1%1i" + % ix) # Rx eq + setts += chr(int(result0[4:])) + nc += 1 + status.cur = nc + radio.status_fn(status) + elif ((not radio.SG) and (cmc == "EX087")) \ + or (radio.SG and (cmc == "EX001")): + result0 = command(radio.pipe, cmc, 9, W8S, "0000") + result0 += read_str(radio) # Read pwron message + result0 = result0[8:] + nx = len(result0) + for ix in range(8): + if ix < nx: + sx = result0[ix] # may need to test valid char + setts += sx + else: + setts += chr(0) + skipme = True + nc += 1 + status.cur = nc + radio.status_fn(status) + elif (cmc == "MF0") or (cmc == "MF1"): + result0 = command(radio.pipe, cmc, 0, W8S) + skipme = True # cmd only, no response + else: # issue the cmc cmd as-is with argx + if str(cmc).startswith("EX"): + argx = "0000" + result0 = command(radio.pipe, cmc, 0, W8S, argx) + result0 = read_str(radio) # various length responses + # strip the cmd echo + xc = len(cmc) + result0 = result0[xc:] + # Cmd has been sent, process the result + if cmc == "FV": # all chars + skipme = True + setts += result0 + elif cmc == "AN": # Antenna selection has 3 values + skipme = True + setts += _sets_val(result0, 3, 2) # store as 2-bits each + elif (cmc == "FA") or (cmc == "FB"): # Response is 11-bit frq + skipme = True + setts += _make_dat(result0, 4) # 11-bit freq + elif (cmc == "MF0") or (cmc == "MF1"): # No stored response + skipme = True + # Generic single byte processing + if not skipme: + setts += chr(int(result0)) + if cmc == "MF": # Save the initial Menu selection + MEMSEL = int(result0) + nc += 1 + status.cur = nc + radio.status_fn(status) + setts += radio.MODEL.ljust(9) + # Now set the inidial menu selection back + result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % MEMSEL) + # And the original Beep Volume + result0 = command(radio.pipe, "EX0050000%2i" % BEEPVOL, 0, W8L) + return setts + + +def _write_mem(radio): + """ Send MW commands for each channel """ + global BEEPVOL + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = radio._upper + 20 # 10 P chans and 10 EXT + status.msg = "Writing Channel Memory" + radio.status_fn(status) + + result0 = command(radio.pipe, "EX0050000", 9, W8S) + result0 += read_str(radio) + BEEPVOL = int(result0[6:]) + result0 = command(radio.pipe, "EX005000000", 0, W8L) # Silence beeps + + for chn in range(0, (radio._upper + 21)): # Loop stops at +20 + _mem = radio._memobj.ch_mem[chn] + cmx = "MW0 %02i" % chn + if chn > 99: + cmx = "MW0%03i" % chn + stm = cmx + radio._make_base_spec(_mem, _mem.rxfreq) + result0 = command(radio.pipe, stm, 0, W8L) # No response + cmx = "MW1 %02i" % chn + if chn > 99: + cmx = "MW1%03i" % chn + stm = cmx + radio._make_base_spec(_mem, _mem.txfreq) + if _mem.rxfreq > 0: # Dont write MW1 if empty + result0 = command(radio.pipe, stm, 0, W8L) + # UI Update + status.cur = chn + radio.status_fn(status) + return + + +def _write_sets(radio): + """ Send settings and Menu a/b """ + status = chirp_common.Status() + status.cur = 0 + status.max = 187 # Total to send + status.msg = "Writing Settings" + radio.status_fn(status) + # Define mem struct shortcuts + _sets = radio._memobj.settings + _asf = radio._memobj.asf + _ssf = radio._memobj.ssf + _eqx = radio._memobj.eqx + _mex = radio._memobj.exset + snx = 0 # Settings status counter + stlen = 0 # No response count + # Send 32 AS + for ix in range(32): + scm = "AS0%02i%011i%1i%1i" % (ix, _asf[ix].asfreq, + _asf[ix].asmode, _asf[ix].asdata) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 1 + status.cur = snx + radio.status_fn(status) + # Send 50 SS + for ix in range(10): + for kx in range(5): + nx = ix * 5 + kx + scm = "SS%1i%1i%011i" % (ix, kx, _ssf[nx].ssfreq) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 1 + status.cur = snx + radio.status_fn(status) + # Send 16 EQ + for ix in range(8): + scm = "EQ0%1i%1i" % (ix, _eqx[ix].txeq) + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "EQ1%1i%1i" % (ix, _eqx[ix].rxeq) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 2 + status.cur = snx + radio.status_fn(status) + # Send 11 thingies + scm = "AG0%03i" % _sets.ag + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "AN%1i%1i%1i" % (_sets.an1, _sets.an2, _sets.an3) + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "FA%011i" % _sets.fa + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "FB%011i" % _sets.fb + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "MG%03i" % _sets.mg + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "PC%03i" % _sets.pc + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "RG%03i" % _sets.rg + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "TP%03i" % _sets.tp + result0 = command(radio.pipe, scm, stlen, W8S) + scm = "MF0" # Select menu A/B + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 11 + status.cur = snx + radio.status_fn(status) + # Send 38 Menu A EX + setc = radio.EX # list of EX cmds + for ix in range(2): + for cmx in setc: + if str(cmx)[0:2] == "MF": + scm = cmx + else: # The EX cmds + # Test for the power-on string + if (radio.SG and cmx == "EX001") or \ + ((not radio.SG) and cmx == "EX087"): + scm = cmx + "0000" + for chx in _mex[ix].ex001: # Both get string here + scm += chr(chx) + scm = scm.strip() + scm = scm.strip(chr(0)) # in case any got thru + # Now for the other EX cmds + else: + if radio.SG: + scm = "%s0000%i" % (cmx, getattr(_mex[ix], + cmx.lower())) + else: # Gotta use the cross reference dict for cmd + scm = "%s0000%i" % (cmx, getattr(_mex[ix], + radio.EX_X[cmx].lower())) + result0 = command(radio.pipe, scm, stlen, W8S) + snx += 1 + status.cur = snx + radio.status_fn(status) + # Now set the inidial menu selection back + result0 = command(radio.pipe, "MF", 0, W8L, "%1i" % _sets.mf) + # And the original Beep Volume + result0 = command(radio.pipe, "EX0050000%2i" % BEEPVOL, 0, W8L) + return + + +@directory.register +class TS590Radio(chirp_common.CloneModeRadio): + """Kenwood TS-590""" + VENDOR = "Kenwood" + MODEL = "TS-590SG_CloneMode" + ID = "ID023;" + SG = True + # Settings read/write cmd sequence list + SETC = ["AS0", "SS", "EQ", "AG0", "AN", "FA", "FB", + "FV", "MF", "MG", "PC", "RG", "TP", "MF0"] + # This is the TS-590SG MENU A/B read_settings paramter tuple list + # The order is mandatory; to match the Mem_Format sequence + EX = ["EX001", "EX002", "EX003", "EX005", "EX006", "EX007", + "EX008", "EX009", "EX010", "EX011", "EX012", "EX013", "EX016", + "EX017", "EX018", "EX019", "EX021", "EX022", "EX023", "EX024", + "EX025", "EX026", "EX054", "EX055", "EX076", "EX077", "EX087", + "EX088", "EX089", "EX090", "EX091", "EX092", "EX093", "EX094", + "EX095", "EX096", "EX097", "EX098", "EX099", "MF1"] + # EX menu settings label dictionary. Key is the EX number + EX_LBL = {2: " Display brightness", + 1: " Power-On message", + 3: " Backlight color", + 5: " Beep volume", + 6: " Sidetone volume", + 7: " Message playback volume", + 8: " Voice guide volume", + 9: " Voice guide speed", + 10: " Voice guide language", + 11: " Auto Announcement", + 12: " MHz step", + 13: " Tuning control adj rate (Hz)", + 16: " SSB tune step (KHz)", + 17: " CW/FSK tune step (KHz)", + 18: " AM tune step (KHz)", + 19: " FM tune step (KHz)", + 21: " Max number of Quick Mem chans", + 22: " Temporary MR Chan freq allowed", + 23: " Program Scan slowdown", + 24: " Program Scan slowdown range (Hz)", + 25: " Program Scan hold", + 26: " Scan Resume method", + 54: " TX Power fine adjust", + 55: " Timeout timer (Secs)", + 76: " Data VOX", + 77: " Data VOX delay (x30 mSecs)", + 87: " Panel PF-A function", + 88: " Panel PF-B function", + 89: " RIT key function", + 90: " XIT key function", + 91: " CL key function", + 92: " Front panel MULTI/CH key (non-CW mode)", + 93: " Front panel MULTI/CH key (CW mode)", + 94: " MIC PF1 function", + 95: " MIC PF2 function", + 96: " MIC PF3 function", + 97: " MIC PF4 function", + 98: " MIC PF (DWN) function", + 99: " MIC PF (UP) function"} + + BAUD_RATE = 115200 + _upper = 99 + + # Special Channels Declaration + # WARNING Indecis are hard wired in get/set_memory code !!! + # Channels print in + increasing index order + SPECIAL_MEMORIES = {"EXT 0": 110, + "EXT 1": 111, + "EXT 2": 112, + "EXT 3": 113, + "EXT 4": 114, + "EXT 5": 115, + "EXT 6": 116, + "EXT 7": 117, + "EXT 8": 118, + "EXT 9": 119} + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.can_odd_split = False + rf.has_bank = False + rf.has_ctone = True + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_name = True + rf.has_settings = True + rf.has_offset = True + rf.has_mode = True + rf.has_tuning_step = False # Not in mem chan + rf.has_nostep_tuning = True # Radio accepts any entered freq + rf.has_cross = True + rf.has_comment = False + rf.memory_bounds = (0, self._upper) + rf.valid_bands = [(30000, 24999999), (25000000, 59999999)] + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "*+-/" + rf.valid_duplexes = TS590_DUPLEX + rf.valid_modes = TS590_MODES + rf.valid_skips = TS590_SKIP + rf.valid_tuning_steps = [0.5, 1.0, 2.5, 5.0, 6.25, 10.0, 12.5, + 15.0, 20.0, 25.0, 30.0, 50.0, 100.0] + rf.valid_tmodes = ["", "Tone", "TSQL", "Cross"] + rf.valid_cross_modes = ["Tone->Tone"] + rf.valid_name_length = 8 # 8 character channel names + rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys()) + + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = _(dedent("""\ + Click on the "Special Channels" toggle-button of the memory + editor to see/set the EXT channels. P-VFO channels 100-109 + are considered Settings.\n + Only a subset of the over 200 available radio settings + are supported in this release.\n + Ignore the beeps from the radio on upload and download. + """)) + rp.pre_download = _(dedent("""\ + Follow these instructions to download the radio memory: + 1 - Connect your interface cable + 2 - Radio > Download from radio: DO NOT mess with the radio + during download! + 3 - Disconnect your interface cable + """)) + rp.pre_upload = _(dedent("""\ + Follow these instructions to upload the radio memory: + 1 - Connect your interface cable + 2 - Radio > Upload to radio: DO NOT mess with the radio + during upload! + 3 - Disconnect your interface cable + """)) + return rp + + def sync_in(self): + """Download from radio""" + try: + _connect_radio(self) + data = _read_mem(self) + data += _read_settings(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + return + + def sync_out(self): + """Upload to radio""" + try: + _connect_radio(self) + _write_mem(self) + _write_sets(self) + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + return + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + return + + def get_memory(self, number): + """Convert raw channel data (_mem) into UI columns (mem)""" + mem = chirp_common.Memory() + mem.extra = RadioSettingGroup("extra", "Extra") + if isinstance(number, str): + mem.name = number # Spcl chns 1st var + mem.number = self.SPECIAL_MEMORIES[number] + _mem = self._memobj.ch_mem[mem.number] + else: # Normal mem chans and VFO edges + if number > 99 and number < 110: + return # Don't show VFO edges as mem chans + _mem = self._memobj.ch_mem[number] + mem.number = number + mnx = "" + for char in _mem.name: + mnx += chr(char) + mem.name = mnx.strip() + mem.name = mem.name.upper() + # From here on is common to both types + if _mem.rxfreq == 0: + _mem.txfreq = 0 + mem.empty = True + mem.freq = 0 + mem.mode = "LSB" + mem.offset = 0 + return mem + mem.empty = False + mem.freq = int(_mem.rxfreq) + mem.duplex = TS590_DUPLEX[0] # None by default + mem.offset = 0 + if _mem.rxfreq < _mem.txfreq: # + shift + mem.duplex = TS590_DUPLEX[2] + mem.offset = _mem.txfreq - _mem.rxfreq + if _mem.rxfreq > _mem.txfreq: # - shift + mem.duplex = TS590_DUPLEX[1] + mem.offset = _mem.rxfreq - _mem.txfreq + if _mem.txfreq == 0: + # leave offset alone, or run_tests will bomb + mem.duplex = TS590_DUPLEX[0] + mx = _mem.xmode - 1 # CAT modes start at 1 + if _mem.xmode == 9: # except CAT FSK-R is 9, there is no 8 + mx = 7 + if _mem.data: # LSB+Data= 8, USB+Data= 9, FM+Data= 10 + if _mem.xmode == 1: # CAT LSB + mx = 8 + elif _mem.xmode == 2: # CAT USB + mx = 9 + elif _mem.xmode == 4: # CAT FM + mx = 10 + mem.mode = TS590_MODES[mx] + mem.tmode = "" + mem.cross_mode = "Tone->Tone" + mem.ctone = TS590_TONES[_mem.ctone] + mem.rtone = TS590_TONES[_mem.rtone] + if _mem.tmode == 1: + mem.tmode = "Tone" + elif _mem.tmode == 2: + mem.tmode = "TSQL" + elif _mem.tmode == 3: + mem.tmode = "Cross" + mem.skip = TS590_SKIP[_mem.skip] + + # Channel Extra settings: Only Boolean & List methods, no call-backs + options = ["Wide", "Narrow"] + rx = RadioSettingValueList(options, options[_mem.fmnrw]) + # NOTE: first param of RadioSetting is the object attribute name + rset = RadioSetting("fmnrw", "FM mode", rx) + rset.set_apply_callback(my_val_list, options, _mem, "fmnrw") + mem.extra.append(rset) + + options = ["Filter A", "Filter B"] + rx = RadioSettingValueList(options, options[_mem.filter]) + rset = RadioSetting("filter", "Filter A/B", rx) + rset.set_apply_callback(my_val_list, options, _mem, "filter") + mem.extra.append(rset) + + return mem + + def set_memory(self, mem): + """Convert UI column data (mem) into MEM_FORMAT memory (_mem)""" + _mem = self._memobj.ch_mem[mem.number] + if mem.empty: + _mem.rxfreq = 0 + _mem.txfreq = 0 + _mem.xmode = 0 + _mem.data = 0 + _mem.tmode = 0 + _mem.rtone = 0 + _mem.ctone = 0 + _mem.filter = 0 + _mem.skip = 0 + _mem.fmnrw = 0 + _mem.name = " " + return + + if mem.number > self._upper: # Specials: No Name changes + ix = 0 + # LOG.warning("Special Chan set_mem @ %i" % mem.number) + else: + nx = len(mem.name) + for ix in range(8): + if ix < nx: + _mem.name[ix] = mem.name[ix].upper() + else: + _mem.name[ix] = " " # assignment needs 8 chrs + _mem.rxfreq = mem.freq + _mem.txfreq = 0 + if mem.duplex == "+": + _mem.txfreq = mem.freq + mem.offset + if mem.duplex == "-": + _mem.txfreq = mem.freq - mem.offset + ix = TS590_MODES.index(mem.mode) + _mem.data = 0 + _mem.xmode = ix + 1 # stored as CAT values, LSB= 1 + if ix == 7: # FSK-R + _mem.xmode = 9 # There is no CAT 8 + if ix > 7: # a Data mode + _mem.data = 1 + if ix == 8: + _mem.xmode = 1 # LSB + elif ix == 9: + _mem.xmode = 2 # USB + elif ix == 10: + _mem.xmode = 4 # FM + _mem.tmode = 0 + _mem.rtone = TS590_TONES.index(mem.rtone) + _mem.ctone = TS590_TONES.index(mem.ctone) + if mem.tmode == "Tone": + _mem.tmode = 1 + if mem.tmode == "TSQL": + _mem.tmode = 2 + if mem.tmode == "Cross" or mem.tmode == "Tone->Tone": + _mem.tmode = 3 + _mem.skip = 0 + if mem.skip == "S": + _mem.skip = 1 + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + return + + def _parse_mem_spec(self, spec0, spec1): + """ Extract ascii memory paramters; build data string """ + # spec0 is simplex result, spec1 is split + # pad string so indexes match Kenwood docs + spec0 = "x" + spec0 # match CAT document 1-based description + ix = len(spec0) + # _pxx variables are STRINGS + _p1 = spec0[3] # P1 Split Specification + _p3 = spec0[4:7] # P3 Memory Channel + _p4 = spec0[7:18] # P4 Frequency + _p5 = spec0[18] # P5 Mode + _p6 = spec0[19] # P6 Data Mode + _p7 = spec0[20] # P7 Tone Mode + _p8 = spec0[21:23] # P8 Tone Frequency Index + if _p8 == "00": # Can't be 0 at upload + _p8 = "08" + _p9 = spec0[23:25] # P9 CTCSS Frequency Index + if _p9 == "00": + _p9 = "08" + _p10 = spec0[25:28] # P10 Always 0 + _p11 = spec0[28] # P11 Filter A/B + _p12 = spec0[29] # P12 Always 0 + _p13 = spec0[30:38] # P13 Always 0 + _p14 = spec0[38:40] # P14 FM Mode + _p15 = spec0[40] # P15 Chan Lockout (Skip) + _p16 = spec0[41:49] # P16 Max 8-Char Name if assigned + + spec1 = "x" + spec1 + _p4s = int(spec1[7:18]) # P4: Offset freq + + datm = "" # Fill in MEM_FORMAT sequence + datm += _make_dat(_p4, 4) # rxreq: u32, 4 bytes/chars + datm += _make_dat(_p4s, 4) # tx freq + v1 = int(_p5) << 4 # xMode: 0-9, upper 4 bits + v1 = v1 | (int(_p6) << 2) # Data: 0/1 + v1 = v1 | int(_p7) # Tmode: 0-3 + datm += chr(v1) + datm += chr(int(_p8)) # rtone: 00-42 + datm += chr(int(_p9)) # ctone + v1 = int(_p11) << 7 # Filter A/B 1 bit msb + v1 = v1 | (int(_p14) << 6) # fmwide: 1 bit + v1 = v1 | (int(_p15) << 5) # skip: 1 bit + datm += chr(v1) + v1 = len(_p16) + for ix in range(8): + if ix < v1: + datm += _p16[ix] + else: + datm += " " + + return datm + + def _make_base_spec(self, mem, freq): + """ Generate memory channel parameter string """ + spec = "%011i%1i%1i%1i%02i%02i000%1i0000000000%02i%1i%s" \ + % (freq, mem.xmode, mem.data, mem.tmode, mem.rtone, + mem.ctone, mem.filter, mem.fmnrw, mem.skip, mem.name) + + return spec.strip() + + def get_settings(self): + """Translate the MEM_FORMAT structs into settings in the UI""" + # Define mem struct write-back shortcuts + _sets = self._memobj.settings + _asf = self._memobj.asf + _ssf = self._memobj.ssf + _eqx = self._memobj.eqx + _mex = self._memobj.exset + _chm = self._memobj.ch_mem + basic = RadioSettingGroup("basic", "Basic Settings") + pvfo = RadioSettingGroup("pvfo", "VFO Band Edges") + mena = RadioSettingGroup("mena", "Menu A") + menb = RadioSettingGroup("menb", "Menu B") + equ = RadioSettingGroup("equ", "Equalizers") + amode = RadioSettingGroup("amode", "Auto Mode") + ssc = RadioSettingGroup("ssc", "Slow Scan") + group = RadioSettings(basic, pvfo, mena, menb, equ, amode, ssc) + + mhz1 = 1000000. + nsg = not self.SG + if nsg: # Make reverse EX_X dictionary + x_ex = dict(zip(self.EX_X.values(), self.EX_X.keys())) + + # Callback functions + def _my_readonly(setting, obj, atrb): + """NOP callback, prevents writing the setting""" + vx = 0 + return + + def my_adjraw(setting, obj, atrb, fix=0, ndx=-1): + """Callback for Integer add or subtract fix from value.""" + vx = int(str(setting.value)) + value = vx + int(fix) + if value < 0: + value = 0 + if ndx < 0: + setattr(obj, atrb, value) + else: + setattr(obj[ndx], atrb, value) + return + + def my_mhz_val(setting, obj, atrb, ndx=-1): + """ Callback to set freq back to Htz""" + vx = float(str(setting.value)) + vx = int(vx * mhz1) + if ndx < 0: + setattr(obj, atrb, vx) + else: + setattr(obj[ndx], atrb, vx) + return + + def my_bool(setting, obj, atrb, ndx=-1): + """ Callback to properly set boolean """ + # set_settings is not setting [indexed] booleans??? + vx = 0 + if str(setting.value) == "True": + vx = 1 + if ndx < 0: + setattr(obj, atrb, vx) + else: + setattr(obj[ndx], atrb, vx) + return + + def my_asf_mode(setting, obj, nx=0): + """ Callback to extract mode and create asmode, asdata """ + v1 = TS590_MODES.index(str(setting.value)) + v2 = 0 # asdata + vx = v1 + 1 # stored as CAT values, same as xmode + if v1 == 7: + vx = 9 + if v1 > 7: # a Data mode + v2 = 1 + if v1 == 8: + vx = 1 # LSB + elif v1 == 9: + vx = 2 # USB + elif v1 == 10: + vx = 4 # FM + setattr(obj[nx], "asdata", v2) + setattr(obj[nx], "asmode", vx) + return + + def my_fnctns(setting, obj, ndx, atrb): + """ Filter only valid key function assignments """ + vx = int(str(setting.value)) + if self.SG: + vmx = 210 + if (vx > 99 and vx < 120) or (vx > 170 and vx < 200): + raise errors.RadioError(" %i Change Ignored for %s." + % (vx, atrb)) + return # not valid, ignored + else: + vmx = 208 + if (vx > 87 and vx < 100) or (vx > 134 and vx < 200): + raise errors.RadioError(" %i Change Ignored for %s." + % (vx, atrb)) + return + if vx > vmx: + vx = 255 # Off + setattr(obj[ndx], atrb, vx) + return + + def my_labels(kx): # nsg and x_ex defined above + lbl = "%03i:" % kx # SG EX number + if nsg: + lbl = x_ex["EX%03i" % kx][2:] + ":" # S-model EX num + lbl += self.EX_LBL[kx] # and the label to match + return lbl + + # ===== BASIC GROUP ===== + sx = "" + for i in range(4): + sx += chr(_sets.fv[i]) + rx = RadioSettingValueString(0, 4, sx) + rset = RadioSetting("settings.fv", "FirmwareVersion", rx) + rset.set_apply_callback(_my_readonly, _sets, "fv") + basic.append(rset) + + rx = RadioSettingValueInteger(0, 255, _sets.ag + 1) + rset = RadioSetting("settings.ag", "AF Gain", rx) + rset.set_apply_callback(my_adjraw, _sets, "ag", -1) + basic.append(rset) + + rx = RadioSettingValueInteger(0, 255, _sets.rg + 1) + rset = RadioSetting("settings.rg", "RF Gain", rx) + rset.set_apply_callback(my_adjraw, _sets, "rg", -1) + basic.append(rset) + + options = ["ANT1", "ANT2"] + # CAUTION: an1 has value of 1 or 2 + rx = RadioSettingValueList(options, options[_sets.an1 - 1]) + rset = RadioSetting("settings.an1", "Antenna Selected", rx) + # Add 1 to the changed value. S/b 1/2 + rset.set_apply_callback(my_val_list, options, _sets, "an1", 1) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.an2)) + rset = RadioSetting("settings.an2", "Recv Antenna is used", rx) + basic.append(rset) + + rx = RadioSettingValueBoolean(bool(_sets.an3)) + rset = RadioSetting("settings.an3", "Drive Out On", rx) + basic.append(rset) + + rx = RadioSettingValueInteger(0, 100, _sets.mg) + rset = RadioSetting("settings.mg", "Microphone gain", rx) + basic.append(rset) + + nx = 5 # Coarse step + if bool(_mex[0].ex054): # Power Fine enabled in menu A + nx = 1 + vx = _sets.pc # Trap invalid values from run_tests.py + if vx < 5: + vx = 5 + rx = RadioSettingValueInteger(5, 100, vx, nx) + sx = "TX Output power (Watts)" + rset = RadioSetting("settings.pc", sx, rx) + basic.append(rset) + + vx = _sets.tp + rx = RadioSettingValueInteger(5, 100, vx, nx) + sx = "TX Tuning power (Watts)" + rset = RadioSetting("settings.tp", sx, rx) + basic.append(rset) + + val = _sets.fa / mhz1 # Allow Rx freq range + rx = RadioSettingValueFloat(0.3, 60.0, val, 0.001, 3) + sx = "VFO-A Frequency (MHz)" + rset = RadioSetting("settings.fa", sx, rx) + rset.set_apply_callback(my_mhz_val, _sets, "fa") + basic.append(rset) + + val = _sets.fb / mhz1 + rx = RadioSettingValueFloat(0.3, 60.0, val, 0.001, 3) + sx = "VFO-B Frequency (MHz)" + rset = RadioSetting("settings.fb", sx, rx) + rset.set_apply_callback(my_mhz_val, _sets, "fb") + basic.append(rset) + + options = ["Menu A", "Menu B"] + rx = RadioSettingValueList(options, options[_sets.mf]) + sx = "Menu Selected" + rset = RadioSetting("settings.mf", sx, rx) + rset.set_apply_callback(my_val_list, options, _sets, "mf") + basic.append(rset) + + # ==== VFO Edges Group ================ + + for mx in range(100, 110): + val = _chm[mx].rxfreq / mhz1 + if val < 1.8: # Many operators never use this + val = 1.8 # So default is 0.0 + rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3) + sx = "VFO-Band %i lower limit (MHz)" % (mx - 100) + rset = RadioSetting("ch_mem.rxfreq/%d" % mx, sx, rx) + rset.set_apply_callback(my_mhz_val, _chm, "rxfreq", mx) + pvfo.append(rset) + + val = _chm[mx].txfreq / mhz1 + if val < 1.8: + val = 54.0 + rx = RadioSettingValueFloat(1.8, 54.0, val, 0.001, 3) + sx = " VFO-Band %i upper limit (MHz)" % (mx - 100) + rset = RadioSetting("ch_mem.txfreq/%d" % mx, sx, rx) + rset.set_apply_callback(my_mhz_val, _chm, "txfreq", mx) + pvfo.append(rset) + + kx = _chm[mx].xmode + options = ["None", "LSB", "USB", "CW", "FM", "AM", "FSK", + "CW-R", "N/A", "FSK-R"] + rx = RadioSettingValueList(options, options[kx]) + sx = " VFO-Band %i Tx/Rx Mode" % (mx - 100) + rset = RadioSetting("ch_mem.xmode/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _chm, + "xmode", 0, mx) + pvfo.append(rset) + + # ==== Menu A/B Group ================= + + for mx in range(2): # A/B index + sx = "" + for i in range(8): + if int(_mex[mx].ex001[i]) != 0: + sx += chr(_mex[mx].ex001[i]) + sx = sx.strip() + rx = RadioSettingValueString(0, 8, sx) + sx = my_labels(1) # Proper label for EX001 + rset = RadioSetting("exset.ex001/%d" % mx, sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + sx = my_labels(2) + rx = RadioSettingValueInteger(0, 6, _mex[mx].ex002) + rset = RadioSetting("exset.ex002", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + nx = 2 + if self.SG: + nx = 10 + vx = _mex[mx].ex003 + 1 # radio rtns 0-9 + rx = RadioSettingValueInteger(1, nx, vx) + sx = my_labels(3) + rset = RadioSetting("exset.ex003", sx, rx) + rset.set_apply_callback(my_adjraw, _mex, "ex003", -1, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + nx = 9 + if self.SG: + nx = 20 + rx = RadioSettingValueInteger(0, nx, _mex[mx].ex005) + sx = my_labels(5) + rset = RadioSetting("exset.ex005", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + sx = my_labels(6) + rx = RadioSettingValueInteger(0, nx, _mex[mx].ex006) + rset = RadioSetting("exset.ex006", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + sx = my_labels(7) + rx = RadioSettingValueInteger(0, nx, _mex[mx].ex007) + rset = RadioSetting("exset.ex007", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + nx = 7 + if self.SG: + nx = 20 + sx = my_labels(8) + rx = RadioSettingValueInteger(0, nx, _mex[mx].ex008) + rset = RadioSetting("exset.ex008", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + sx = my_labels(9) + rx = RadioSettingValueInteger(0, 4, _mex[mx].ex009) + rset = RadioSetting("exset.ex009", sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["English", "Japanese"] + rx = RadioSettingValueList(options, options[_mex[mx].ex010]) + sx = my_labels(10) + rset = RadioSetting("exset.ex010/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex010", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["Off", "1", "2"] + rx = RadioSettingValueList(options, options[_mex[mx].ex011]) + sx = my_labels(11) + rset = RadioSetting("exset.ex011/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex011", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["0.1", "0.5", "1.0"] + rx = RadioSettingValueList(options, options[_mex[mx].ex012]) + sx = my_labels(12) + rset = RadioSetting("exset.ex012", sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex012", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["250", "500", "1000"] + rx = RadioSettingValueList(options, options[_mex[mx].ex013]) + sx = my_labels(13) + rset = RadioSetting("exset.ex013/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex013", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + # S and SG have different ranges for control steps + options = ["0.5", "1.0", "2.5", "5.0", "10.0"] + if self.SG: + options = ["Off", "0.5", "0.5", "1.0", "2.5", + "5.0", "10.0"] + rx = RadioSettingValueList(options, options[_mex[mx].ex016]) + sx = my_labels(16) + if nsg: + sx = "014: Tuning step for SSB/CW/FSK (KHz)" + rset = RadioSetting("exset.ex016/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex016", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + if self.SG: # this setting only for SG + rx = RadioSettingValueList(options, + options[_mex[mx].ex017]) + + sx = my_labels(17) + rset = RadioSetting("exset.ex017/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex017", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["Off", "5.0", "6.25", "10.0", "12.5", "15.0", + "20.0", "25.0", "30.0", "50.0", "100.0"] + if self.SG: + options.remove("Off") + rx = RadioSettingValueList(options, options[_mex[mx].ex018]) + sx = my_labels(18) + rset = RadioSetting("exset.ex018/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex018", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueList(options, options[_mex[mx].ex019]) + sx = my_labels(19) + rset = RadioSetting("exset.ex019/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex019", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["3", "5", "10"] + rx = RadioSettingValueList(options, options[_mex[mx].ex021]) + sx = my_labels(21) + rset = RadioSetting("exset.ex021/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex021", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex022)) + sx = my_labels(22) + rset = RadioSetting("exset.ex022/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex022", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex023)) + sx = my_labels(23) + rset = RadioSetting("exset.ex023/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex023", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["100", "200", "300", "400", "500"] + rx = RadioSettingValueList(options, options[_mex[mx].ex024]) + sx = my_labels(24) + rset = RadioSetting("exset.ex024/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex024", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex025)) + sx = my_labels(25) + rset = RadioSetting("exset.ex025/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex025", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["TO", "CO"] + rx = RadioSettingValueList(options, options[_mex[mx].ex026]) + sx = my_labels(26) + rset = RadioSetting("exset.ex026/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex026", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex054)) + sx = my_labels(54) + rset = RadioSetting("exset.ex054/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex054", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + options = ["Off", "3", "5", "10", "20", "30"] + rx = RadioSettingValueList(options, options[_mex[mx].ex055]) + sx = my_labels(55) + rset = RadioSetting("exset.ex055/%d" % mx, sx, rx) + rset.set_apply_callback(my_val_list, options, _mex, + "ex055", 0, mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueBoolean(bool(_mex[mx].ex076)) + sx = my_labels(76) + rset = RadioSetting("exset.ex076/%d" % mx, sx, rx) + rset.set_apply_callback(my_bool, _mex, "ex076", mx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 100, _mex[mx].ex077, 5) + sx = my_labels(77) + rset = RadioSetting("exset.ex077/%d" % mx, sx, rx) + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex087) + sx = my_labels(87) + rset = RadioSetting("exset.ex087/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex087") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex088) + sx = my_labels(88) + rset = RadioSetting("exset.ex088/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex088") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + if self.SG: # Next 5 settings not supported in 590S + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex089) + sx = my_labels(89) + rset = RadioSetting("exset.ex089/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex089") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex090) + sx = my_labels(90) + rset = RadioSetting("exset.ex090/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex090") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex091) + sx = my_labels(91) + rset = RadioSetting("exset.ex091/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex091") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex092) + sx = my_labels(92) + rset = RadioSetting("exset.ex092/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex092") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex093) + sx = my_labels(93) + rset = RadioSetting("exset.ex093/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex093") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + # Now both S and SG models + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex094) + sx = my_labels(94) + rset = RadioSetting("exset.ex094/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex094") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex095) + sx = my_labels(95) + rset = RadioSetting("exset.ex095/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex095") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex096) + sx = my_labels(96) + rset = RadioSetting("exset.ex096/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex096") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex097) + sx = my_labels(97) + rset = RadioSetting("exset.ex097/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex097") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex098) + sx = my_labels(98) + rset = RadioSetting("exset.ex098/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex098") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + + rx = RadioSettingValueInteger(0, 256, _mex[mx].ex099) + sx = my_labels(99) + rset = RadioSetting("exset.ex099/%d" % mx, sx, rx) + rset.set_apply_callback(my_fnctns, _mex, mx, "ex099") + if mx == 0: + mena.append(rset) + else: + menb.append(rset) + # End of for mx loop + + # ==== Auto Scan Params (amode) ============== + for ix in range(32): + val = _asf[ix].asfreq / mhz1 + rx = RadioSettingValueFloat(0.03, 60.0, val, 0.001, 3) + rset = RadioSetting("asf.asfreq/%d" % ix, + "Scan %02i Freq (MHz)" % ix, rx) + rset.set_apply_callback(my_mhz_val, _asf, "asfreq", ix) + amode.append(rset) + + mx = _asf[ix].asmode - 1 # Same logic as xmode + if _asf[ix].asmode == 9: + mx = 7 + if _asf[ix].asdata: + if _asf[ix].asmode == 1: + mx = 8 + elif _asf[ix].asmode == 2: + mx = 9 + elif _asf[ix].asmode == 4: + mx = 10 + rx = RadioSettingValueList(TS590_MODES, TS590_MODES[mx]) + rset = RadioSetting("asf.asmode/%d" % ix, " Mode", rx) + rset.set_apply_callback(my_asf_mode, _asf, ix) + amode.append(rset) + + # ==== Slow Scan Settings === + for ix in range(10): # Chans + for nx in range(5): # spots + px = ((ix * 5) + nx) + val = _ssf[px].ssfreq / mhz1 + stx = " - - - Slot %02i Freq (MHz)" % nx + if nx == 0: + stx = "Slow Scan %02i, Slot 0 Freq (MHz" % ix + rx = RadioSettingValueFloat(0, 54.0, val, 0.001, 3) + rset = RadioSetting("ssf.ssfreq/%d" % px, stx, rx) + rset.set_apply_callback(my_mhz_val, _ssf, "ssfreq", px) + ssc.append(rset) + + # ==== Equalizer subgroup ===== + mohd = ["SSB", "SSB-DATA", "CW/CW-R", "FM", "FM-DATA", "AM", + "AM-DATA", "FSK/FSK-R"] + tcurves = ["Off", "HB1", "HB2", "FP", "BB1", "BB2", + "C", "U"] + rcurves = ["Off", "HB1", "HB2", "FP", "BB1", "BB2", + "FLAT", "U"] + for ix in range(8): + rx = RadioSettingValueList(tcurves, tcurves[_eqx[ix].txeq]) + rset = RadioSetting("eqx.txeq/%d" % ix, "TX %s Equalizer" + % mohd[ix], rx) + rset.set_apply_callback(my_val_list, tcurves, _eqx, + "txeq", 0, ix) + equ.append(rset) + + rx = RadioSettingValueList(rcurves, rcurves[_eqx[ix].rxeq]) + rset = RadioSetting("eqx.rxeq/%d" % ix, "RX %s Equalizer" + % mohd[ix], rx) + rset.set_apply_callback(my_val_list, rcurves, _eqx, + "rxeq", 0, ix) + equ.append(rset) + + return group # END get_settings() + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, fdata, fyle): + """ Included to prevent 'File > New' error """ + return False + + +@directory.register +class TS590SRadio(TS590Radio): + """ Kenwood TS-590S variant of the TS590 """ + VENDOR = "Kenwood" + MODEL = "TS-590S_CloneMode" + ID = "ID021;" + SG = False + # This is the equivalent Menu A/B list for the TS-590S + # The equivalnt S param is stored in the SG Mem_Format slot + EX = ["EX087", "EX000", "EX001", "EX003", "EX004", "EX005", + "EX006", "EX007", "EX008", "EX009", "EX010", "EX011", "EX014", + "EX014", "EX015", "EX016", "EX017", "EX018", "EX019", "EX020", + "EX021", "EX022", "EX048", "EX049", "EX069", "EX070", "EX079", + "EX080", "EX080", "EX080", "EX080", "EX080", "EX080", "EX081", + "EX082", "EX083", "EX084", "EX085", "EX086", "MF1"] + # EX cross reference dictionary- key is S param, value is the SG + EX_X = {"EX087": "EX001", "EX000": "EX002", "EX001": "EX003", + "EX003": "EX005", "EX004": "EX006", "EX005": "EX007", + "EX006": "EX008", "EX007": "EX009", "EX008": "EX010", + "EX009": "EX011", "EX010": "EX012", "EX011": "EX013", + "EX014": "EX016", "EX015": "EX018", "EX081": "EX094", + "EX016": "EX019", "EX017": "EX021", "EX018": "EX022", + "EX019": "EX023", + "EX020": "EX024", "EX021": "EX025", "EX022": "EX026", + "EX048": "EX054", "EX049": "EX055", "EX069": "EX076", + "EX070": "EX077", "EX079": "EX087", "EX080": "EX088", + "EX082": "EX095", "EX083": "EX096", "EX084": "EX097", + "EX085": "EX098", "EX086": "EX099"} diff --git a/chirp/drivers/ts850.py b/chirp/drivers/ts850.py new file mode 100644 index 0000000..4520aca --- /dev/null +++ b/chirp/drivers/ts850.py @@ -0,0 +1,254 @@ +# Copyright 2018 Antony Jordan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +from chirp import chirp_common, directory, errors +from chirp.drivers.kenwood_live import KenwoodLiveRadio, \ + command, iserr, NOCACHE + +LOG = logging.getLogger(__name__) + +TS850_DUPLEX = ["off", "split"] +TS850_TMODES = ["", "Tone"] +TS850_SKIP = ["", "S"] + +TS850_MODES = { + "N/A": " ", + "N/A": "0", + "LSB": "1", + "USB": "2", + "CW": "3", + "FM": "4", + "AM": "5", + "FSK": "6", + "CW-R": "7", + "FSK-R": "9", +} +TS850_MODES_REV = {val: mode for mode, val in TS850_MODES.iteritems()} + +TS850_TONES = list(chirp_common.OLD_TONES) +TS850_TONES.remove(69.3) + +TS850_BANDS = [ + (1800000, 2000000), # 160M Band + (3500000, 4000000), # 80M Band + (7000000, 7300000), # 40M Band + (10100000, 10150000), # 30M Band + (14000000, 14350000), # 20M Band + (18068000, 18168000), # 17M Band + (21000000, 21450000), # 15M Band + (24890000, 24990000), # 12M Band + (28000000, 29700000) # 10M Band +] + + +@directory.register +class TS850Radio(KenwoodLiveRadio): + """Kenwood TS-850""" + MODEL = "TS-850" + BAUD_RATE = 4800 + + _upper = 99 + _kenwood_valid_tones = list(TS850_TONES) + + def get_features(self): + rf = chirp_common.RadioFeatures() + + rf.can_odd_split = True + + rf.has_bank = False + rf.has_ctone = False + rf.has_dtcs = False + rf.has_dtcs_polarity = False + rf.has_name = False + rf.has_tuning_step = False + + rf.memory_bounds = (0, self._upper) + + rf.valid_bands = TS850_BANDS + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + rf.valid_duplexes = TS850_DUPLEX + rf.valid_modes = TS850_MODES.keys() + rf.valid_skips = TS850_SKIP + rf.valid_tmodes = TS850_TMODES + + return rf + + def get_memory(self, number): + if number < 0 or number > self._upper: + raise errors.InvalidMemoryLocation( + "Number must be between 0 and %i" % self._upper) + if number in self._memcache and not NOCACHE: + return self._memcache[number] + + result = command(self.pipe, *self._cmd_get_memory(number)) + + if result == "N": + mem = chirp_common.Memory() + mem.number = number + mem.empty = True + self._memcache[mem.number] = mem + return mem + + mem = self._parse_mem_spec(result) + self._memcache[mem.number] = mem + + # check for split frequency operation + result = command(self.pipe, *self._cmd_get_split(number)) + self._parse_split_spec(mem, result) + + return mem + + def set_memory(self, memory): + if memory.number < 0 or memory.number > self._upper: + raise errors.InvalidMemoryLocation( + "Number must be between 0 and %i" % self._upper) + + if memory.number > 90: + if memory.duplex == TS850_DUPLEX[0]: + memory.duplex = TS850_DUPLEX[1] + memory.offset = memory.freq + else: + if memory.freq > memory.offset: + temp = memory.freq + memory.freq = memory.offset + memory.offset = temp + + # Clear out memory contents to prevent errors + spec = self._make_base_spec(memory, 0) + spec = "".join(spec) + result = command(self.pipe, *self._cmd_set_memory(memory.number, spec)) + + if iserr(result): + raise errors.InvalidDataError("Radio refused %i" % + memory.number) + + # If we have a split set the transmit frequency first. + if memory.duplex == TS850_DUPLEX[1]: + spec = "".join(self._make_split_spec(memory)) + result = command(self.pipe, *self._cmd_set_split(memory.number, + spec)) + if iserr(result): + raise errors.InvalidDataError("Radio refused %i" % + memory.number) + + spec = self._make_mem_spec(memory) + spec = "".join(spec) + result = command(self.pipe, *self._cmd_set_memory(memory.number, spec)) + if iserr(result): + raise errors.InvalidDataError("Radio refused %i" % memory.number) + + def erase_memory(self, number): + if number not in self._memcache: + return + + resp = command(self.pipe, *self._cmd_erase_memory(number)) + if iserr(resp): + raise errors.RadioError("Radio refused delete of %i" % number) + + del self._memcache[number] + + def _cmd_get_memory(self, number): + return "MR", "0 %02i" % number + + def _cmd_get_split(self, number): + return "MR", "1 %02i" % number + + def _cmd_get_memory_name(self, number): + LOG.error("TS-850 does not support memory channel names") + return "" + + def _cmd_set_memory(self, number, spec): + return "MW", "0 %02i%s" % (number, spec) + + def _cmd_set_split(self, number, spec): + return "MW", "1 %02i%s" % (number, spec) + + def _cmd_set_memory_name(self, number, name): + LOG.error("TS-850 does not support memory channel names") + return "" + + def _cmd_erase_memory(self, number): + return "MW0 %02i%014i " % (number, 0) + + def _parse_mem_spec(self, spec): + mem = chirp_common.Memory() + + # pad string so indexes match Kenwood docs + spec = " " + spec # Param Format Function + + _p1 = spec[3] # P1 9 Split Specification + # _p2 = spec[4] # P2 - Blank + _p3 = spec[5:7] # P3 7 Memory Channel + _p4 = spec[7:18] # P4 4 Frequency + _p5 = spec[18] # P5 2 Mode + _p6 = spec[19] # P6 10 Memory Lockout + _p7 = spec[20] # P7 1 Tone On/Off + _p8 = spec[21:23] # P8 14 Tone Frequency + # _p9 = spec[23] # P9 - Blank + + mem.duplex = TS850_DUPLEX[int(_p1)] + mem.number = int(_p3) + mem.freq = int(_p4) + mem.mode = TS850_MODES_REV[_p5] + mem.skip = TS850_SKIP[int(_p6)] + mem.tmode = TS850_TMODES[int(_p7)] + + if mem.tmode == TS850_TMODES[1]: + mem.rtone = TS850_TONES[int(_p8)-1] + + return mem + + def _parse_split_spec(self, mem, spec): + + # pad string so indexes match Kenwood docs + spec = " " + spec + + split_freq = int(spec[7:18]) # P4 + + if mem.freq != 0: + mem.duplex = "split" + mem.offset = split_freq + + return mem + + def _make_base_spec(self, mem, freq): + if mem.mode == "FM" \ + and mem.duplex == TS850_DUPLEX[1] \ + and mem.tmode == TS850_TMODES[1]: + tmode = "1" + tone = "%02i" % (TS850_TONES.index(mem.rtone)+1) + else: + tmode = "0" + tone = " " + + spec = ( # Param Format Function + "%011i" % freq, # P4 4 Frequency + TS850_MODES[mem.mode], # P5 2 Mode + # (Except Tune) + "%i" % (mem.skip == TS850_SKIP[1]), # P6 10 Memory Lockout + tmode, # P7 1 Tone On/Off + tone, # P8 1 Tone Frequency + " " # P9 14 Padding + ) + + return spec + + def _make_mem_spec(self, mem): + return self._make_base_spec(mem, mem.freq) + + def _make_split_spec(self, mem): + return self._make_base_spec(mem, mem.offset) diff --git a/chirp/drivers/uv5r.py b/chirp/drivers/uv5r.py new file mode 100644 index 0000000..ea40d02 --- /dev/null +++ b/chirp/drivers/uv5r.py @@ -0,0 +1,1939 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import struct +import time +import os +import logging + +from chirp import chirp_common, errors, util, directory, memmap +from chirp import bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError, RadioSettings +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0008; +struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unused1:3, + isuhf:1, + scode:4; + u8 unknown1:7, + txtoneicon:1; + u8 mailicon:3, + unknown2:3, + lowpower:2; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; +} memory[128]; + +#seekto 0x0B08; +struct { + u8 code[5]; + u8 unused[11]; +} pttid[15]; + +#seekto 0x0C88; +struct { + u8 code222[3]; + u8 unused222[2]; + u8 code333[3]; + u8 unused333[2]; + u8 alarmcode[3]; + u8 unused119[2]; + u8 unknown1; + u8 code555[3]; + u8 unused555[2]; + u8 code666[3]; + u8 unused666[2]; + u8 code777[3]; + u8 unused777[2]; + u8 unknown2; + u8 code60606[5]; + u8 code70707[5]; + u8 code[5]; + u8 unused1:6, + aniid:2; + u8 unknown[2]; + u8 dtmfon; + u8 dtmfoff; +} ani; + +#seekto 0x0E28; +struct { + u8 squelch; + u8 step; + u8 unknown1; + u8 save; + u8 vox; + u8 unknown2; + u8 abr; + u8 tdr; + u8 beep; + u8 timeout; + u8 unknown3[4]; + u8 voice; + u8 unknown4; + u8 dtmfst; + u8 unknown5; + u8 unknown12:6, + screv:2; + u8 pttid; + u8 pttlt; + u8 mdfa; + u8 mdfb; + u8 bcl; + u8 autolk; // NOTE: The UV-6 calls this byte voxenable, but the UV-5R + // calls it autolk. Since this is a minor difference, it will + // be referred to by the wrong name for the UV-6. + u8 sftd; + u8 unknown6[3]; + u8 wtled; + u8 rxled; + u8 txled; + u8 almod; + u8 band; + u8 tdrab; + u8 ste; + u8 rpste; + u8 rptrl; + u8 ponmsg; + u8 roger; + u8 rogerrx; + u8 tdrch; // NOTE: The UV-82HP calls this byte rtone, but the UV-6 + // calls it tdrch. Since this is a minor difference, it will + // be referred to by the wrong name for the UV-82HP. + u8 displayab:1, + unknown1:2, + fmradio:1, + alarm:1, + unknown2:1, + reset:1, + menu:1; + u8 unknown1:6, + singleptt:1, + vfomrlock:1; + u8 workmode; + u8 keylock; +} settings; + +#seekto 0x0E7E; +struct { + u8 unused1:1, + mrcha:7; + u8 unused2:1, + mrchb:7; +} wmchannel; + +#seekto 0x0F10; +struct { + u8 freq[8]; + u8 unknown1; + u8 offset[4]; + u8 unknown2; + ul16 rxtone; + ul16 txtone; + u8 unused1:7, + band:1; + u8 unknown3; + u8 unused2:2, + sftd:2, + scode:4; + u8 unknown4; + u8 unused3:1 + step:3, + unused4:4; + u8 txpower:1, + widenarr:1, + unknown5:4, + txpower3:2; +} vfoa; + +#seekto 0x0F30; +struct { + u8 freq[8]; + u8 unknown1; + u8 offset[4]; + u8 unknown2; + ul16 rxtone; + ul16 txtone; + u8 unused1:7, + band:1; + u8 unknown3; + u8 unused2:2, + sftd:2, + scode:4; + u8 unknown4; + u8 unused3:1 + step:3, + unused4:4; + u8 txpower:1, + widenarr:1, + unknown5:4, + txpower3:2; +} vfob; + +#seekto 0x0F56; +u16 fm_presets; + +#seekto 0x1008; +struct { + char name[7]; + u8 unknown2[9]; +} names[128]; + +#seekto 0x1818; +struct { + char line1[7]; + char line2[7]; +} sixpoweron_msg; + +#seekto 0x%04X; +struct { + char line1[7]; + char line2[7]; +} poweron_msg; + +#seekto 0x1838; +struct { + char line1[7]; + char line2[7]; +} firmware_msg; + +struct limit { + u8 enable; + bbcd lower[2]; + bbcd upper[2]; +}; + +#seekto 0x1908; +struct { + struct limit vhf; + struct limit uhf; +} limits_new; + +#seekto 0x1910; +struct { + u8 unknown1[2]; + struct limit vhf; + u8 unknown2; + u8 unknown3[8]; + u8 unknown4[2]; + struct limit uhf; +} limits_old; + +struct squelch { + u8 sql0; + u8 sql1; + u8 sql2; + u8 sql3; + u8 sql4; + u8 sql5; + u8 sql6; + u8 sql7; + u8 sql8; + u8 sql9; +}; + +#seekto 0x18A8; +struct { + struct squelch vhf; + u8 unknown1[6]; + u8 unknown2[16]; + struct squelch uhf; +} squelch_new; + +#seekto 0x18E8; +struct { + struct squelch vhf; + u8 unknown[6]; + struct squelch uhf; +} squelch_old; + +""" + +# 0x1EC0 - 0x2000 + +vhf_220_radio = bytes(b"\x02") + +BASETYPE_UV5R = [b"BFS", b"BFB", b"N5R-2", b"N5R2", b"N5RV", b"BTS", b"D5R2", + b"B5R2"] +BASETYPE_F11 = [b"USA"] +BASETYPE_UV82 = [b"US2S2", b"B82S", b"BF82", b"N82-2", b"N822"] +BASETYPE_BJ55 = [b"BJ55"] # needed for for the Baojie UV-55 in bjuv55.py +BASETYPE_UV6 = [b"BF1", b"UV6"] +BASETYPE_KT980HP = [b"BFP3V3 B"] +BASETYPE_F8HP = [b"BFP3V3 F", b"N5R-3", b"N5R3", b"F5R3", b"BFT"] +BASETYPE_UV82HP = [b"N82-3", b"N823", b"N5R2"] +BASETYPE_UV82X3 = [b"HN5RV01"] +BASETYPE_LIST = BASETYPE_UV5R + BASETYPE_F11 + BASETYPE_UV82 + \ + BASETYPE_BJ55 + BASETYPE_UV6 + BASETYPE_KT980HP + \ + BASETYPE_F8HP + BASETYPE_UV82HP + BASETYPE_UV82X3 + +AB_LIST = ["A", "B"] +ALMOD_LIST = ["Site", "Tone", "Code"] +BANDWIDTH_LIST = ["Wide", "Narrow"] +COLOR_LIST = ["Off", "Blue", "Orange", "Purple"] +DTMFSPEED_LIST = ["%s ms" % x for x in range(50, 2010, 10)] +DTMFST_LIST = ["OFF", "DT-ST", "ANI-ST", "DT+ANI"] +MODE_LIST = ["Channel", "Name", "Frequency"] +PONMSG_LIST = ["Full", "Message"] +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)] +RTONE_LIST = ["1000 Hz", "1450 Hz", "1750 Hz", "2100Hz"] +RESUME_LIST = ["TO", "CO", "SE"] +ROGERRX_LIST = ["Off"] + AB_LIST +RPSTE_LIST = ["OFF"] + ["%s" % x for x in range(1, 11)] +SAVE_LIST = ["Off", "1:1", "1:2", "1:3", "1:4"] +SCODE_LIST = ["%s" % x for x in range(1, 16)] +SHIFTD_LIST = ["Off", "+", "-"] +STEDELAY_LIST = ["OFF"] + ["%s ms" % x for x in range(100, 1100, 100)] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0] +STEP_LIST = [str(x) for x in STEPS] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] +STEP291_LIST = [str(x) for x in STEPS] +TDRAB_LIST = ["Off"] + AB_LIST +TDRCH_LIST = ["CH%s" % x for x in range(1, 129)] +TIMEOUT_LIST = ["%s sec" % x for x in range(15, 615, 15)] +TXPOWER_LIST = ["High", "Low"] +TXPOWER3_LIST = ["High", "Mid", "Low"] +VOICE_LIST = ["Off", "English", "Chinese"] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 11)] +WORKMODE_LIST = ["Frequency", "Channel"] + +SETTING_LISTS = { + "almod": ALMOD_LIST, + "aniid": PTTID_LIST, + "displayab": AB_LIST, + "dtmfst": DTMFST_LIST, + "dtmfspeed": DTMFSPEED_LIST, + "mdfa": MODE_LIST, + "mdfb": MODE_LIST, + "ponmsg": PONMSG_LIST, + "pttid": PTTID_LIST, + "rtone": RTONE_LIST, + "rogerrx": ROGERRX_LIST, + "rpste": RPSTE_LIST, + "rxled": COLOR_LIST, + "save": SAVE_LIST, + "scode": PTTIDCODE_LIST, + "screv": RESUME_LIST, + "sftd": SHIFTD_LIST, + "stedelay": STEDELAY_LIST, + "step": STEP_LIST, + "step291": STEP291_LIST, + "tdrab": TDRAB_LIST, + "tdrch": TDRCH_LIST, + "timeout": TIMEOUT_LIST, + "txled": COLOR_LIST, + "txpower": TXPOWER_LIST, + "txpower3": TXPOWER3_LIST, + "voice": VOICE_LIST, + "vox": VOX_LIST, + "widenarr": BANDWIDTH_LIST, + "workmode": WORKMODE_LIST, + "wtled": COLOR_LIST +} + + +def _do_status(radio, block): + status = chirp_common.Status() + status.msg = "Cloning" + status.cur = block + status.max = radio.get_memsize() + radio.status_fn(status) + +UV5R_MODEL_ORIG = bytes(b"\x50\xBB\xFF\x01\x25\x98\x4D") +UV5R_MODEL_291 = bytes(b"\x50\xBB\xFF\x20\x12\x07\x25") +UV5R_MODEL_F11 = bytes(b"\x50\xBB\xFF\x13\xA1\x11\xDD") +UV5R_MODEL_UV82 = bytes(b"\x50\xBB\xFF\x20\x13\x01\x05") +UV5R_MODEL_UV6 = bytes(b"\x50\xBB\xFF\x20\x12\x08\x23") +UV5R_MODEL_UV6_ORIG = bytes(b"\x50\xBB\xFF\x12\x03\x98\x4D") +UV5R_MODEL_A58 = bytes(b"\x50\xBB\xFF\x20\x14\x04\x13") + + +def _upper_band_from_data(data): + return data[0x03:0x04] + + +def _upper_band_from_image(radio): + return _upper_band_from_data(radio.get_mmap()) + + +def _firmware_version_from_data(data, version_start, version_stop): + version_tag = data[version_start:version_stop] + return version_tag + + +def _firmware_version_from_image(radio): + version = _firmware_version_from_data( + radio.get_mmap().get_byte_compatible(), + radio._fw_ver_file_start, + radio._fw_ver_file_stop) + LOG.debug("_firmware_version_from_image: " + util.hexprint(version)) + return version + + +def _do_ident(radio, magic, secondack=True): + serial = radio.pipe + serial.timeout = 1 + + LOG.info("Sending Magic: %s" % util.hexprint(magic)) + for byte in magic: + serial.write(bytes([byte])) + time.sleep(0.01) + ack = serial.read(1) + + if ack != b"\x06": + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond") + + serial.write(bytes(b"\x02")) + + # Until recently, the "ident" returned by the radios supported by this + # driver have always been 8 bytes long. The image sturcture is the 8 byte + # "ident" followed by the downloaded memory data. So all of the settings + # structures are offset by 8 bytes. The ident returned from a UV-6 radio + # can be 8 bytes (original model) or now 12 bytes. + # + # To accomodate this, the "ident" is now read one byte at a time until the + # last byte ("\xdd") is encountered. The bytes containing the value "\x01" + # are discarded to shrink the "ident" length down to 8 bytes to keep the + # image data aligned with the existing settings structures. + + # Ok, get the response + response = bytes(b"") + for i in range(1, 13): + byte = serial.read(1) + response += byte + # stop reading once the last byte ("\xdd") is encountered + if byte == b"\xDD": + break + + # check if response is OK + if len(response) in [8, 12]: + # DEBUG + LOG.info("Valid response, got this:") + LOG.debug(util.hexprint(response)) + if len(response) == 12: + ident = (bytes([response[0], response[3], response[5]]) + + response[7:]) + else: + ident = response + else: + # bad response + msg = "Unexpected response, got this:" + msg += util.hexprint(response) + LOG.debug(msg) + raise errors.RadioError("Unexpected response from radio.") + + if secondack: + serial.write(b"\x06") + ack = serial.read(1) + if ack != b"\x06": + raise errors.RadioError("Radio refused clone") + + return ident + + +def _read_block(radio, start, size, first_command=False): + msg = struct.pack(">BHB", ord("S"), start, size) + radio.pipe.write(msg) + + if first_command is False: + ack = radio.pipe.read(1) + if ack != b"\x06": + raise errors.RadioError( + "Radio refused to send second block 0x%04x" % start) + + answer = radio.pipe.read(4) + if len(answer) != 4: + raise errors.RadioError("Radio refused to send block 0x%04x" % start) + + cmd, addr, length = struct.unpack(">BHB", answer) + if cmd != ord("X") or addr != start or length != size: + LOG.error("Invalid answer for block 0x%04x:" % start) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (cmd, addr, length)) + raise errors.RadioError("Unknown response from radio") + + chunk = radio.pipe.read(0x40) + if not chunk: + raise errors.RadioError("Radio did not send block 0x%04x" % start) + elif len(chunk) != size: + LOG.error("Chunk length was 0x%04i" % len(chunk)) + raise errors.RadioError("Radio sent incomplete block 0x%04x" % start) + + radio.pipe.write(b"\x06") + time.sleep(0.05) + + return chunk + + +def _get_radio_firmware_version(radio): + if radio.MODEL == "BJ-UV55": + block = _read_block(radio, 0x1FF0, 0x40, True) + version = block[0:6] + else: + block1 = _read_block(radio, 0x1EC0, 0x40, True) + block2 = _read_block(radio, 0x1F00, 0x40, False) + block = block1 + block2 + version = block[48:62] + return version + + +IDENT_BLACKLIST = { + b"\x50\x0D\x0C\x20\x16\x03\x28": "Radio identifies as BTECH UV-5X3", +} + + +def _ident_radio(radio): + for magic in radio._idents: + error = None + try: + data = _do_ident(radio, magic) + return data + except errors.RadioError as e: + LOG.error("uv5r._ident_radio: %s", e) + error = e + time.sleep(2) + + for magic, reason in list(IDENT_BLACKLIST.items()): + try: + _do_ident(radio, magic, secondack=False) + except errors.RadioError as e: + # No match, try the next one + continue + + # If we got here, it means we identified the radio as + # something other than one of our valid idents. Warn + # the user so they can do the right thing. + LOG.warning(('Identified radio as a blacklisted model ' + '(details: %s)') % reason) + raise errors.RadioError(('%s. Please choose the proper vendor/' + 'model and try again.') % reason) + + if error: + raise error + raise errors.RadioError("Radio did not respond") + + +def _do_download(radio): + data = _ident_radio(radio) + + radio_version = _get_radio_firmware_version(radio) + LOG.info("Radio Version is %s" % repr(radio_version)) + + if b"HN5RV" in radio_version: + # A radio with HN5RV firmware has been detected. It could be a + # UV-5R style radio with HIGH/LOW power levels or it could be a + # BF-F8HP style radio with HIGH/MID/LOW power levels. + # We are going to count on the user to make the right choice and + # then append that model type to the end of the image so it can + # be properly detected when loaded. + append_model = True + elif b"\xFF" * 7 in radio_version: + # A radio UV-5R style radio that reports no firmware version has + # been detected. + # We are going to count on the user to make the right choice and + # then append that model type to the end of the image so it can + # be properly detected when loaded. + append_model = True + elif not any(type in radio_version for type in radio._basetype): + # This radio can't be properly detected by parsing its firmware + # version. + raise errors.RadioError("Incorrect 'Model' selected.") + else: + # This radio can be properly detected by parsing its firmware version. + # There is no need to append its model type to the end of the image. + append_model = False + + # Main block + LOG.debug("downloading main block...") + for i in range(0, 0x1800, 0x40): + data += _read_block(radio, i, 0x40, False) + _do_status(radio, i) + _do_status(radio, radio.get_memsize()) + LOG.debug("done.") + LOG.debug("downloading aux block...") + # Auxiliary block starts at 0x1ECO (?) + for i in range(0x1EC0, 0x2000, 0x40): + data += _read_block(radio, i, 0x40, False) + + if append_model: + data += radio.MODEL.ljust(8) + + LOG.debug("done.") + return memmap.MemoryMapBytes(data) + + +def _send_block(radio, addr, data): + msg = struct.pack(">BHB", ord("X"), addr, len(data)) + radio.pipe.write(msg + data) + time.sleep(0.05) + + ack = radio.pipe.read(1) + if ack != b"\x06": + raise errors.RadioError("Radio refused to accept block 0x%04x" % addr) + + +def _do_upload(radio): + ident = _ident_radio(radio) + radio_upper_band = ident[3:4] + image_upper_band = _upper_band_from_image(radio) + + if image_upper_band == vhf_220_radio or radio_upper_band == vhf_220_radio: + if image_upper_band != radio_upper_band: + raise errors.RadioError("Image not supported by radio") + + image_version = _firmware_version_from_image(radio) + radio_version = _get_radio_firmware_version(radio) + LOG.info("Image Version is %s" % repr(image_version)) + LOG.info("Radio Version is %s" % repr(radio_version)) + + # default ranges + _ranges_main_default = [ + (0x0008, 0x0CF8), + (0x0D08, 0x0DF8), + (0x0E08, 0x1808) + ] + _ranges_aux_default = [ + (0x1EC0, 0x1EF0), + ] + + # extra aux ranges + _ranges_aux_extra = [ + (0x1F60, 0x1F70), + (0x1F80, 0x1F90), + (0x1FC0, 0x1FD0) + ] + + if image_version == radio_version: + image_matched_radio = True + if image_version.startswith(b"HN5RV"): + ranges_main = _ranges_main_default + ranges_aux = _ranges_aux_default + _ranges_aux_extra + elif image_version == 0xFF * 7: + ranges_main = _ranges_main_default + ranges_aux = _ranges_aux_default + _ranges_aux_extra + else: + ranges_main = radio._ranges_main + ranges_aux = radio._ranges_aux + elif any(type in radio_version for type in radio._basetype): + image_matched_radio = False + ranges_main = _ranges_main_default + ranges_aux = _ranges_aux_default + else: + msg = ("The upload was stopped because the firmware " + "version of the image (%s) does not match that " + "of the radio (%s).") + raise errors.RadioError(msg % (image_version, radio_version)) + + # Main block + mmap = radio.get_mmap().get_byte_compatible() + for start_addr, end_addr in ranges_main: + for i in range(start_addr, end_addr, 0x10): + _send_block(radio, i - 0x08, mmap[i:i + 0x10]) + _do_status(radio, i) + _do_status(radio, radio.get_memsize()) + + if len(mmap.get_packed()) == 0x1808: + LOG.info("Old image, not writing aux block") + return # Old image, no aux block + + # Auxiliary block at radio address 0x1EC0, our offset 0x1808 + for start_addr, end_addr in ranges_aux: + for i in range(start_addr, end_addr, 0x10): + addr = 0x1808 + (i - 0x1EC0) + _send_block(radio, i, mmap[addr:addr + 0x10]) + + if not image_matched_radio: + msg = ("Upload finished, but the 'Other Settings' " + "could not be sent because the firmware " + "version of the image (%s) does not match " + "that of the radio (%s).") + raise errors.RadioError(msg % (image_version, radio_version)) + +UV5R_POWER_LEVELS = [chirp_common.PowerLevel("High", watts=4.00), + chirp_common.PowerLevel("Low", watts=1.00)] + +UV5R_POWER_LEVELS3 = [chirp_common.PowerLevel("High", watts=8.00), + chirp_common.PowerLevel("Med", watts=4.00), + chirp_common.PowerLevel("Low", watts=1.00)] + +UV5R_DTCS = sorted(chirp_common.DTCS_CODES + [645]) + +UV5R_CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + \ + "!@#$%^&*()+-=[]:\";'<>?,./" + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + + if len(data) == 0x1950: + rid = data[0x1948:0x1950] + return rid.startswith(cls.MODEL) + elif len(data) == 0x1948: + rid = data[cls._fw_ver_file_start:cls._fw_ver_file_stop] + if any(type in rid for type in cls._basetype): + return True + else: + return False + + +class BaofengUV5R(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + + """Baofeng UV-5R""" + VENDOR = "Baofeng" + MODEL = "UV-5R" + BAUD_RATE = 9600 + NEEDS_COMPAT_SERIAL = False + + _memsize = 0x1808 + _basetype = BASETYPE_UV5R + _idents = [UV5R_MODEL_291, + UV5R_MODEL_ORIG + ] + _vhf_range = (136000000, 174000000) + _220_range = (220000000, 260000000) + _uhf_range = (400000000, 520000000) + _mem_params = (0x1828 # poweron_msg offset + ) + # offset of fw version in image file + _fw_ver_file_start = 0x1838 + _fw_ver_file_stop = 0x1846 + + _ranges_main = [ + (0x0008, 0x1808), + ] + _ranges_aux = [ + (0x1EC0, 0x2000), + ] + _valid_chars = UV5R_CHARSET + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('Due to the fact that the manufacturer continues to ' + 'release new versions of the firmware with obscure and ' + 'hard-to-track changes, this driver may not work with ' + 'your device. Thus far and to the best knowledge of the ' + 'author, no UV-5R radios have been harmed by using CHIRP. ' + 'However, proceed at your own risk!') + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to mic/spkr connector. + 3. Make sure connector is firmly connected. + 4. Turn radio on (volume may need to be set at 100%). + 5. Ensure that the radio is tuned to channel with no activity. + 6. Click OK to download image from device.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to mic/spkr connector. + 3. Make sure connector is firmly connected. + 4. Turn radio on (volume may need to be set at 100%). + 5. Ensure that the radio is tuned to channel with no activity. + 6. Click OK to upload image to device.""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = True + rf.valid_name_length = 7 + rf.valid_characters = self._valid_chars + rf.valid_skips = ["", "S"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_power_levels = UV5R_POWER_LEVELS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_modes = ["FM", "NFM"] + rf.valid_tuning_steps = STEPS + + normal_bands = [self._vhf_range, self._uhf_range] + rax_bands = [self._vhf_range, self._220_range] + + if self._mmap is None: + rf.valid_bands = [normal_bands[0], rax_bands[1], normal_bands[1]] + elif not self._is_orig() and self._my_upper_band() == vhf_220_radio: + rf.valid_bands = rax_bands + else: + rf.valid_bands = normal_bands + rf.memory_bounds = (0, 127) + return rf + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + if len(filedata) in [0x1808, 0x1948, 0x1950]: + match_size = True + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap) + + def sync_in(self): + try: + self._mmap = _do_download(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + _do_upload(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.txfreq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + def _get_mem(self, number): + return self._memobj.memory[number] + + def _get_nam(self, number): + return self._memobj.names[number] + + def get_memory(self, number): + _mem = self._get_mem(number) + _nam = self._get_nam(number) + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + if self._is_txinh(_mem): + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 70000000: + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _nam.name: + if str(char) == "\xFF": + char = " " # The UV-5R software may have 0xFF mid-name + mem.name += str(char) + mem.name = mem.name.rstrip() + + dtcs_pol = ["N", "N"] + + if _mem.txtone in [0, 0xFFFF]: + txmode = "" + elif _mem.txtone >= 0x0258: + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + elif _mem.txtone <= 0x0258: + txmode = "DTCS" + if _mem.txtone > 0x69: + index = _mem.txtone - 0x6A + dtcs_pol[0] = "R" + else: + index = _mem.txtone - 1 + mem.dtcs = UV5R_DTCS[index] + else: + LOG.warn("Bug: txtone is %04x" % _mem.txtone) + + if _mem.rxtone in [0, 0xFFFF]: + rxmode = "" + elif _mem.rxtone >= 0x0258: + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + elif _mem.rxtone <= 0x0258: + rxmode = "DTCS" + if _mem.rxtone >= 0x6A: + index = _mem.rxtone - 0x6A + dtcs_pol[1] = "R" + else: + index = _mem.rxtone - 1 + mem.rx_dtcs = UV5R_DTCS[index] + else: + LOG.warn("Bug: rxtone is %04x" % _mem.rxtone) + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + if not _mem.scan: + mem.skip = "S" + + if self.MODEL in ("KT-980HP", "BF-F8HP", "UV-82HP"): + levels = UV5R_POWER_LEVELS3 + else: + levels = UV5R_POWER_LEVELS + try: + mem.power = levels[_mem.lowpower] + except IndexError: + LOG.error("Radio reported invalid power level %s (in %s)" % + (_mem.lowpower, levels)) + mem.power = levels[0] + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + rs = RadioSetting("scode", "PTT ID Code", + RadioSettingValueList(PTTIDCODE_LIST, + PTTIDCODE_LIST[_mem.scode])) + mem.extra.append(rs) + + return mem + + def _set_mem(self, number): + return self._memobj.memory[number] + + def _set_nam(self, number): + return self._memobj.names[number] + + def set_memory(self, mem): + _mem = self._get_mem(mem.number) + _nam = self._get_nam(mem.number) + + if mem.empty: + _mem.set_raw("\xff" * 16) + _nam.set_raw("\xff" * 16) + return + + was_empty = False + # same method as used in get_memory to find + # out whether a raw memory is empty + if _mem.get_raw()[0] == "\xff": + was_empty = True + LOG.debug("UV5R: this mem was empty") + else: + # memorize old extra-values before erasing the whole memory + # used to solve issue 4121 + LOG.debug("mem was not empty, memorize extra-settings") + prev_bcl = _mem.bcl.get_value() + prev_scode = _mem.scode.get_value() + prev_pttid = _mem.pttid.get_value() + + _mem.set_raw("\x00" * 16) + + _mem.rxfreq = mem.freq / 10 + + if mem.duplex == "off": + for i in range(0, 4): + _mem.txfreq[i].set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = mem.freq / 10 + + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _nam.name[i] = mem.name[i] + except IndexError: + _nam.name[i] = "\xFF" + + rxmode = txmode = "" + if mem.tmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + _mem.rxtone = 0 + elif mem.tmode == "TSQL": + _mem.txtone = int(mem.ctone * 10) + _mem.rxtone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + rxmode = txmode = "DTCS" + _mem.txtone = UV5R_DTCS.index(mem.dtcs) + 1 + _mem.rxtone = UV5R_DTCS.index(mem.dtcs) + 1 + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + _mem.txtone = int(mem.rtone * 10) + elif txmode == "DTCS": + _mem.txtone = UV5R_DTCS.index(mem.dtcs) + 1 + else: + _mem.txtone = 0 + if rxmode == "Tone": + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCS": + _mem.rxtone = UV5R_DTCS.index(mem.rx_dtcs) + 1 + else: + _mem.rxtone = 0 + else: + _mem.rxtone = 0 + _mem.txtone = 0 + + if txmode == "DTCS" and mem.dtcs_polarity[0] == "R": + _mem.txtone += 0x69 + if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R": + _mem.rxtone += 0x69 + + _mem.scan = mem.skip != "S" + _mem.wide = mem.mode == "FM" + + if mem.power: + if self.MODEL in ("KT-980HP", "BF-F8HP", "UV-82HP"): + levels = [str(l) for l in UV5R_POWER_LEVELS3] + _mem.lowpower = levels.index(str(mem.power)) + else: + _mem.lowpower = UV5R_POWER_LEVELS.index(mem.power) + else: + _mem.lowpower = 0 + + if not was_empty: + # restoring old extra-settings (issue 4121 + _mem.bcl.set_value(prev_bcl) + _mem.scode.set_value(prev_scode) + _mem.pttid.set_value(prev_pttid) + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def _is_orig(self): + version_tag = _firmware_version_from_image(self) + LOG.debug("@_is_orig, version_tag: %s", util.hexprint(version_tag)) + try: + if b'BFB' in version_tag: + idx = version_tag.index(b"BFB") + 3 + version = int(version_tag[idx:idx + 3]) + return version < 291 + return False + except: + pass + raise errors.RadioError("Unable to parse version string %s" % + version_tag) + + def _my_version(self): + version_tag = _firmware_version_from_image(self) + if b'BFB' in version_tag: + idx = version_tag.index(b"BFB") + 3 + return int(version_tag[idx:idx + 3]) + + raise Exception("Unrecognized firmware version string") + + def _my_upper_band(self): + band_tag = _upper_band_from_image(self) + return band_tag + + def _get_settings(self): + _ani = self._memobj.ani + _fm_presets = self._memobj.fm_presets + _settings = self._memobj.settings + _squelch = self._memobj.squelch_new + _vfoa = self._memobj.vfoa + _vfob = self._memobj.vfob + _wmchannel = self._memobj.wmchannel + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + + group = RadioSettings(basic, advanced) + + rs = RadioSetting("squelch", "Carrier Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("save", "Battery Saver", + RadioSettingValueList( + SAVE_LIST, SAVE_LIST[_settings.save])) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Sensitivity", + RadioSettingValueList( + VOX_LIST, VOX_LIST[_settings.vox])) + advanced.append(rs) + + if self.MODEL == "UV-6": + # NOTE: The UV-6 calls this byte voxenable, but the UV-5R calls it + # autolk. Since this is a minor difference, it will be referred to + # by the wrong name for the UV-6. + rs = RadioSetting("autolk", "Vox", + RadioSettingValueBoolean(_settings.autolk)) + advanced.append(rs) + + if self.MODEL != "UV-6": + rs = RadioSetting("abr", "Backlight Timeout", + RadioSettingValueInteger(0, 24, _settings.abr)) + basic.append(rs) + + rs = RadioSetting("tdr", "Dual Watch", + RadioSettingValueBoolean(_settings.tdr)) + advanced.append(rs) + + if self.MODEL == "UV-6": + rs = RadioSetting("tdrch", "Dual Watch Channel", + RadioSettingValueList( + TDRCH_LIST, TDRCH_LIST[_settings.tdrch])) + advanced.append(rs) + + rs = RadioSetting("tdrab", "Dual Watch TX Priority", + RadioSettingValueBoolean(_settings.tdrab)) + advanced.append(rs) + else: + rs = RadioSetting("tdrab", "Dual Watch TX Priority", + RadioSettingValueList( + TDRAB_LIST, TDRAB_LIST[_settings.tdrab])) + advanced.append(rs) + + if self.MODEL == "UV-6": + rs = RadioSetting("alarm", "Alarm Sound", + RadioSettingValueBoolean(_settings.alarm)) + advanced.append(rs) + + if _settings.almod > 0x02: + val = 0x01 + else: + val = _settings.almod + rs = RadioSetting("almod", "Alarm Mode", + RadioSettingValueList( + ALMOD_LIST, ALMOD_LIST[val])) + advanced.append(rs) + + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + rs = RadioSetting("timeout", "Timeout Timer", + RadioSettingValueList( + TIMEOUT_LIST, TIMEOUT_LIST[_settings.timeout])) + basic.append(rs) + + if self._is_orig() and self._my_version() < 251: + rs = RadioSetting("voice", "Voice", + RadioSettingValueBoolean(_settings.voice)) + advanced.append(rs) + else: + rs = RadioSetting("voice", "Voice", + RadioSettingValueList( + VOICE_LIST, VOICE_LIST[_settings.voice])) + advanced.append(rs) + + rs = RadioSetting("screv", "Scan Resume", + RadioSettingValueList( + RESUME_LIST, RESUME_LIST[_settings.screv])) + advanced.append(rs) + + if self.MODEL != "UV-6": + rs = RadioSetting("mdfa", "Display Mode (A)", + RadioSettingValueList( + MODE_LIST, MODE_LIST[_settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("mdfb", "Display Mode (B)", + RadioSettingValueList( + MODE_LIST, MODE_LIST[_settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueBoolean(_settings.bcl)) + advanced.append(rs) + + if self.MODEL != "UV-6": + rs = RadioSetting("autolk", "Automatic Key Lock", + RadioSettingValueBoolean(_settings.autolk)) + advanced.append(rs) + + rs = RadioSetting("fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_settings.fmradio)) + advanced.append(rs) + + if self.MODEL != "UV-6": + rs = RadioSetting("wtled", "Standby LED Color", + RadioSettingValueList( + COLOR_LIST, COLOR_LIST[_settings.wtled])) + basic.append(rs) + + rs = RadioSetting("rxled", "RX LED Color", + RadioSettingValueList( + COLOR_LIST, COLOR_LIST[_settings.rxled])) + basic.append(rs) + + rs = RadioSetting("txled", "TX LED Color", + RadioSettingValueList( + COLOR_LIST, COLOR_LIST[_settings.txled])) + basic.append(rs) + + if isinstance(self, BaofengUV82Radio): + rs = RadioSetting("roger", "Roger Beep (TX)", + RadioSettingValueBoolean(_settings.roger)) + basic.append(rs) + rs = RadioSetting("rogerrx", "Roger Beep (RX)", + RadioSettingValueList( + ROGERRX_LIST, + ROGERRX_LIST[_settings.rogerrx])) + basic.append(rs) + else: + rs = RadioSetting("roger", "Roger Beep", + RadioSettingValueBoolean(_settings.roger)) + basic.append(rs) + + rs = RadioSetting("ste", "Squelch Tail Eliminate (HT to HT)", + RadioSettingValueBoolean(_settings.ste)) + advanced.append(rs) + + rs = RadioSetting("rpste", "Squelch Tail Eliminate (repeater)", + RadioSettingValueList( + RPSTE_LIST, RPSTE_LIST[_settings.rpste])) + advanced.append(rs) + + rs = RadioSetting("rptrl", "STE Repeater Delay", + RadioSettingValueList( + STEDELAY_LIST, STEDELAY_LIST[_settings.rptrl])) + advanced.append(rs) + + if self.MODEL != "UV-6": + rs = RadioSetting("reset", "RESET Menu", + RadioSettingValueBoolean(_settings.reset)) + advanced.append(rs) + + rs = RadioSetting("menu", "All Menus", + RadioSettingValueBoolean(_settings.menu)) + advanced.append(rs) + + if self.MODEL == "F-11": + # this is an F-11 only feature + rs = RadioSetting("vfomrlock", "VFO/MR Button", + RadioSettingValueBoolean(_settings.vfomrlock)) + advanced.append(rs) + + if isinstance(self, BaofengUV82Radio): + # this is a UV-82C only feature + rs = RadioSetting("vfomrlock", "VFO/MR Switching (UV-82C only)", + RadioSettingValueBoolean(_settings.vfomrlock)) + advanced.append(rs) + + if self.MODEL == "UV-82HP": + # this is a UV-82HP only feature + rs = RadioSetting( + "vfomrlock", "VFO/MR Switching (BTech UV-82HP only)", + RadioSettingValueBoolean(_settings.vfomrlock)) + advanced.append(rs) + + if isinstance(self, BaofengUV82Radio): + # this is an UV-82C only feature + rs = RadioSetting("singleptt", "Single PTT (UV-82C only)", + RadioSettingValueBoolean(_settings.singleptt)) + advanced.append(rs) + + if self.MODEL == "UV-82HP": + # this is an UV-82HP only feature + rs = RadioSetting("singleptt", "Single PTT (BTech UV-82HP only)", + RadioSettingValueBoolean(_settings.singleptt)) + advanced.append(rs) + + if self.MODEL == "UV-82HP": + # this is an UV-82HP only feature + rs = RadioSetting( + "tdrch", "Tone Burst Frequency (BTech UV-82HP only)", + RadioSettingValueList(RTONE_LIST, RTONE_LIST[_settings.tdrch])) + advanced.append(rs) + + if len(self._mmap.get_packed()) == 0x1808: + # Old image, without aux block + return group + + other = RadioSettingGroup("other", "Other Settings") + group.append(other) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = self._memobj.firmware_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val) + other.append(rs) + + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val) + other.append(rs) + + if self.MODEL != "UV-6": + _msg = self._memobj.sixpoweron_msg + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + _msg = self._memobj.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + rs = RadioSetting("ponmsg", "Power-On Message", + RadioSettingValueList( + PONMSG_LIST, PONMSG_LIST[_settings.ponmsg])) + other.append(rs) + + if self._is_orig(): + limit = "limits_old" + else: + limit = "limits_new" + + vhf_limit = getattr(self._memobj, limit).vhf + rs = RadioSetting("%s.vhf.lower" % limit, "VHF Lower Limit (MHz)", + RadioSettingValueInteger(1, 1000, + vhf_limit.lower)) + other.append(rs) + + rs = RadioSetting("%s.vhf.upper" % limit, "VHF Upper Limit (MHz)", + RadioSettingValueInteger(1, 1000, + vhf_limit.upper)) + other.append(rs) + + rs = RadioSetting("%s.vhf.enable" % limit, "VHF TX Enabled", + RadioSettingValueBoolean(vhf_limit.enable)) + other.append(rs) + + uhf_limit = getattr(self._memobj, limit).uhf + rs = RadioSetting("%s.uhf.lower" % limit, "UHF Lower Limit (MHz)", + RadioSettingValueInteger(1, 1000, + uhf_limit.lower)) + other.append(rs) + rs = RadioSetting("%s.uhf.upper" % limit, "UHF Upper Limit (MHz)", + RadioSettingValueInteger(1, 1000, + uhf_limit.upper)) + other.append(rs) + rs = RadioSetting("%s.uhf.enable" % limit, "UHF TX Enabled", + RadioSettingValueBoolean(uhf_limit.enable)) + other.append(rs) + + if self.MODEL != "UV-6": + workmode = RadioSettingGroup("workmode", "Work Mode Settings") + group.append(workmode) + + rs = RadioSetting("displayab", "Display", + RadioSettingValueList( + AB_LIST, AB_LIST[_settings.displayab])) + workmode.append(rs) + + rs = RadioSetting("workmode", "VFO/MR Mode", + RadioSettingValueList( + WORKMODE_LIST, + WORKMODE_LIST[_settings.workmode])) + workmode.append(rs) + + rs = RadioSetting("keylock", "Keypad Lock", + RadioSettingValueBoolean(_settings.keylock)) + workmode.append(rs) + + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(0, 127, + _wmchannel.mrcha)) + workmode.append(rs) + + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(0, 127, + _wmchannel.mrchb)) + workmode.append(rs) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + value = chirp_common.parse_freq(value) + if 17400000 <= value and value < 40000000: + msg = ("Can't be between 174.00000-400.00000") + raise InvalidValueError(msg) + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + obj.band = value >= 40000000 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_vfoa.freq)) + val1a.set_validate_callback(my_validate) + rs = RadioSetting("vfoa.freq", "VFO A Frequency", val1a) + rs.set_apply_callback(apply_freq, _vfoa) + workmode.append(rs) + + val1b = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_vfob.freq)) + val1b.set_validate_callback(my_validate) + rs = RadioSetting("vfob.freq", "VFO B Frequency", val1b) + rs.set_apply_callback(apply_freq, _vfob) + workmode.append(rs) + + rs = RadioSetting("vfoa.sftd", "VFO A Shift", + RadioSettingValueList( + SHIFTD_LIST, SHIFTD_LIST[_vfoa.sftd])) + workmode.append(rs) + + rs = RadioSetting("vfob.sftd", "VFO B Shift", + RadioSettingValueList( + SHIFTD_LIST, SHIFTD_LIST[_vfob.sftd])) + workmode.append(rs) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + for byte in bytes: + real_offset = (real_offset * 10) + byte + return chirp_common.format_freq(real_offset * 10000) + + def apply_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10000 + for i in range(3, -1, -1): + obj.offset[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_vfoa.offset)) + rs = RadioSetting("vfoa.offset", + "VFO A Offset (0.00-69.95)", val1a) + rs.set_apply_callback(apply_offset, _vfoa) + workmode.append(rs) + + val1b = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_vfob.offset)) + rs = RadioSetting("vfob.offset", + "VFO B Offset (0.00-69.95)", val1b) + rs.set_apply_callback(apply_offset, _vfob) + workmode.append(rs) + + if self.MODEL in ("KT-980HP", "BF-F8HP", "UV-82HP"): + rs = RadioSetting("vfoa.txpower3", "VFO A Power", + RadioSettingValueList( + TXPOWER3_LIST, + TXPOWER3_LIST[_vfoa.txpower3])) + workmode.append(rs) + + rs = RadioSetting("vfob.txpower3", "VFO B Power", + RadioSettingValueList( + TXPOWER3_LIST, + TXPOWER3_LIST[_vfob.txpower3])) + workmode.append(rs) + else: + rs = RadioSetting("vfoa.txpower", "VFO A Power", + RadioSettingValueList( + TXPOWER_LIST, + TXPOWER_LIST[_vfoa.txpower])) + workmode.append(rs) + + rs = RadioSetting("vfob.txpower", "VFO B Power", + RadioSettingValueList( + TXPOWER_LIST, + TXPOWER_LIST[_vfob.txpower])) + workmode.append(rs) + + rs = RadioSetting("vfoa.widenarr", "VFO A Bandwidth", + RadioSettingValueList( + BANDWIDTH_LIST, + BANDWIDTH_LIST[_vfoa.widenarr])) + workmode.append(rs) + + rs = RadioSetting("vfob.widenarr", "VFO B Bandwidth", + RadioSettingValueList( + BANDWIDTH_LIST, + BANDWIDTH_LIST[_vfob.widenarr])) + workmode.append(rs) + + rs = RadioSetting("vfoa.scode", "VFO A PTT-ID", + RadioSettingValueList( + PTTIDCODE_LIST, PTTIDCODE_LIST[_vfoa.scode])) + workmode.append(rs) + + rs = RadioSetting("vfob.scode", "VFO B PTT-ID", + RadioSettingValueList( + PTTIDCODE_LIST, PTTIDCODE_LIST[_vfob.scode])) + workmode.append(rs) + + if not self._is_orig(): + rs = RadioSetting("vfoa.step", "VFO A Tuning Step", + RadioSettingValueList( + STEP291_LIST, STEP291_LIST[_vfoa.step])) + workmode.append(rs) + rs = RadioSetting("vfob.step", "VFO B Tuning Step", + RadioSettingValueList( + STEP291_LIST, STEP291_LIST[_vfob.step])) + workmode.append(rs) + else: + rs = RadioSetting("vfoa.step", "VFO A Tuning Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfoa.step])) + workmode.append(rs) + rs = RadioSetting("vfob.step", "VFO B Tuning Step", + RadioSettingValueList( + STEP_LIST, STEP_LIST[_vfob.step])) + workmode.append(rs) + + fm_preset = RadioSettingGroup("fm_preset", "FM Radio Preset") + group.append(fm_preset) + + if _fm_presets <= 108.0 * 10 - 650: + preset = _fm_presets / 10.0 + 65 + elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10: + preset = _fm_presets / 10.0 + else: + preset = 76.0 + rs = RadioSetting("fm_presets", "FM Preset(MHz)", + RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)) + fm_preset.append(rs) + + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + group.append(dtmf) + dtmfchars = "0123456789 *#ABCD" + + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("pttid/%i.code" % i, + "PTT ID Code %i" % (i + 1), val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 5): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, self._memobj.pttid[i]) + dtmf.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("ani.code", "ANI Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 5): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, _ani) + dtmf.append(rs) + + rs = RadioSetting("ani.aniid", "ANI ID", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_ani.aniid])) + dtmf.append(rs) + + _codeobj = self._memobj.ani.alarmcode + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 3, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("ani.alarmcode", "Alarm Code", val) + + def apply_code(setting, obj): + alarmcode = [] + for j in range(0, 3): + try: + alarmcode.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + alarmcode.append(0xFF) + obj.alarmcode = alarmcode + rs.set_apply_callback(apply_code, _ani) + dtmf.append(rs) + + rs = RadioSetting("dtmfst", "DTMF Sidetone", + RadioSettingValueList(DTMFST_LIST, + DTMFST_LIST[_settings.dtmfst])) + dtmf.append(rs) + + if _ani.dtmfon > 0xC3: + val = 0x00 + else: + val = _ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(DTMFSPEED_LIST, + DTMFSPEED_LIST[val])) + dtmf.append(rs) + + if _ani.dtmfoff > 0xC3: + val = 0x00 + else: + val = _ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(DTMFSPEED_LIST, + DTMFSPEED_LIST[val])) + dtmf.append(rs) + + rs = RadioSetting("pttlt", "PTT ID Delay", + RadioSettingValueInteger(0, 50, _settings.pttlt)) + dtmf.append(rs) + + if not self._is_orig(): + service = RadioSettingGroup("service", "Service Settings") + group.append(service) + + for band in ["vhf", "uhf"]: + for index in range(0, 10): + key = "squelch_new.%s.sql%i" % (band, index) + if band == "vhf": + _obj = self._memobj.squelch_new.vhf + elif band == "uhf": + _obj = self._memobj.squelch_new.uhf + name = "%s Squelch %i" % (band.upper(), index) + rs = RadioSetting(key, name, + RadioSettingValueInteger( + 0, 123, + getattr(_obj, "sql%i" % (index)))) + service.append(rs) + + return group + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception as e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + for element in settings: + try: + val = element.value + if self._memobj.fm_presets <= 108.0 * 10 - 650: + value = int(val.get_value() * 10 - 650) + else: + value = int(val.get_value() * 10) + LOG.debug("Setting fm_presets = %s" % (value)) + self._memobj.fm_presets = value + except Exception as e: + LOG.debug(element.get_name()) + raise + + +class UV5XAlias(chirp_common.Alias): + VENDOR = "Baofeng" + MODEL = "UV-5X" + + +class RT5RAlias(chirp_common.Alias): + VENDOR = "Retevis" + MODEL = "RT-5R" + + +class RT5RVAlias(chirp_common.Alias): + VENDOR = "Retevis" + MODEL = "RT-5RV" + + +class RT5Alias(chirp_common.Alias): + VENDOR = "Retevis" + MODEL = "RT5" + + +class RT5_TPAlias(chirp_common.Alias): + VENDOR = "Retevis" + MODEL = "RT5(tri-power)" + + +class RH5RAlias(chirp_common.Alias): + VENDOR = "Rugged" + MODEL = "RH5R" + + +class ROUV5REXAlias(chirp_common.Alias): + VENDOR = "Radioddity" + MODEL = "UV-5R EX" + + +class A5RAlias(chirp_common.Alias): + VENDOR = "Ansoko" + MODEL = "A-5R" + + +class TenwayUV5RPro(chirp_common.Alias): + VENDOR = 'Tenway' + MODEL = 'UV-5R Pro' + + +@directory.register +class BaofengUV5RGeneric(BaofengUV5R): + ALIASES = [UV5XAlias, RT5RAlias, RT5RVAlias, RT5Alias, RH5RAlias, + ROUV5REXAlias, A5RAlias, TenwayUV5RPro] + + +@directory.register +class BaofengF11Radio(BaofengUV5R): + VENDOR = "Baofeng" + MODEL = "F-11" + _basetype = BASETYPE_F11 + _idents = [UV5R_MODEL_F11] + + def _is_orig(self): + # Override this for F11 to always return False + return False + + +@directory.register +class BaofengUV82Radio(BaofengUV5R): + MODEL = "UV-82" + _basetype = BASETYPE_UV82 + _idents = [UV5R_MODEL_UV82] + _vhf_range = (130000000, 176000000) + _uhf_range = (400000000, 521000000) + _valid_chars = chirp_common.CHARSET_ASCII + + def _is_orig(self): + # Override this for UV82 to always return False + return False + + +@directory.register +class Radioddity82X3Radio(BaofengUV82Radio): + VENDOR = "Radioddity" + MODEL = "UV-82X3" + _basetype = BASETYPE_UV82X3 + + def get_features(self): + rf = BaofengUV5R.get_features(self) + rf.valid_bands = [self._vhf_range, + (200000000, 260000000), + self._uhf_range] + return rf + + +@directory.register +class BaofengUV6Radio(BaofengUV5R): + + """Baofeng UV-6/UV-7""" + VENDOR = "Baofeng" + MODEL = "UV-6" + _basetype = BASETYPE_UV6 + _idents = [UV5R_MODEL_UV6, + UV5R_MODEL_UV6_ORIG + ] + + def get_features(self): + rf = BaofengUV5R.get_features(self) + rf.memory_bounds = (1, 128) + return rf + + def _get_mem(self, number): + return self._memobj.memory[number - 1] + + def _get_nam(self, number): + return self._memobj.names[number - 1] + + def _set_mem(self, number): + return self._memobj.memory[number - 1] + + def _set_nam(self, number): + return self._memobj.names[number - 1] + + def _is_orig(self): + # Override this for UV6 to always return False + return False + + +@directory.register +class IntekKT980Radio(BaofengUV5R): + VENDOR = "Intek" + MODEL = "KT-980HP" + _basetype = BASETYPE_KT980HP + _idents = [UV5R_MODEL_291] + _vhf_range = (130000000, 180000000) + _uhf_range = (400000000, 521000000) + + def get_features(self): + rf = BaofengUV5R.get_features(self) + rf.valid_power_levels = UV5R_POWER_LEVELS3 + return rf + + def _is_orig(self): + # Override this for KT980HP to always return False + return False + + +class ROGA5SAlias(chirp_common.Alias): + VENDOR = "Radioddity" + MODEL = "GA-5S" + + +@directory.register +class BaofengBFF8HPRadio(BaofengUV5R): + VENDOR = "Baofeng" + MODEL = "BF-F8HP" + ALIASES = [RT5_TPAlias, ROGA5SAlias] + _basetype = BASETYPE_F8HP + _idents = [UV5R_MODEL_291, + UV5R_MODEL_A58 + ] + _vhf_range = (130000000, 180000000) + _uhf_range = (400000000, 521000000) + + def get_features(self): + rf = BaofengUV5R.get_features(self) + rf.valid_power_levels = UV5R_POWER_LEVELS3 + return rf + + def _is_orig(self): + # Override this for BFF8HP to always return False + return False + + +@directory.register +class BaofengUV82HPRadio(BaofengUV5R): + VENDOR = "Baofeng" + MODEL = "UV-82HP" + _basetype = BASETYPE_UV82HP + _idents = [UV5R_MODEL_UV82] + _vhf_range = (136000000, 175000000) + _uhf_range = (400000000, 521000000) + + def get_features(self): + rf = BaofengUV5R.get_features(self) + rf.valid_power_levels = UV5R_POWER_LEVELS3 + return rf + + def _is_orig(self): + # Override this for UV82HP to always return False + return False + + +@directory.register +class RadioddityUV5RX3Radio(BaofengUV5R): + VENDOR = "Radioddity" + MODEL = "UV-5RX3" + + def get_features(self): + rf = BaofengUV5R.get_features(self) + rf.valid_bands = [self._vhf_range, + (200000000, 260000000), + self._uhf_range] + return rf + + @classmethod + def match_model(cls, filename, filedata): + return False diff --git a/chirp/drivers/uv5x3.py b/chirp/drivers/uv5x3.py new file mode 100644 index 0000000..612dea0 --- /dev/null +++ b/chirp/drivers/uv5x3.py @@ -0,0 +1,1232 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp.drivers import baofeng_common +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, \ + InvalidValueError +from textwrap import dedent + +##### MAGICS ######################################################### + +# BTECH UV-5X3 magic string +MSTRING_UV5X3 = "\x50\x0D\x0C\x20\x16\x03\x28" + +# MTC UV-5R-3 magic string +MSTRING_UV5R3 = "\x50\x0D\x0C\x20\x17\x09\x19" + +##### ID strings ##################################################### + +# BTECH UV-5X3 +UV5X3_fp1 = "UVVG302" # BFB300 original +UV5X3_fp2 = "UVVG301" # UVV300 original +UV5X3_fp3 = "UVVG306" # UVV306 original + +# MTC UV-5R-3 +UV5R3_fp1 = "5R31709" + +DTMF_CHARS = " 1234567890*#ABCD" +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] + +LIST_AB = ["A", "B"] +LIST_ALMOD = ["Site", "Tone", "Code"] +LIST_BANDWIDTH = ["Wide", "Narrow"] +LIST_COLOR = ["Off", "Blue", "Orange", "Purple"] +LIST_DELAYPROCTIME = ["%s ms" % x for x in range(100, 4100, 100)] +LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"] +LIST_MODE = ["Channel", "Name", "Frequency"] +LIST_OFF1TO9 = ["Off"] + list("123456789") +LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"] +LIST_OFFAB = ["Off"] + LIST_AB +LIST_RESETTIME = ["%s ms" % x for x in range(100, 16100, 100)] +LIST_RESUME = ["TO", "CO", "SE"] +LIST_PONMSG = ["Full", "Message"] +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_SCODE = ["%s" % x for x in range(1, 16)] +LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SHIFTD = ["Off", "+", "-"] +LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)] +LIST_STEP = [str(x) for x in STEPS] +LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)] +LIST_TXPOWER = ["High", "Low"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_WORKMODE = ["Frequency", "Channel"] +LIST_DTMF_SPECIAL_DIGITS = [ "*", "#", "A", "B", "C", "D"] +LIST_DTMF_SPECIAL_VALUES = [ 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00] + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + match_rid1 = False + match_rid2 = False + + rid1 = data[0x1EF0:0x1EF7] + + if rid1 in cls._fileid: + match_rid1 = True + + if match_rid1: + return True + else: + return False + + +@directory.register +class UV5X3(baofeng_common.BaofengCommonHT): + """BTech UV-5X3""" + VENDOR = "BTECH" + MODEL = "UV-5X3" + + _fileid = [UV5X3_fp3, + UV5X3_fp2, + UV5X3_fp1] + + _magic = [MSTRING_UV5X3, ] + _magic_response_length = 14 + _fw_ver_start = 0x1EF0 + _recv_block_size = 0x40 + _mem_size = 0x2000 + _ack_block = True + + _ranges = [(0x0000, 0x0DF0), + (0x0E00, 0x1800), + (0x1EE0, 0x1EF0), + (0x1F60, 0x1F70), + (0x1F80, 0x1F90), + (0x1FA0, 0x1FB0), + (0x1FE0, 0x2000)] + _send_block_size = 0x10 + + MODES = ["FM", "NFM"] + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "!@#$%^&*()+-=[]:\";'<>?,./" + LENGTH_NAME = 7 + SKIP_VALUES = ["", "S"] + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Low", watts=1.00)] + VALID_BANDS = [(130000000, 180000000), + (220000000, 226000000), + (400000000, 521000000)] + PTTID_LIST = LIST_PTTID + SCODE_LIST = LIST_SCODE + + + MEM_FORMAT = """ + #seekto 0x0000; + struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1; + u8 unknown2:7, + lowpower:1; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; + } memory[128]; + + #seekto 0x0B00; + struct { + u8 code[16]; + } pttid[15]; + + #seekto 0x0C80; + struct { + u8 inspection[8]; + u8 monitor[8]; + u8 alarmcode[8]; + u8 stun[8]; + u8 kill[8]; + u8 revive[8]; + u8 code[7]; + u8 unknown06; + u8 dtmfon; + u8 dtmfoff; + u8 unused00:6, + aniid:2; + u8 unknown07[5]; + u8 masterid[5]; + u8 unknown08[3]; + u8 viceid[5]; + u8 unknown09[3]; + u8 unused01:7, + mastervice:1; + u8 unused02:3, + mrevive:1, + mkill:1, + mstun:1, + mmonitor:1, + minspection:1; + u8 unused03:3, + vrevive:1, + vkill:1, + vstun:1, + vmonitor:1, + vinspection:1; + u8 unused04:6, + txdisable:1, + rxdisable:1; + u8 groupcode; + u8 spacecode; + u8 delayproctime; + u8 resettime; + } ani; + + #seekto 0x0E20; + struct { + u8 unused00:4, + squelch:4; + u8 unused01:5, + step:3; + u8 unknown00; + u8 unused02:5, + save:3; + u8 unused03:4, + vox:4; + u8 unknown01; + u8 unused04:4, + abr:4; + u8 unused05:7, + tdr:1; + u8 unused06:7, + beep:1; + u8 unused07:2, + timeout:6; + u8 unknown02[4]; + u8 unused09:6, + voice:2; + u8 unknown03; + u8 unused10:6, + dtmfst:2; + u8 unknown04; + u8 unused11:6, + screv:2; + u8 unused12:6, + pttid:2; + u8 unused13:2, + pttlt:6; + u8 unused14:6, + mdfa:2; + u8 unused15:6, + mdfb:2; + u8 unknown05; + u8 unused16:7, + sync:1; + u8 unknown06[4]; + u8 unused17:6, + wtled:2; + u8 unused18:6, + rxled:2; + u8 unused19:6, + txled:2; + u8 unused20:6, + almod:2; + u8 unknown07; + u8 unused21:6, + tdrab:2; + u8 unused22:7, + ste:1; + u8 unused23:4, + rpste:4; + u8 unused24:4, + rptrl:4; + u8 unused25:7, + ponmsg:1; + u8 unused26:7, + roger:1; + u8 unused27:7, + dani:1; + u8 unused28:2, + dtmfg:6; + u8 unknown08:6, + reset:1, + unknown09:1; + u8 unknown10[3]; + u8 cht; + u8 unknown11[13]; + u8 displayab:1, + unknown12:2, + fmradio:1, + alarm:1, + unknown13:2, + menu:1; + u8 unknown14; + u8 unused29:7, + workmode:1; + u8 unused30:7, + keylock:1; + } settings; + + #seekto 0x0E76; + struct { + u8 unused0:1, + mrcha:7; + u8 unused1:1, + mrchb:7; + } wmchannel; + + struct vfo { + u8 unknown0[8]; + u8 freq[8]; + u8 offset[6]; + ul16 rxtone; + ul16 txtone; + u8 unused0:7, + band:1; + u8 unknown3; + u8 unknown4:2, + sftd:2, + scode:4; + u8 unknown5; + u8 unknown6:1, + step:3, + unknown7:4; + u8 txpower:1, + widenarr:1, + unknown8:6; + }; + + #seekto 0x0F00; + struct { + struct vfo a; + struct vfo b; + } vfo; + + #seekto 0x0F4E; + u16 fm_presets; + + #seekto 0x1000; + struct { + char name[7]; + u8 unknown[9]; + } names[128]; + + #seekto 0x1ED0; + struct { + char line1[7]; + char line2[7]; + } sixpoweron_msg; + + #seekto 0x1EF0; + struct { + char line1[7]; + char line2[7]; + } firmware_msg; + + struct squelch { + u8 sql0; + u8 sql1; + u8 sql2; + u8 sql3; + u8 sql4; + u8 sql5; + u8 sql6; + u8 sql7; + u8 sql8; + u8 sql9; + }; + + #seekto 0x1F60; + struct { + struct squelch vhf; + u8 unknown0[6]; + u8 unknown1[16]; + struct squelch vhf2; + u8 unknown2[6]; + u8 unknown3[16]; + struct squelch uhf; + } squelch; + + #seekto 0x1FE0; + struct { + char line1[7]; + char line2[7]; + } poweron_msg; + + struct limit { + u8 enable; + bbcd lower[2]; + bbcd upper[2]; + }; + + #seekto 0x1FF0; + struct { + struct limit vhf; + struct limit vhf2; + struct limit uhf; + } limits; + + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + fm_preset = RadioSettingGroup("fm_preset", "FM Preset") + dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings") + dtmfd = RadioSettingGroup("dtmfd", "DTMF Decode Settings") + service = RadioSettingGroup("service", "Service Settings") + top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe, + dtmfd, service) + + # Basic settings + if _mem.settings.squelch > 0x09: + val = 0x00 + else: + val = _mem.settings.squelch + rs = RadioSetting("settings.squelch", "Squelch", + RadioSettingValueList( + LIST_OFF1TO9, LIST_OFF1TO9[val])) + basic.append(rs) + + if _mem.settings.save > 0x04: + val = 0x00 + else: + val = _mem.settings.save + rs = RadioSetting("settings.save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, LIST_SAVE[val])) + basic.append(rs) + + if _mem.settings.vox > 0x0A: + val = 0x00 + else: + val = _mem.settings.vox + rs = RadioSetting("settings.vox", "Vox", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + if _mem.settings.abr > 0x0A: + val = 0x00 + else: + val = _mem.settings.abr + rs = RadioSetting("settings.abr", "Backlight Timeout", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + rs = RadioSetting("settings.tdr", "Dual Watch", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(rs) + + rs = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(rs) + + if _mem.settings.timeout > 0x27: + val = 0x03 + else: + val = _mem.settings.timeout + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + LIST_TIMEOUT, LIST_TIMEOUT[val])) + basic.append(rs) + + if _mem.settings.voice > 0x02: + val = 0x01 + else: + val = _mem.settings.voice + rs = RadioSetting("settings.voice", "Voice Prompt", + RadioSettingValueList( + LIST_VOICE, LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("settings.dtmfst", "DTMF Sidetone", + RadioSettingValueList(LIST_DTMFST, LIST_DTMFST[ + _mem.settings.dtmfst])) + basic.append(rs) + + if _mem.settings.screv > 0x02: + val = 0x01 + else: + val = _mem.settings.screv + rs = RadioSetting("settings.screv", "Scan Resume", + RadioSettingValueList( + LIST_RESUME, LIST_RESUME[val])) + basic.append(rs) + + rs = RadioSetting("settings.pttid", "When to send PTT ID", + RadioSettingValueList(LIST_PTTID, LIST_PTTID[ + _mem.settings.pttid])) + basic.append(rs) + + if _mem.settings.pttlt > 0x1E: + val = 0x05 + else: + val = _mem.settings.pttlt + rs = RadioSetting("pttlt", "PTT ID Delay", + RadioSettingValueInteger(0, 50, val)) + basic.append(rs) + + rs = RadioSetting("settings.mdfa", "Display Mode (A)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("settings.mdfb", "Display Mode (B)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("settings.sync", "Sync A & B", + RadioSettingValueBoolean(_mem.settings.sync)) + basic.append(rs) + + rs = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.wtled])) + basic.append(rs) + + rs = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.rxled])) + basic.append(rs) + + rs = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.txled])) + basic.append(rs) + + if _mem.settings.almod > 0x02: + val = 0x00 + else: + val = _mem.settings.almod + rs = RadioSetting("settings.almod", "Alarm Mode", + RadioSettingValueList( + LIST_ALMOD, LIST_ALMOD[val])) + basic.append(rs) + + if _mem.settings.tdrab > 0x02: + val = 0x00 + else: + val = _mem.settings.tdrab + rs = RadioSetting("settings.tdrab", "Dual Watch TX Priority", + RadioSettingValueList( + LIST_OFFAB, LIST_OFFAB[val])) + basic.append(rs) + + rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)", + RadioSettingValueBoolean(_mem.settings.ste)) + basic.append(rs) + + if _mem.settings.rpste > 0x0A: + val = 0x00 + else: + val = _mem.settings.rpste + rs = RadioSetting("settings.rpste", + "Squelch Tail Eliminate (repeater)", + RadioSettingValueList( + LIST_RPSTE, LIST_RPSTE[val])) + basic.append(rs) + + if _mem.settings.rptrl > 0x0A: + val = 0x00 + else: + val = _mem.settings.rptrl + rs = RadioSetting("settings.rptrl", "STE Repeater Delay", + RadioSettingValueList( + LIST_STEDELAY, LIST_STEDELAY[val])) + basic.append(rs) + + rs = RadioSetting("settings.ponmsg", "Power-On Message", + RadioSettingValueList(LIST_PONMSG, LIST_PONMSG[ + _mem.settings.ponmsg])) + basic.append(rs) + + rs = RadioSetting("settings.roger", "Roger Beep", + RadioSettingValueBoolean(_mem.settings.roger)) + basic.append(rs) + + rs = RadioSetting("settings.dani", "Decode ANI", + RadioSettingValueBoolean(_mem.settings.dani)) + basic.append(rs) + + if _mem.settings.dtmfg > 0x3C: + val = 0x14 + else: + val = _mem.settings.dtmfg + rs = RadioSetting("settings.dtmfg", "DTMF Gain", + RadioSettingValueInteger(0, 60, val)) + basic.append(rs) + + # Advanced settings + rs = RadioSetting("settings.reset", "RESET Menu", + RadioSettingValueBoolean(_mem.settings.reset)) + advanced.append(rs) + + rs = RadioSetting("settings.menu", "All Menus", + RadioSettingValueBoolean(_mem.settings.menu)) + advanced.append(rs) + + rs = RadioSetting("settings.fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_mem.settings.fmradio)) + advanced.append(rs) + + rs = RadioSetting("settings.alarm", "Alarm Sound", + RadioSettingValueBoolean(_mem.settings.alarm)) + advanced.append(rs) + + # Other settings + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = _mem.firmware_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val) + other.append(rs) + + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val) + other.append(rs) + + _msg = _mem.sixpoweron_msg + val = RadioSettingValueString(0, 7, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val) + other.append(rs) + val = RadioSettingValueString(0, 7, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val) + other.append(rs) + + _msg = _mem.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + if str(_mem.firmware_msg.line1) == ("UVVG302" or "5R31709"): + lower = 136 + upper = 174 + else: + lower = 130 + upper = 179 + rs = RadioSetting("limits.vhf.lower", "VHF Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf.lower)) + other.append(rs) + + rs = RadioSetting("limits.vhf.upper", "VHF Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf.upper)) + other.append(rs) + + if str(_mem.firmware_msg.line1) == "UVVG302": + lower = 200 + upper = 230 + elif str(_mem.firmware_msg.line1) == "5R31709": + lower = 200 + upper = 260 + else: + lower = 220 + upper = 225 + rs = RadioSetting("limits.vhf2.lower", "VHF2 Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf2.lower)) + other.append(rs) + + rs = RadioSetting("limits.vhf2.upper", "VHF2 Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf2.upper)) + other.append(rs) + + if str(_mem.firmware_msg.line1) == "UVVG302": + lower = 400 + upper = 480 + else: + lower = 400 + upper = 520 + rs = RadioSetting("limits.uhf.lower", "UHF Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.uhf.lower)) + other.append(rs) + + rs = RadioSetting("limits.uhf.upper", "UHF Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.uhf.upper)) + other.append(rs) + + # Work mode settings + rs = RadioSetting("settings.displayab", "Display", + RadioSettingValueList( + LIST_AB, LIST_AB[_mem.settings.displayab])) + work.append(rs) + + rs = RadioSetting("settings.workmode", "VFO/MR Mode", + RadioSettingValueList( + LIST_WORKMODE, + LIST_WORKMODE[_mem.settings.workmode])) + work.append(rs) + + rs = RadioSetting("settings.keylock", "Keypad Lock", + RadioSettingValueBoolean(_mem.settings.keylock)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrcha)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrchb)) + work.append(rs) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + _vhf_lower = int(_mem.limits.vhf.lower) + _vhf_upper = int(_mem.limits.vhf.upper) + _vhf2_lower = int(_mem.limits.vhf2.lower) + _vhf2_upper = int(_mem.limits.vhf2.upper) + _uhf_lower = int(_mem.limits.uhf.lower) + _uhf_upper = int(_mem.limits.uhf.upper) + value = chirp_common.parse_freq(value) + msg = ("Can't be less than %i.0000") + if value > 99000000 and value < _vhf_lower * 1000000: + raise InvalidValueError(msg % (_vhf_lower)) + msg = ("Can't be between %i.9975-%i.0000") + if (_vhf_upper + 1) * 1000000 <= value and \ + value < _vhf2_lower * 1000000: + raise InvalidValueError(msg % (_vhf_upper, _vhf2_lower)) + if (_vhf2_upper + 1) * 1000000 <= value and \ + value < _uhf_lower * 1000000: + raise InvalidValueError(msg % (_vhf2_upper, _uhf_lower)) + msg = ("Can't be greater than %i.9975") + if value > 99000000 and value >= (_uhf_upper + 1) * 1000000: + raise InvalidValueError(msg % (_uhf_upper)) + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.a.freq)) + val1a.set_validate_callback(my_validate) + rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a) + rs.set_apply_callback(apply_freq, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.b.freq)) + val1b.set_validate_callback(my_validate) + rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b) + rs.set_apply_callback(apply_freq, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.sftd", "VFO A Shift", + RadioSettingValueList( + LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.a.sftd])) + work.append(rs) + + rs = RadioSetting("vfo.b.sftd", "VFO B Shift", + RadioSettingValueList( + LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.b.sftd])) + work.append(rs) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + for byte in bytes: + real_offset = (real_offset * 10) + byte + return chirp_common.format_freq(real_offset * 1000) + + def apply_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 1000 + for i in range(5, -1, -1): + obj.offset[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_mem.vfo.a.offset)) + rs = RadioSetting("vfo.a.offset", + "VFO A Offset", val1a) + rs.set_apply_callback(apply_offset, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_mem.vfo.b.offset)) + rs = RadioSetting("vfo.b.offset", + "VFO B Offset", val1b) + rs.set_apply_callback(apply_offset, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.txpower", "VFO A Power", + RadioSettingValueList( + LIST_TXPOWER, + LIST_TXPOWER[_mem.vfo.a.txpower])) + work.append(rs) + + rs = RadioSetting("vfo.b.txpower", "VFO B Power", + RadioSettingValueList( + LIST_TXPOWER, + LIST_TXPOWER[_mem.vfo.b.txpower])) + work.append(rs) + + rs = RadioSetting("vfo.a.widenarr", "VFO A Bandwidth", + RadioSettingValueList( + LIST_BANDWIDTH, + LIST_BANDWIDTH[_mem.vfo.a.widenarr])) + work.append(rs) + + rs = RadioSetting("vfo.b.widenarr", "VFO B Bandwidth", + RadioSettingValueList( + LIST_BANDWIDTH, + LIST_BANDWIDTH[_mem.vfo.b.widenarr])) + work.append(rs) + + rs = RadioSetting("vfo.a.scode", "VFO A S-CODE", + RadioSettingValueList( + LIST_SCODE, + LIST_SCODE[_mem.vfo.a.scode])) + work.append(rs) + + rs = RadioSetting("vfo.b.scode", "VFO B S-CODE", + RadioSettingValueList( + LIST_SCODE, + LIST_SCODE[_mem.vfo.b.scode])) + work.append(rs) + + rs = RadioSetting("vfo.a.step", "VFO A Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.a.step])) + work.append(rs) + rs = RadioSetting("vfo.b.step", "VFO B Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.b.step])) + work.append(rs) + + # broadcast FM settings + _fm_presets = self._memobj.fm_presets + if _fm_presets <= 108.0 * 10 - 650: + preset = _fm_presets / 10.0 + 65 + elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10: + preset = _fm_presets / 10.0 + else: + preset = 76.0 + rs = RadioSetting("fm_presets", "FM Preset(MHz)", + RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)) + fm_preset.append(rs) + + # DTMF encode settings + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 16, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("pttid/%i.code" % i, + "Signal Code %i" % (i + 1), val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 16): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, self._memobj.pttid[i]) + dtmfe.append(rs) + + if _mem.ani.dtmfon > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + if _mem.ani.dtmfoff > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 7, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.code", "ANI Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 7): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfe.append(rs) + + rs = RadioSetting("ani.aniid", "When to send ANI ID", + RadioSettingValueList(LIST_PTTID, + LIST_PTTID[_mem.ani.aniid])) + dtmfe.append(rs) + + # DTMF decode settings + rs = RadioSetting("ani.mastervice", "Master and Vice ID", + RadioSettingValueBoolean(_mem.ani.mastervice)) + dtmfd.append(rs) + + _codeobj = _mem.ani.masterid + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.masterid", "Master Control ID", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 5): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.masterid = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + rs = RadioSetting("ani.minspection", "Master Inspection", + RadioSettingValueBoolean(_mem.ani.minspection)) + dtmfd.append(rs) + + rs = RadioSetting("ani.mmonitor", "Master Monitor", + RadioSettingValueBoolean(_mem.ani.mmonitor)) + dtmfd.append(rs) + + rs = RadioSetting("ani.mstun", "Master Stun", + RadioSettingValueBoolean(_mem.ani.mstun)) + dtmfd.append(rs) + + rs = RadioSetting("ani.mkill", "Master Kill", + RadioSettingValueBoolean(_mem.ani.mkill)) + dtmfd.append(rs) + + rs = RadioSetting("ani.mrevive", "Master Revive", + RadioSettingValueBoolean(_mem.ani.mrevive)) + dtmfd.append(rs) + + _codeobj = _mem.ani.viceid + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.viceid", "Vice Control ID", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 5): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.viceid = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + rs = RadioSetting("ani.vinspection", "Vice Inspection", + RadioSettingValueBoolean(_mem.ani.vinspection)) + dtmfd.append(rs) + + rs = RadioSetting("ani.vmonitor", "Vice Monitor", + RadioSettingValueBoolean(_mem.ani.vmonitor)) + dtmfd.append(rs) + + rs = RadioSetting("ani.vstun", "Vice Stun", + RadioSettingValueBoolean(_mem.ani.vstun)) + dtmfd.append(rs) + + rs = RadioSetting("ani.vkill", "Vice Kill", + RadioSettingValueBoolean(_mem.ani.vkill)) + dtmfd.append(rs) + + rs = RadioSetting("ani.vrevive", "Vice Revive", + RadioSettingValueBoolean(_mem.ani.vrevive)) + dtmfd.append(rs) + + _codeobj = _mem.ani.inspection + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 8, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.inspection", "Inspection Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 8): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.inspection = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + _codeobj = _mem.ani.monitor + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 8, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.monitor", "Monitor Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 8): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.monitor = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + _codeobj = _mem.ani.alarmcode + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 8, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.alarm", "Alarm Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 8): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.alarmcode = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + _codeobj = _mem.ani.stun + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 8, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.stun", "Stun Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 8): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.stun = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + _codeobj = _mem.ani.kill + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 8, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.kill", "Kill Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 8): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.kill = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + _codeobj = _mem.ani.revive + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 8, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.revive", "Revive Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 8): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.revive = code + rs.set_apply_callback(apply_code, self._memobj.ani) + dtmfd.append(rs) + + def apply_dmtf_listvalue(setting, obj): + LOG.debug("Setting value: "+ str(setting.value) + " from list") + val = str(setting.value) + index = LIST_DTMF_SPECIAL_DIGITS.index(val) + val = LIST_DTMF_SPECIAL_VALUES[index] + obj.set_value(val) + + if _mem.ani.groupcode in LIST_DTMF_SPECIAL_VALUES: + idx = LIST_DTMF_SPECIAL_VALUES.index(_mem.ani.groupcode) + else: + idx = LIST_DTMF_SPECIAL_VALUES.index(0x0B) + rs = RadioSetting("ani.groupcode", "Group Code", + RadioSettingValueList(LIST_DTMF_SPECIAL_DIGITS, + LIST_DTMF_SPECIAL_DIGITS[idx])) + rs.set_apply_callback(apply_dmtf_listvalue, _mem.ani.groupcode) + dtmfd.append(rs) + + if _mem.ani.spacecode in LIST_DTMF_SPECIAL_VALUES: + idx = LIST_DTMF_SPECIAL_VALUES.index(_mem.ani.spacecode) + else: + idx = LIST_DTMF_SPECIAL_VALUES.index(0x0C) + rs = RadioSetting("ani.spacecode", "Space Code", + RadioSettingValueList(LIST_DTMF_SPECIAL_DIGITS, + LIST_DTMF_SPECIAL_DIGITS[idx])) + rs.set_apply_callback(apply_dmtf_listvalue, _mem.ani.spacecode) + dtmfd.append(rs) + + if _mem.ani.resettime > 0x9F: + val = 0x4F + else: + val = _mem.ani.resettime + rs = RadioSetting("ani.resettime", "Reset Time", + RadioSettingValueList(LIST_RESETTIME, + LIST_RESETTIME[val])) + dtmfd.append(rs) + + if _mem.ani.delayproctime > 0x27: + val = 0x04 + else: + val = _mem.ani.delayproctime + rs = RadioSetting("ani.delayproctime", "Delay Processing Time", + RadioSettingValueList(LIST_DELAYPROCTIME, + LIST_DELAYPROCTIME[val])) + dtmfd.append(rs) + + # Service settings + for band in ["vhf", "vhf2", "uhf"]: + for index in range(0, 10): + key = "squelch.%s.sql%i" % (band, index) + if band == "vhf": + _obj = self._memobj.squelch.vhf + _name = "VHF" + elif band == "vhf2": + _obj = self._memobj.squelch.vhf2 + _name = "220" + elif band == "uhf": + _obj = self._memobj.squelch.uhf + _name = "UHF" + val = RadioSettingValueInteger(0, 123, + getattr(_obj, "sql%i" % (index))) + if index == 0: + val.set_mutable(False) + name = "%s Squelch %i" % (_name, index) + rs = RadioSetting(key, name, val) + service.append(rs) + + return top + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == 0x200E: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +@directory.register +class MTCUV5R3Radio(UV5X3): + VENDOR = "MTC" + MODEL = "UV-5R-3" + + _fileid = [UV5R3_fp1, ] + + _magic = [MSTRING_UV5R3, ] + + VALID_BANDS = [(136000000, 174000000), + (200000000, 260000000), + (400000000, 521000000)] diff --git a/chirp/drivers/uv6r.py b/chirp/drivers/uv6r.py new file mode 100644 index 0000000..57f3456 --- /dev/null +++ b/chirp/drivers/uv6r.py @@ -0,0 +1,872 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp.drivers import baofeng_common +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, \ + InvalidValueError +from textwrap import dedent + +##### MAGICS ######################################################### + +# Baofeng UV-6R magic string +MSTRING_UV6R = "\x50\xBB\xFF\x20\x14\x11\x22" + +##### ID strings ##################################################### + +# Baofeng UV-6R +UV6R_fp1 = " BF230#1" +UV6R_fp2 = " BF230#2" + +DTMF_CHARS = "0123456789 *#ABCD" +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] + +LIST_AB = ["A", "B"] +LIST_ALMOD = ["Site", "Tone", "Code"] +LIST_BANDWIDTH = ["Wide", "Narrow"] +LIST_COLOR = ["Off", "Blue", "Orange", "Purple"] +LIST_DTMFSPEED = ["%s ms" % x for x in range(50, 2010, 10)] +LIST_DTMFST = ["Off", "DT-ST", "ANI-ST", "DT+ANI"] +LIST_MODE = ["Channel", "Name", "Frequency"] +LIST_OFF1TO9 = ["Off"] + list("123456789") +LIST_OFF1TO10 = LIST_OFF1TO9 + ["10"] +LIST_OFFAB = ["Off"] + LIST_AB +LIST_RESUME = ["TO", "CO", "SE"] +LIST_PONMSG = ["Full", "Message"] +LIST_PTTID = ["Off", "BOT", "EOT", "Both"] +LIST_SCODE = ["%s" % x for x in range(1, 16)] +LIST_RPSTE = ["Off"] + ["%s" % x for x in range(1, 11)] +LIST_SAVE = ["Off", "1:1", "1:2", "1:3", "1:4"] +LIST_SHIFTD = ["Off", "+", "-"] +LIST_STEDELAY = ["Off"] + ["%s ms" % x for x in range(100, 1100, 100)] +LIST_STEP = [str(x) for x in STEPS] +LIST_TCALL = ["Off", "1000 Hz", "1450 Hz", "1750 Hz", "2100 Hz"] +LIST_TIMEOUT = ["%s sec" % x for x in range(15, 615, 15)] +LIST_TXPOWER = ["High", "Low"] +LIST_VOICE = ["Off", "English", "Chinese"] +LIST_WORKMODE = ["Frequency", "Channel"] + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + match_rid1 = False + match_rid2 = False + + rid1 = data[0x1FF8:0x2000] + + if rid1 in cls._fileid: + match_rid1 = True + + rid2 = data[0x1FD0:0x1FD5] + + if rid2 == cls.MODEL: + match_rid2 = True + + if match_rid1 and match_rid2: + return True + else: + return False + + +@directory.register +class UV6R(baofeng_common.BaofengCommonHT): + """Baofeng UV-6R""" + VENDOR = "Baofeng" + MODEL = "UV-6R" + + _fileid = [UV6R_fp2, UV6R_fp1, ] + + _magic = [MSTRING_UV6R, ] + _magic_response_length = 8 + _fw_ver_start = 0x1FF0 + _recv_block_size = 0x40 + _mem_size = 0x2000 + _ack_block = False + + _ranges = [(0x0000, 0x1800), + (0x1F40, 0x1F50), + (0x1FC0, 0x1FD0), + (0x1FE0, 0x1FF0)] + _send_block_size = 0x10 + + MODES = ["FM", "NFM"] + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "!@#$%^&*()+-=[]:\";'<>?,./" + LENGTH_NAME = 6 + SKIP_VALUES = ["", "S"] + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Low", watts=1.00)] + VALID_BANDS = [(136000000, 174000000), + (400000000, 520000000)] + PTTID_LIST = LIST_PTTID + SCODE_LIST = LIST_SCODE + + + MEM_FORMAT = """ + #seekto 0x0000; + struct { + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 unknown0:4, + scode:4; + u8 unknown1; + u8 unknown2:7, + lowpower:1; + u8 unknown3:1, + wide:1, + unknown4:2, + bcl:1, + scan:1, + pttid:2; + } memory[128]; + + #seekto 0x0B00; + struct { + u8 code[5]; + u8 unused[11]; + } pttid[15]; + + #seekto 0x0CAA; + struct { + u8 code[5]; + u8 unused:6, + aniid:2; + u8 unknown[2]; + u8 dtmfon; + u8 dtmfoff; + } ani; + + #seekto 0x0E20; + struct { + u8 unused00:4, + squelch:4; + u8 unused01:5, + step:3; + u8 unknown00; + u8 unused02:5, + save:3; + u8 unused03:4, + vox:4; + u8 unknown01; + u8 unused04:4, + abr:4; + u8 unused05:7, + tdr:1; + u8 unused06:7, + beep:1; + u8 unused07:2, + timeout:6; + u8 unused08:6, + tcall:2; + u8 unknown02[3]; + u8 unused09:6, + voice:2; + u8 unknown03; + u8 unused10:6, + dtmfst:2; + u8 unknown04; + u8 unused11:6, + screv:2; + u8 unused12:6, + pttid:2; + u8 unused13:2, + pttlt:6; + u8 unused14:6, + mdfa:2; + u8 unused15:6, + mdfb:2; + u8 unknown05; + u8 unused16:7, + autolk:1; + u8 unknown06[4]; + u8 unused17:6, + wtled:2; + u8 unused18:6, + rxled:2; + u8 unused19:6, + txled:2; + u8 unused20:6, + almod:2; + u8 unknown07[2]; + u8 unused22:7, + ste:1; + u8 unused23:4, + rpste:4; + u8 unused24:4, + rptrl:4; + u8 unused25:7, + ponmsg:1; + u8 unused26:7, + roger:1; + u8 unused27:7, + reset:1; + u8 unknown08; + u8 displayab:1, + unknown09:2, + fmradio:1, + alarm:1, + unknown10:1, + reset:1, + menu:1; + u8 unknown11; + u8 unused29:7, + workmode:1; + u8 unused30:7, + keylock:1; + u8 cht; + } settings; + + #seekto 0x0E76; + struct { + u8 unused0:1, + mrcha:7; + u8 unused1:1, + mrchb:7; + } wmchannel; + + struct vfo { + u8 unknown0[8]; + u8 freq[8]; + u8 offset[6]; + ul16 rxtone; + ul16 txtone; + u8 unused0:7, + band:1; + u8 unknown3; + u8 unknown4:2, + sftd:2, + scode:4; + u8 unknown5; + u8 unknown6:1, + step:3, + unknown7:4; + u8 txpower:1, + widenarr:1, + unknown8:6; + }; + + #seekto 0x0F00; + struct { + struct vfo a; + struct vfo b; + } vfo; + + #seekto 0x0F4E; + u16 fm_presets; + + #seekto 0x1000; + struct { + char name[6]; + u8 unknown[10]; + } names[128]; + + #seekto 0x1F40; + struct { + u8 sql0; + u8 sql1; + u8 sql2; + u8 sql3; + u8 sql4; + u8 sql5; + u8 sql6; + u8 sql7; + u8 sql8; + u8 sql9; + } squelch; + + struct limit { + u8 enable; + bbcd lower[2]; + bbcd upper[2]; + }; + + #seekto 0x1FC0; + struct { + struct limit vhf; + struct limit uhf; + } limits; + + #seekto 0x1FD0; + struct { + char line1[8]; + char line2[8]; + } sixpoweron_msg; + + #seekto 0x1FE0; + struct { + char line1[7]; + char line2[7]; + } poweron_msg; + + #seekto 0x1FF0; + struct { + char line1[8]; + char line2[8]; + } firmware_msg; + + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The BTech UV-6R driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow these instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + advanced = RadioSettingGroup("advanced", "Advanced Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + fm_preset = RadioSettingGroup("fm_preset", "FM Preset") + dtmfe = RadioSettingGroup("dtmfe", "DTMF Encode Settings") + service = RadioSettingGroup("service", "Service Settings") + top = RadioSettings(basic, advanced, other, work, fm_preset, dtmfe, + service) + + # Basic settings + if _mem.settings.squelch > 0x09: + val = 0x00 + else: + val = _mem.settings.squelch + rs = RadioSetting("settings.squelch", "Squelch", + RadioSettingValueList( + LIST_OFF1TO9, LIST_OFF1TO9[val])) + basic.append(rs) + + if _mem.settings.save > 0x04: + val = 0x00 + else: + val = _mem.settings.save + rs = RadioSetting("settings.save", "Battery Saver", + RadioSettingValueList( + LIST_SAVE, LIST_SAVE[val])) + basic.append(rs) + + if _mem.settings.vox > 0x0A: + val = 0x00 + else: + val = _mem.settings.vox + rs = RadioSetting("settings.vox", "Vox", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + if _mem.settings.abr > 0x0A: + val = 0x00 + else: + val = _mem.settings.abr + rs = RadioSetting("settings.abr", "Backlight Timeout", + RadioSettingValueList( + LIST_OFF1TO10, LIST_OFF1TO10[val])) + basic.append(rs) + + rs = RadioSetting("settings.tdr", "Dual Watch", + RadioSettingValueBoolean(_mem.settings.tdr)) + basic.append(rs) + + rs = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(_mem.settings.beep)) + basic.append(rs) + + if _mem.settings.timeout > 0x27: + val = 0x03 + else: + val = _mem.settings.timeout + rs = RadioSetting("settings.timeout", "Timeout Timer", + RadioSettingValueList( + LIST_TIMEOUT, LIST_TIMEOUT[val])) + basic.append(rs) + + if _mem.settings.voice > 0x02: + val = 0x01 + else: + val = _mem.settings.voice + rs = RadioSetting("settings.voice", "Voice Prompt", + RadioSettingValueList( + LIST_VOICE, LIST_VOICE[val])) + basic.append(rs) + + rs = RadioSetting("settings.dtmfst", "DTMF Sidetone", + RadioSettingValueList(LIST_DTMFST, LIST_DTMFST[ + _mem.settings.dtmfst])) + basic.append(rs) + + if _mem.settings.screv > 0x02: + val = 0x01 + else: + val = _mem.settings.screv + rs = RadioSetting("settings.screv", "Scan Resume", + RadioSettingValueList( + LIST_RESUME, LIST_RESUME[val])) + basic.append(rs) + + rs = RadioSetting("settings.pttid", "When to send PTT ID", + RadioSettingValueList(LIST_PTTID, LIST_PTTID[ + _mem.settings.pttid])) + basic.append(rs) + + if _mem.settings.pttlt > 0x1E: + val = 0x05 + else: + val = _mem.settings.pttlt + rs = RadioSetting("pttlt", "PTT ID Delay", + RadioSettingValueInteger(0, 50, val)) + basic.append(rs) + + rs = RadioSetting("settings.mdfa", "Display Mode (A)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfa])) + basic.append(rs) + + rs = RadioSetting("settings.mdfb", "Display Mode (B)", + RadioSettingValueList(LIST_MODE, LIST_MODE[ + _mem.settings.mdfb])) + basic.append(rs) + + rs = RadioSetting("settings.autolk", "Auto Lock Keypad", + RadioSettingValueBoolean(_mem.settings.autolk)) + basic.append(rs) + + rs = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.wtled])) + basic.append(rs) + + rs = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.rxled])) + basic.append(rs) + + rs = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList( + LIST_COLOR, LIST_COLOR[_mem.settings.txled])) + basic.append(rs) + + if _mem.settings.almod > 0x02: + val = 0x00 + else: + val = _mem.settings.almod + rs = RadioSetting("settings.almod", "Alarm Mode", + RadioSettingValueList( + LIST_ALMOD, LIST_ALMOD[val])) + basic.append(rs) + + if _mem.settings.tcall > 0x05: + val = 0x00 + else: + val = _mem.settings.tcall + rs = RadioSetting("settings.tcall", "Tone Burst Frequency", + RadioSettingValueList( + LIST_TCALL, LIST_TCALL[val])) + basic.append(rs) + + rs = RadioSetting("settings.ste", "Squelch Tail Eliminate (HT to HT)", + RadioSettingValueBoolean(_mem.settings.ste)) + basic.append(rs) + + if _mem.settings.rpste > 0x0A: + val = 0x00 + else: + val = _mem.settings.rpste + rs = RadioSetting("settings.rpste", + "Squelch Tail Eliminate (repeater)", + RadioSettingValueList( + LIST_RPSTE, LIST_RPSTE[val])) + basic.append(rs) + + if _mem.settings.rptrl > 0x0A: + val = 0x00 + else: + val = _mem.settings.rptrl + rs = RadioSetting("settings.rptrl", "STE Repeater Delay", + RadioSettingValueList( + LIST_STEDELAY, LIST_STEDELAY[val])) + basic.append(rs) + + rs = RadioSetting("settings.ponmsg", "Power-On Message", + RadioSettingValueList(LIST_PONMSG, LIST_PONMSG[ + _mem.settings.ponmsg])) + basic.append(rs) + + rs = RadioSetting("settings.roger", "Roger Beep", + RadioSettingValueBoolean(_mem.settings.roger)) + basic.append(rs) + + # Advanced settings + rs = RadioSetting("settings.reset", "RESET Menu", + RadioSettingValueBoolean(_mem.settings.reset)) + advanced.append(rs) + + rs = RadioSetting("settings.menu", "All Menus", + RadioSettingValueBoolean(_mem.settings.menu)) + advanced.append(rs) + + rs = RadioSetting("settings.fmradio", "Broadcast FM Radio", + RadioSettingValueBoolean(_mem.settings.fmradio)) + advanced.append(rs) + + rs = RadioSetting("settings.alarm", "Alarm Sound", + RadioSettingValueBoolean(_mem.settings.alarm)) + advanced.append(rs) + + # Other settings + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + _msg = _mem.firmware_msg + val = RadioSettingValueString(0, 8, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line1", "Firmware Message 1", val) + other.append(rs) + + val = RadioSettingValueString(0, 8, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("firmware_msg.line2", "Firmware Message 2", val) + other.append(rs) + + _msg = _mem.sixpoweron_msg + val = RadioSettingValueString(0, 8, _filter(_msg.line1)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line1", "6+Power-On Message 1", val) + other.append(rs) + val = RadioSettingValueString(0, 8, _filter(_msg.line2)) + val.set_mutable(False) + rs = RadioSetting("sixpoweron_msg.line2", "6+Power-On Message 2", val) + other.append(rs) + + _msg = _mem.poweron_msg + rs = RadioSetting("poweron_msg.line1", "Power-On Message 1", + RadioSettingValueString( + 0, 7, _filter(_msg.line1))) + other.append(rs) + rs = RadioSetting("poweron_msg.line2", "Power-On Message 2", + RadioSettingValueString( + 0, 7, _filter(_msg.line2))) + other.append(rs) + + lower = 136 + upper = 174 + rs = RadioSetting("limits.vhf.lower", "VHF Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf.lower)) + other.append(rs) + + rs = RadioSetting("limits.vhf.upper", "VHF Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.vhf.upper)) + other.append(rs) + + lower = 400 + upper = 520 + rs = RadioSetting("limits.uhf.lower", "UHF Lower Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.uhf.lower)) + other.append(rs) + + rs = RadioSetting("limits.uhf.upper", "UHF Upper Limit (MHz)", + RadioSettingValueInteger( + lower, upper, _mem.limits.uhf.upper)) + other.append(rs) + + # Work mode settings + rs = RadioSetting("settings.displayab", "Display", + RadioSettingValueList( + LIST_AB, LIST_AB[_mem.settings.displayab])) + work.append(rs) + + rs = RadioSetting("settings.workmode", "VFO/MR Mode", + RadioSettingValueList( + LIST_WORKMODE, + LIST_WORKMODE[_mem.settings.workmode])) + work.append(rs) + + rs = RadioSetting("settings.keylock", "Keypad Lock", + RadioSettingValueBoolean(_mem.settings.keylock)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrcha", "MR A Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrcha)) + work.append(rs) + + rs = RadioSetting("wmchannel.mrchb", "MR B Channel", + RadioSettingValueInteger(0, 127, + _mem.wmchannel.mrchb)) + work.append(rs) + + def convert_bytes_to_freq(bytes): + real_freq = 0 + for byte in bytes: + real_freq = (real_freq * 10) + byte + return chirp_common.format_freq(real_freq * 10) + + def my_validate(value): + _vhf_lower = int(_mem.limits.vhf.lower) + _vhf_upper = int(_mem.limits.vhf.upper) + _uhf_lower = int(_mem.limits.uhf.lower) + _uhf_upper = int(_mem.limits.uhf.upper) + value = chirp_common.parse_freq(value) + msg = ("Can't be less than %i.0000") + if value > 99000000 and value < _vhf_lower * 1000000: + raise InvalidValueError(msg % _vhf_lower) + msg = ("Can't be between %i.9975-%i.0000") + if _vhf_upper * 1000000 <= value and value < _uhf_lower * 1000000: + raise InvalidValueError(msg % (_vhf_upper - 1, _uhf_lower)) + msg = ("Can't be greater than %i.9975") + if value > 99000000 and value >= _uhf_upper * 1000000: + raise InvalidValueError(msg % (_uhf_upper - 1)) + return chirp_common.format_freq(value) + + def apply_freq(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 10 + for i in range(7, -1, -1): + obj.freq[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.a.freq)) + val1a.set_validate_callback(my_validate) + rs = RadioSetting("vfo.a.freq", "VFO A Frequency", val1a) + rs.set_apply_callback(apply_freq, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString(0, 10, + convert_bytes_to_freq(_mem.vfo.b.freq)) + val1b.set_validate_callback(my_validate) + rs = RadioSetting("vfo.b.freq", "VFO B Frequency", val1b) + rs.set_apply_callback(apply_freq, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.sftd", "VFO A Shift", + RadioSettingValueList( + LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.a.sftd])) + work.append(rs) + + rs = RadioSetting("vfo.b.sftd", "VFO B Shift", + RadioSettingValueList( + LIST_SHIFTD, LIST_SHIFTD[_mem.vfo.b.sftd])) + work.append(rs) + + def convert_bytes_to_offset(bytes): + real_offset = 0 + for byte in bytes: + real_offset = (real_offset * 10) + byte + return chirp_common.format_freq(real_offset * 1000) + + def apply_offset(setting, obj): + value = chirp_common.parse_freq(str(setting.value)) / 1000 + for i in range(5, -1, -1): + obj.offset[i] = value % 10 + value /= 10 + + val1a = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_mem.vfo.a.offset)) + rs = RadioSetting("vfo.a.offset", + "VFO A Offset", val1a) + rs.set_apply_callback(apply_offset, _mem.vfo.a) + work.append(rs) + + val1b = RadioSettingValueString( + 0, 10, convert_bytes_to_offset(_mem.vfo.b.offset)) + rs = RadioSetting("vfo.b.offset", + "VFO B Offset", val1b) + rs.set_apply_callback(apply_offset, _mem.vfo.b) + work.append(rs) + + rs = RadioSetting("vfo.a.txpower", "VFO A Power", + RadioSettingValueList( + LIST_TXPOWER, + LIST_TXPOWER[_mem.vfo.a.txpower])) + work.append(rs) + + rs = RadioSetting("vfo.b.txpower", "VFO B Power", + RadioSettingValueList( + LIST_TXPOWER, + LIST_TXPOWER[_mem.vfo.b.txpower])) + work.append(rs) + + rs = RadioSetting("vfo.a.widenarr", "VFO A Bandwidth", + RadioSettingValueList( + LIST_BANDWIDTH, + LIST_BANDWIDTH[_mem.vfo.a.widenarr])) + work.append(rs) + + rs = RadioSetting("vfo.b.widenarr", "VFO B Bandwidth", + RadioSettingValueList( + LIST_BANDWIDTH, + LIST_BANDWIDTH[_mem.vfo.b.widenarr])) + work.append(rs) + + rs = RadioSetting("vfo.a.scode", "VFO A S-CODE", + RadioSettingValueList( + LIST_SCODE, + LIST_SCODE[_mem.vfo.a.scode])) + work.append(rs) + + rs = RadioSetting("vfo.b.scode", "VFO B S-CODE", + RadioSettingValueList( + LIST_SCODE, + LIST_SCODE[_mem.vfo.b.scode])) + work.append(rs) + + rs = RadioSetting("vfo.a.step", "VFO A Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.a.step])) + work.append(rs) + rs = RadioSetting("vfo.b.step", "VFO B Tuning Step", + RadioSettingValueList( + LIST_STEP, LIST_STEP[_mem.vfo.b.step])) + work.append(rs) + + # broadcast FM settings + _fm_presets = self._memobj.fm_presets + if _fm_presets <= 108.0 * 10 - 650: + preset = _fm_presets / 10.0 + 65 + elif _fm_presets >= 65.0 * 10 and _fm_presets <= 108.0 * 10: + preset = _fm_presets / 10.0 + else: + preset = 76.0 + rs = RadioSetting("fm_presets", "FM Preset(MHz)", + RadioSettingValueFloat(65, 108.0, preset, 0.1, 1)) + fm_preset.append(rs) + + # DTMF settings + def apply_code(setting, obj, length): + code = [] + for j in range(0, length): + try: + code.append(DTMF_CHARS.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + + for i in range(0, 15): + _codeobj = self._memobj.pttid[i].code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + pttid = RadioSetting("pttid/%i.code" % i, + "Signal Code %i" % (i + 1), val) + pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 5) + dtmfe.append(pttid) + + if _mem.ani.dtmfon > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfon + rs = RadioSetting("ani.dtmfon", "DTMF Speed (on)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + if _mem.ani.dtmfoff > 0xC3: + val = 0x03 + else: + val = _mem.ani.dtmfoff + rs = RadioSetting("ani.dtmfoff", "DTMF Speed (off)", + RadioSettingValueList(LIST_DTMFSPEED, + LIST_DTMFSPEED[val])) + dtmfe.append(rs) + + _codeobj = self._memobj.ani.code + _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 5, _code, False) + val.set_charset(DTMF_CHARS) + rs = RadioSetting("ani.code", "ANI Code", val) + rs.set_apply_callback(apply_code, self._memobj.ani, 5) + dtmfe.append(rs) + + rs = RadioSetting("ani.aniid", "When to send ANI ID", + RadioSettingValueList(LIST_PTTID, + LIST_PTTID[_mem.ani.aniid])) + dtmfe.append(rs) + + # Service settings + for index in range(0, 10): + key = "squelch.sql%i" % (index) + _obj = self._memobj.squelch + val = RadioSettingValueInteger(0, 123, + getattr(_obj, "sql%i" % (index))) + if index == 0: + val.set_mutable(False) + name = "Squelch %i" % (index) + rs = RadioSetting(key, name, val) + service.append(rs) + + return top + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == 0x2008 or 0x2010: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False diff --git a/chirp/drivers/uvb5.py b/chirp/drivers/uvb5.py new file mode 100644 index 0000000..a748cc3 --- /dev/null +++ b/chirp/drivers/uvb5.py @@ -0,0 +1,799 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division + +import struct +import logging +from chirp import chirp_common, directory, bitwise, memmap, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +mem_format = """ +struct memory { + lbcd freq[4]; + lbcd offset[4]; + u8 unknown1:2, + txpol:1, + rxpol:1, + compander:1, + unknown2:3; + u8 rxtone; + u8 txtone; + u8 pttid:1, + scanadd:1, + isnarrow:1, + bcl:1, + highpower:1, + revfreq:1, + duplex:2; + u8 step; + u8 unknown[3]; +}; + +#seekto 0x0000; +char ident[32]; +u8 blank[16]; +struct memory vfo1; +struct memory channels[99]; +#seekto 0x0850; +struct memory vfo2; + +#seekto 0x09D0; +u16 fm_presets[16]; + +#seekto 0x0A30; +struct { + u8 name[5]; +} names[99]; + +#seekto 0x0D30; +struct { + u8 squelch; + u8 freqmode_ab:1, + save_funct:1, + backlight:1, + beep_tone_disabled:1, + roger:1, + tdr:1, + scantype:2; + u8 language:1, + workmode_b:1, + workmode_a:1, + workmode_fm:1, + voice_prompt:1, + fm:1, + pttid:2; + u8 unknown_0:5, + timeout:3; + u8 mdf_b:2, + mdf_a:2, + unknown_1:2, + txtdr:2; + u8 unknown_2:4, + ste_disabled:1, + unknown_3:2, + sidetone:1; + u8 vox; + u8 unk1; + u8 mem_chan_a; + u16 fm_vfo; + u8 unk4; + u8 unk5; + u8 mem_chan_b; + u8 unk6; + u8 last_menu; // number of last menu item accessed +} settings; + +#seekto 0x0D50; +struct { + u8 code[6]; +} pttid; + +#seekto 0x0F30; +struct { + lbcd lower_vhf[2]; + lbcd upper_vhf[2]; + lbcd lower_uhf[2]; + lbcd upper_uhf[2]; +} limits; + +#seekto 0x0FF0; +struct { + u8 vhfsquelch0; + u8 vhfsquelch1; + u8 vhfsquelch2; + u8 vhfsquelch3; + u8 vhfsquelch4; + u8 vhfsquelch5; + u8 vhfsquelch6; + u8 vhfsquelch7; + u8 vhfsquelch8; + u8 vhfsquelch9; + u8 unknown1[6]; + u8 uhfsquelch0; + u8 uhfsquelch1; + u8 uhfsquelch2; + u8 uhfsquelch3; + u8 uhfsquelch4; + u8 uhfsquelch5; + u8 uhfsquelch6; + u8 uhfsquelch7; + u8 uhfsquelch8; + u8 uhfsquelch9; + u8 unknown2[6]; + u8 vhfhipwr0; + u8 vhfhipwr1; + u8 vhfhipwr2; + u8 vhfhipwr3; + u8 vhfhipwr4; + u8 vhfhipwr5; + u8 vhfhipwr6; + u8 vhfhipwr7; + u8 vhflopwr0; + u8 vhflopwr1; + u8 vhflopwr2; + u8 vhflopwr3; + u8 vhflopwr4; + u8 vhflopwr5; + u8 vhflopwr6; + u8 vhflopwr7; + u8 uhfhipwr0; + u8 uhfhipwr1; + u8 uhfhipwr2; + u8 uhfhipwr3; + u8 uhfhipwr4; + u8 uhfhipwr5; + u8 uhfhipwr6; + u8 uhfhipwr7; + u8 uhflopwr0; + u8 uhflopwr1; + u8 uhflopwr2; + u8 uhflopwr3; + u8 uhflopwr4; + u8 uhflopwr5; + u8 uhflopwr6; + u8 uhflopwr7; +} test; +""" + + +def do_ident(radio): + radio.pipe.timeout = 3 + radio.pipe.write(b"\x05PROGRAM") + for x in range(10): + ack = radio.pipe.read(1) + if ack == b'\x06': + break + else: + raise errors.RadioError("Radio did not ack programming mode") + radio.pipe.write(b"\x02") + ident = radio.pipe.read(8) + LOG.debug(util.hexprint(ident)) + if not ident.startswith(b'HKT511'): + raise errors.RadioError("Unsupported model") + radio.pipe.write(b"\x06") + ack = radio.pipe.read(1) + if ack != b"\x06": + raise errors.RadioError("Radio did not ack ident") + + +def do_status(radio, direction, addr): + status = chirp_common.Status() + status.msg = "Cloning %s radio" % direction + status.cur = addr + status.max = 0x1000 + radio.status_fn(status) + + +def do_download(radio): + do_ident(radio) + data = b"KT511 Radio Program data v1.08\x00\x00" + data += (b"\x00" * 16) + firstack = None + for i in range(0, 0x1000, 16): + frame = struct.pack(">cHB", "R", i, 16) + radio.pipe.write(frame) + result = radio.pipe.read(20) + if frame[1:4] != result[1:4]: + LOG.debug(util.hexprint(result)) + raise errors.RadioError("Invalid response for address 0x%04x" % i) + radio.pipe.write(b"\x06") + ack = radio.pipe.read(1) + if not firstack: + firstack = ack + else: + if not ack == firstack: + LOG.debug("first ack: %s ack received: %s", + util.hexprint(firstack), util.hexprint(ack)) + raise errors.RadioError("Unexpected response") + data += result[4:] + do_status(radio, "from", i) + + return memmap.MemoryMapBytes(data) + + +def do_upload(radio): + do_ident(radio) + data = radio._mmap.get_byte_compatible()[0x0030:] + + for i in range(0, 0x1000, 16): + frame = struct.pack(">cHB", b"W", i, 16) + frame += data[i:i + 16] + radio.pipe.write(frame) + ack = radio.pipe.read(1) + if ack != b"\x06": + # UV-B5/UV-B6 radios with 27 menus do not support service settings + # and will stop ACKing when the upload reaches 0x0F10 + if i == 0x0F10: + # must be a radio with 27 menus detected - stop upload + break + else: + LOG.debug("Radio NAK'd block at address 0x%04x" % i) + raise errors.RadioError( + "Radio NAK'd block at address 0x%04x" % i) + LOG.debug("Radio ACK'd block at address 0x%04x" % i) + do_status(radio, "to", i) + +DUPLEX = ["", "-", "+"] +UVB5_STEPS = [5.00, 6.25, 10.0, 12.5, 20.0, 25.0] +CHARSET = "0123456789- ABCDEFGHIJKLMNOPQRSTUVWXYZ/_+*" +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), + chirp_common.PowerLevel("High", watts=5)] + + +@directory.register +class BaofengUVB5(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """Baofeng UV-B5""" + VENDOR = "Baofeng" + MODEL = "UV-B5" + BAUD_RATE = 9600 + NEEDS_COMPAT_SERIAL = False + SPECIALS = { + "VFO1": -2, + "VFO2": -1, + } + + _memsize = 0x1000 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This version of the UV-B5 driver allows you to ' + 'modify the Test Mode settings of your radio. This has been ' + 'tested and reports from other users indicate that it is a ' + 'safe thing to do. However, modifications to these values may ' + 'have unintended consequences, including damage to your ' + 'device. You have been warned. Proceed at your own risk!') + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to mic/spkr connector. + 3. Make sure connector is firmly connected. + 4. Turn radio on. + 5. Ensure that the radio is tuned to channel with no activity. + 6. Click OK to download image from device.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to mic/spkr connector. + 3. Make sure connector is firmly connected. + 4. Turn radio on. + 5. Ensure that the radio is tuned to channel with no activity. + 6. Click OK to upload image to device.""")) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_duplexes = DUPLEX + ["split"] + rf.can_odd_split = True + rf.valid_skips = ["", "S"] + rf.valid_characters = CHARSET + rf.valid_name_length = 5 + rf.valid_tuning_steps = UVB5_STEPS + rf.valid_bands = [(130000000, 175000000), + (220000000, 269000000), + (400000000, 520000000)] + rf.valid_modes = ["FM", "NFM"] + rf.valid_special_chans = list(self.SPECIALS.keys()) + rf.valid_power_levels = POWER_LEVELS + rf.has_ctone = True + rf.has_bank = False + rf.has_tuning_step = False + rf.memory_bounds = (1, 99) + return rf + + def sync_in(self): + try: + self._mmap = do_download(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + do_upload(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(mem_format, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.channels[number - 1]) + + def _decode_tone(self, _mem, which): + def _get(field): + return getattr(_mem, "%s%s" % (which, field)) + + value = _get('tone') + flag = _get('pol') + + if value > 155: + mode = val = pol = None + elif value > 50: + mode = 'DTCS' + val = chirp_common.DTCS_CODES[value - 51] + pol = flag and 'R' or 'N' + elif value: + mode = 'Tone' + val = chirp_common.TONES[value - 1] + pol = None + else: + mode = val = pol = None + + return mode, val, pol + + def _encode_tone(self, _mem, which, mode, val, pol): + def _set(field, value): + setattr(_mem, "%s%s" % (which, field), value) + + _set("pol", 0) + if mode == "Tone": + _set("tone", chirp_common.TONES.index(val) + 1) + elif mode == "DTCS": + _set("tone", chirp_common.DTCS_CODES.index(val) + 51) + _set("pol", pol == "R") + else: + _set("tone", 0) + + def _get_memobjs(self, number): + if isinstance(number, str): + return (getattr(self._memobj, number.lower()), None) + elif number < 0: + for k, v in list(SPECIALS.items()): + if number == v: + return (getattr(self._memobj, k.lower()), None) + else: + return (self._memobj.channels[number - 1], + self._memobj.names[number - 1].name) + + def get_memory(self, number): + _mem, _nam = self._get_memobjs(number) + mem = chirp_common.Memory() + if isinstance(number, str): + mem.number = self.SPECIALS[number] + mem.extd_number = number + else: + mem.number = number + + if _mem.freq.get_raw(asbytes=True)[0] == 0xff: + mem.empty = True + return mem + + mem.freq = int(_mem.freq) * 10 + mem.offset = int(_mem.offset) * 10 + + chirp_common.split_tone_decode( + mem, + self._decode_tone(_mem, "tx"), + self._decode_tone(_mem, "rx")) + + if 'step' in _mem and _mem.step > 0x05: + _mem.step = 0x00 + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = _mem.isnarrow and "NFM" or "FM" + mem.skip = "" if _mem.scanadd else "S" + mem.power = POWER_LEVELS[_mem.highpower] + + if _nam: + for char in _nam: + try: + mem.name += CHARSET[char] + except IndexError: + break + mem.name = mem.name.rstrip() + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(_mem.bcl)) + mem.extra.append(rs) + + rs = RadioSetting("revfreq", "Reverse Duplex", + RadioSettingValueBoolean(_mem.revfreq)) + mem.extra.append(rs) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueBoolean(_mem.pttid)) + mem.extra.append(rs) + + rs = RadioSetting("compander", "Compander", + RadioSettingValueBoolean(_mem.compander)) + mem.extra.append(rs) + + return mem + + def set_memory(self, mem): + _mem, _nam = self._get_memobjs(mem.number) + + if mem.empty: + if _nam is None: + raise errors.InvalidValueError("VFO channels can not be empty") + _mem.set_raw("\xFF" * 16) + return + + if _mem.get_raw() == ("\xFF" * 16): + _mem.set_raw("\x00" * 13 + "\xFF" * 3) + + _mem.freq = mem.freq / 10 + + if mem.duplex == "split": + diff = mem.offset - mem.freq + _mem.duplex = DUPLEX.index("-") if diff < 0 else DUPLEX.index("+") + _mem.offset = abs(diff) / 10 + else: + _mem.offset = mem.offset / 10 + _mem.duplex = DUPLEX.index(mem.duplex) + + tx, rx = chirp_common.split_tone_encode(mem) + self._encode_tone(_mem, 'tx', *tx) + self._encode_tone(_mem, 'rx', *rx) + + _mem.isnarrow = mem.mode == "NFM" + _mem.scanadd = mem.skip == "" + _mem.highpower = mem.power == POWER_LEVELS[1] + + if _nam: + for i in range(0, 5): + try: + _nam[i] = CHARSET.index(mem.name[i]) + except IndexError: + _nam[i] = 0xFF + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic Settings") + + group = RadioSettings(basic) + + options = ["Time", "Carrier", "Search"] + rs = RadioSetting("scantype", "Scan Type", + RadioSettingValueList(options, + options[_settings.scantype])) + basic.append(rs) + + options = ["Off"] + ["%s min" % x for x in range(1, 8)] + rs = RadioSetting("timeout", "Time Out Timer", + RadioSettingValueList( + options, options[_settings.timeout])) + basic.append(rs) + + options = ["A", "B"] + rs = RadioSetting("freqmode_ab", "Frequency Mode", + RadioSettingValueList( + options, options[_settings.freqmode_ab])) + basic.append(rs) + + options = ["Frequency Mode", "Channel Mode"] + rs = RadioSetting("workmode_a", "Radio Work Mode(A)", + RadioSettingValueList( + options, options[_settings.workmode_a])) + basic.append(rs) + + rs = RadioSetting("workmode_b", "Radio Work Mode(B)", + RadioSettingValueList( + options, options[_settings.workmode_b])) + basic.append(rs) + + options = ["Frequency", "Name", "Channel"] + rs = RadioSetting("mdf_a", "Display Format(F1)", + RadioSettingValueList( + options, options[_settings.mdf_a])) + basic.append(rs) + + rs = RadioSetting("mdf_b", "Display Format(F2)", + RadioSettingValueList( + options, options[_settings.mdf_b])) + basic.append(rs) + + rs = RadioSetting("mem_chan_a", "Mem Channel (A)", + RadioSettingValueInteger( + 1, 99, _settings.mem_chan_a)) + basic.append(rs) + + rs = RadioSetting("mem_chan_b", "Mem Channel (B)", + RadioSettingValueInteger( + 1, 99, _settings.mem_chan_b)) + basic.append(rs) + + options = ["Off", "BOT", "EOT", "Both"] + rs = RadioSetting("pttid", "PTT-ID", + RadioSettingValueList( + options, options[_settings.pttid])) + basic.append(rs) + + dtmfchars = "0123456789ABCD*#" + _codeobj = self._memobj.pttid.code + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 6, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("pttid.code", "PTT-ID Code", val) + + def apply_code(setting, obj): + code = [] + for j in range(0, 6): + try: + code.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + code.append(0xFF) + obj.code = code + rs.set_apply_callback(apply_code, self._memobj.pttid) + basic.append(rs) + + rs = RadioSetting("squelch", "Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Level", + RadioSettingValueInteger(0, 9, _settings.vox)) + basic.append(rs) + + options = ["Frequency Mode", "Channel Mode"] + rs = RadioSetting("workmode_fm", "FM Work Mode", + RadioSettingValueList( + options, options[_settings.workmode_fm])) + basic.append(rs) + + options = ["Current Frequency", "F1 Frequency", "F2 Frequency"] + rs = RadioSetting("txtdr", "Dual Standby TX Priority", + RadioSettingValueList(options, + options[_settings.txtdr])) + basic.append(rs) + + options = ["English", "Chinese"] + rs = RadioSetting("language", "Language", + RadioSettingValueList(options, + options[_settings.language])) + basic.append(rs) + + rs = RadioSetting("tdr", "Dual Standby", + RadioSettingValueBoolean(_settings.tdr)) + basic.append(rs) + + rs = RadioSetting("roger", "Roger Beep", + RadioSettingValueBoolean(_settings.roger)) + basic.append(rs) + + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueBoolean(_settings.backlight)) + basic.append(rs) + + rs = RadioSetting("save_funct", "Save Mode", + RadioSettingValueBoolean(_settings.save_funct)) + basic.append(rs) + + rs = RadioSetting("fm", "FM Function", + RadioSettingValueBoolean(_settings.fm)) + basic.append(rs) + + rs = RadioSetting("beep_tone_disabled", "Beep Prompt", + RadioSettingValueBoolean( + not _settings.beep_tone_disabled)) + basic.append(rs) + + rs = RadioSetting("voice_prompt", "Voice Prompt", + RadioSettingValueBoolean(_settings.voice_prompt)) + basic.append(rs) + + rs = RadioSetting("sidetone", "DTMF Side Tone", + RadioSettingValueBoolean(_settings.sidetone)) + basic.append(rs) + + rs = RadioSetting("ste_disabled", "Squelch Tail Eliminate", + RadioSettingValueBoolean(not _settings.ste_disabled)) + basic.append(rs) + + _limit = int(self._memobj.limits.lower_vhf) // 10 + rs = RadioSetting("limits.lower_vhf", "VHF Lower Limit (MHz)", + RadioSettingValueInteger(128, 270, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.lower_vhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + _limit = int(self._memobj.limits.upper_vhf) // 10 + rs = RadioSetting("limits.upper_vhf", "VHF Upper Limit (MHz)", + RadioSettingValueInteger(128, 270, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.upper_vhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + _limit = int(self._memobj.limits.lower_uhf) // 10 + rs = RadioSetting("limits.lower_uhf", "UHF Lower Limit (MHz)", + RadioSettingValueInteger(400, 520, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.lower_uhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + _limit = int(self._memobj.limits.upper_uhf) // 10 + rs = RadioSetting("limits.upper_uhf", "UHF Upper Limit (MHz)", + RadioSettingValueInteger(400, 520, _limit)) + + def apply_limit(setting, obj): + value = int(setting.value) * 10 + obj.upper_uhf = value + rs.set_apply_callback(apply_limit, self._memobj.limits) + basic.append(rs) + + fm_preset = RadioSettingGroup("fm_preset", "FM Radio Presets") + group.append(fm_preset) + + for i in range(0, 16): + if self._memobj.fm_presets[i] < 0x01AF: + used = True + preset = self._memobj.fm_presets[i] / 10.0 + 65 + else: + used = False + preset = 65 + rs = RadioSetting("fm_presets_%1i" % i, "FM Preset %i" % (i + 1), + RadioSettingValueBoolean(used), + RadioSettingValueFloat(65, 108, preset, 0.1, 1)) + fm_preset.append(rs) + + testmode = RadioSettingGroup("testmode", "Test Mode Settings") + group.append(testmode) + + vhfdata = ["136-139", "140-144", "145-149", "150-154", + "155-159", "160-164", "165-169", "170-174"] + uhfdata = ["400-409", "410-419", "420-429", "430-439", + "440-449", "450-459", "460-469", "470-479"] + powernamedata = ["Hi", "Lo"] + powerkeydata = ["hipwr", "lopwr"] + + for power in range(0, 2): + for index in range(0, 8): + key = "test.vhf%s%i" % (powerkeydata[power], index) + name = "%s Mhz %s Power" % (vhfdata[index], + powernamedata[power]) + rs = RadioSetting( + key, name, RadioSettingValueInteger( + 0, 255, getattr( + self._memobj.test, + "vhf%s%i" % (powerkeydata[power], index)))) + testmode.append(rs) + + for power in range(0, 2): + for index in range(0, 8): + key = "test.uhf%s%i" % (powerkeydata[power], index) + name = "%s Mhz %s Power" % (uhfdata[index], + powernamedata[power]) + rs = RadioSetting( + key, name, RadioSettingValueInteger( + 0, 255, getattr( + self._memobj.test, + "uhf%s%i" % (powerkeydata[power], index)))) + testmode.append(rs) + + for band in ["vhf", "uhf"]: + for index in range(0, 10): + key = "test.%ssquelch%i" % (band, index) + name = "%s Squelch %i" % (band.upper(), index) + rs = RadioSetting( + key, name, RadioSettingValueInteger( + 0, 255, getattr( + self._memobj.test, + "%ssquelch%i" % (band, index)))) + testmode.append(rs) + + return group + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "beep_tone_disabled": + setattr(obj, setting, not int(element.value)) + elif setting == "ste_disabled": + setattr(obj, setting, not int(element.value)) + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception as e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + for element in settings: + try: + index = (int(element.get_name().split("_")[-1])) + val = element.value + if list(val)[0].get_value(): + value = int(list(val)[1].get_value() * 10 - 650) + else: + value = 0x01AF + LOG.debug("Setting fm_presets[%1i] = %s" % (index, value)) + setting = self._memobj.fm_presets + setting[index] = value + except Exception as e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + return (filedata.startswith("KT511 Radio Program data") and + len(filedata) == (cls._memsize + 0x30)) diff --git a/chirp/drivers/vgc.py b/chirp/drivers/vgc.py new file mode 100644 index 0000000..f5da899 --- /dev/null +++ b/chirp/drivers/vgc.py @@ -0,0 +1,1450 @@ +# Copyright 2016: +# * Jim Unroe KC9HI, +# * Pavel Milanes CO7WT +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings +from textwrap import dedent + +MEM_FORMAT = """ +struct mem { + lbcd rxfreq[4]; + lbcd txfreq[4]; + lbcd rxtone[2]; + lbcd txtone[2]; + u8 unknown0:2, + txp:2, + wn:2, + unknown1:1, + bcl:1; + u8 unknown2:2, + revert:1, + dname:1, + unknown3:4; + u8 unknown4[2]; +}; + +struct nam { + char name[6]; + u8 unknown1[2]; +}; + +#seekto 0x0000; +struct mem left_memory[500]; + +#seekto 0x2000; +struct mem right_memory[500]; + +#seekto 0x4000; +struct nam left_names[500]; + +#seekto 0x5000; +struct nam right_names[500]; + +#seekto 0x6000; +u8 left_usedflags[64]; + +#seekto 0x6040; +u8 left_scanflags[64]; + +#seekto 0x6080; +u8 right_usedflags[64]; + +#seekto 0x60C0; +u8 right_scanflags[64]; + +#seekto 0x6160; +struct { + char line32[32]; +} embedded_msg; + +#seekto 0x6180; +struct { + u8 sbmute:2, // sub band mute + unknown1:1, + workmodb:1, // work mode (right side) + dw:1, // dual watch + audio:1, // audio output mode (stereo/mono) + unknown2:1, + workmoda:1; // work mode (left side) + u8 scansb:1, // scan stop beep + aftone:3, // af tone control + scand:1, // scan directon + scanr:3; // scan resume + u8 rxexp:1, // rx expansion + ptt:1, // ptt mode + display:1, // display select (frequency/clock) + omode:1, // operaton mode + beep:2, // beep volume + spkr:2; // speaker + u8 cpuclk:1, // operating mode(cpu clock) + fkey:3, // fkey function + mrscan:1, // memory scan type + color:3; // lcd backlight color + u8 vox:2, // vox + voxs:3, // vox sensitivity + mgain:3; // mic gain + u8 wbandb:4, // work band (right side) + wbanda:4; // work band (left side) + u8 sqlb:4, // squelch level (right side) + sqla:4; // squelch level (left side) + u8 apo:4, // auto power off + ars:1, // automatic repeater shift + tot:3; // time out timer + u8 stepb:4, // auto step (right side) + stepa:4; // auto step (left side) + u8 rxcoverm:1, // rx coverage-memory + lcdc:3, // lcd contrast + rxcoverv:1, // rx coverage-vfo + lcdb:3; // lcd brightness + u8 smode:1, // smart function mode + timefmt:1, // time format + datefmt:2, // date format + timesig:1, // time signal + keyb:3; // key/led brightness + u8 dwstop:1, // dual watch stop + unknown3:1, + sqlexp:1, // sql expansion + decbandsel:1, // decoding band select + dtmfmodenc:1, // dtmf mode encode + bell:3; // bell ringer + u8 unknown4:2, + btime:6; // lcd backlight time + u8 unknown5:2, + tz:6; // time zone + u8 unknown618E; + u8 unknown618F; + ul16 offseta; // work offset (left side) + ul16 offsetb; // work offset (right side) + ul16 mrcha; // selected memory channel (left) + ul16 mrchb; // selected memory channel (right) + ul16 wpricha; // work priority channel (left) + ul16 wprichb; // work priority channel (right) + u8 unknown6:3, + datasql:2, // data squelch + dataspd:1, // data speed + databnd:2; // data band select + u8 unknown7:1, + pfkey2:3, // mic p2 key + unknown8:1, + pfkey1:3; // mic p1 key + u8 unknown9:1, + pfkey4:3, // mic p4 key + unknowna:1, + pfkey3:3; // mic p3 key + u8 unknownb:7, + dtmfmoddec:1; // dtmf mode decode +} settings; + +#seekto 0x61B0; +struct { + char line16[16]; +} poweron_msg; + +#seekto 0x6300; +struct { + u8 unknown1:3, + ttdgt:5; // dtmf digit time + u8 unknown2:3, + ttint:5; // dtmf interval time + u8 unknown3:3, + tt1stdgt:5; // dtmf 1st digit time + u8 unknown4:3, + tt1stdly:5; // dtmf 1st digit delay + u8 unknown5:3, + ttdlyqt:5; // dtmf delay when use qt + u8 unknown6:3, + ttdkey:5; // dtmf d key function + u8 unknown7; + u8 unknown8:4, + ttautod:4; // dtmf auto dial group +} dtmf; + +#seekto 0x6330; +struct { + u8 unknown1:7, + ttsig:1; // dtmf signal + u8 unknown2:4, + ttintcode:4; // dtmf interval code + u8 unknown3:5, + ttgrpcode:3; // dtmf group code + u8 unknown4:4, + ttautorst:4; // dtmf auto reset time + u8 unknown5:5, + ttalert:3; // dtmf alert tone/transpond +} dtmf2; + +#seekto 0x6360; +struct { + u8 code1[8]; // dtmf code + u8 code1_len; // dtmf code length + u8 unknown1[7]; + u8 code2[8]; // dtmf code + u8 code2_len; // dtmf code length + u8 unknown2[7]; + u8 code3[8]; // dtmf code + u8 code3_len; // dtmf code length + u8 unknown3[7]; + u8 code4[8]; // dtmf code + u8 code4_len; // dtmf code length + u8 unknown4[7]; + u8 code5[8]; // dtmf code + u8 code5_len; // dtmf code length + u8 unknown5[7]; + u8 code6[8]; // dtmf code + u8 code6_len; // dtmf code length + u8 unknown6[7]; + u8 code7[8]; // dtmf code + u8 code7_len; // dtmf code length + u8 unknown7[7]; + u8 code8[8]; // dtmf code + u8 code8_len; // dtmf code length + u8 unknown8[7]; + u8 code9[8]; // dtmf code + u8 code9_len; // dtmf code length + u8 unknown9[7]; +} dtmfcode; + +""" + +MEM_SIZE = 0x8000 +BLOCK_SIZE = 0x40 +MODES = ["FM", "Auto", "NFM", "AM"] +SKIP_VALUES = ["", "S"] +TONES = chirp_common.TONES +DTCS_CODES = chirp_common.DTCS_CODES +NAME_LENGTH = 6 +DTMF_CHARS = list("0123456789ABCD*#") +STIMEOUT = 1 + +# Basic settings lists +LIST_AFTONE = ["Low-3", "Low-2", "Low-1", "Normal", "High-1", "High-2"] +LIST_SPKR = ["Off", "Front", "Rear", "Front + Rear"] +LIST_AUDIO = ["Monaural", "Stereo"] +LIST_SBMUTE = ["Off", "TX", "RX", "Both"] +LIST_MLNHM = ["Min", "Low", "Normal", "High", "Max"] +LIST_PTT = ["Momentary", "Toggle"] +LIST_RXEXP = ["General", "Wide coverage"] +LIST_VOX = ["Off", "Internal mic", "Front hand-mic", "Rear hand-mic"] +LIST_DISPLAY = ["Frequency", "Timer/Clock"] +LIST_MINMAX = ["Min"] + ["%s" % x for x in range(2, 8)] + ["Max"] +LIST_COLOR = ["White-Blue", "Sky-Blue", "Marine-Blue", "Green", + "Yellow-Green", "Orange", "Amber", "White"] +LIST_BTIME = ["Continuous"] + ["%s" % x for x in range(1, 61)] +LIST_MRSCAN = ["All", "Selected"] +LIST_DWSTOP = ["Auto", "Hold"] +LIST_SCAND = ["Down", "Up"] +LIST_SCANR = ["Busy", "Hold", "1 sec", "3 sec", "5 sec"] +LIST_APO = ["Off", ".5", "1", "1.5"] + ["%s" % x for x in range(2, 13)] +LIST_BEEP = ["Off", "Low", "High"] +LIST_FKEY = ["MHz/AD-F", "AF Dual 1(line-in)", "AF Dual 2(AM)", "AF Dual 3(FM)", + "PA", "SQL off", "T-call", "WX"] +LIST_PFKEY = ["Off", "SQL off", "TX power", "Scan", "RPT shift", "Reverse", + "T-Call"] +LIST_AB = ["A", "B"] +LIST_COVERAGE = ["In band", "All"] +LIST_TOT = ["Off"] + ["%s" % x for x in range(5, 25, 5)] + ["30"] +LIST_DATEFMT = ["yyyy/mm/dd", "yyyy/dd/mm", "mm/dd/yyyy", "dd/mm/yyyy"] +LIST_TIMEFMT = ["24H", "12H"] +LIST_TZ = ["-12 INT DL W", + "-11 MIDWAY", + "-10 HAST", + "-9 AKST", + "-8 PST", + "-7 MST", + "-6 CST", + "-5 EST", + "-4:30 CARACAS", + "-4 AST", + "-3:30 NST", + "-3 BRASILIA", + "-2 MATLANTIC", + "-1 AZORES", + "-0 LONDON", + "+0 LONDON", + "+1 ROME", + "+2 ATHENS", + "+3 MOSCOW", + "+3:30 REHRW", + "+4 ABUDNABI", + "+4:30 KABUL", + "+5 ISLMABAD", + "+5:30 NEWDELHI", + "+6 DHAKA", + "+6:30 YANGON", + "+7 BANKOK", + "+8 BEIJING", + "+9 TOKYO", + "+10 ADELAIDE", + "+10 SYDNET", + "+11 NWCLDNIA", + "+12 FIJI", + "+13 NUKALOFA" + ] +LIST_BELL = ["Off", "1 time", "3 times", "5 times", "8 times", "Continuous"] +LIST_DATABND = ["Main band", "Sub band", "Left band-fixed", "Right band-fixed"] +LIST_DATASPD = ["1200 bps", "9600 bps"] +LIST_DATASQL = ["Busy/TX", "Busy", "TX"] + +# Other settings lists +LIST_CPUCLK = ["Clock frequency 1", "Clock frequency 2"] + +# Work mode settings lists +LIST_WORK = ["VFO", "Memory System"] +LIST_WBANDB = ["Air", "H-V", "GR1-V", "GR1-U", "H-U", "GR2"] +LIST_WBANDA = ["Line-in", "AM", "FM"] + LIST_WBANDB +LIST_SQL = ["Open"] + ["%s" % x for x in range(1, 10)] +_STEP_LIST = [2.5, 5., 6.25, 8.33, 9., 10., 12.5, 15., 20., 25., 50., 100., + 200.] +LIST_STEP = ["Auto"] + ["{0:.2f} KHz".format(x) for x in _STEP_LIST] +LIST_SMODE = ["F-1", "F-2"] + +# DTMF settings lists +LIST_TTDKEY = ["D code"] + ["Send delay %s s" % x for x in range(1, 17)] +LIST_TT200 = ["%s ms" % x for x in range(50, 210, 10)] +LIST_TT1000 = ["%s ms" % x for x in range(100, 1050, 50)] +LIST_TTSIG = ["Code squelch", "Select call"] +LIST_TTAUTORST = ["Off"] + ["%s s" % x for x in range(1, 16)] +LIST_TTGRPCODE = ["Off"] + list("ABCD*#") +LIST_TTINTCODE = DTMF_CHARS +LIST_TTALERT = ["Off", "Alert tone", "Transpond", "Transpond-ID code", + "Transpond-transpond code"] +LIST_TTAUTOD = ["%s" % x for x in range(1, 10)] + +# valid chars on the LCD +VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" + +# Power Levels +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5), + chirp_common.PowerLevel("Mid", watts=20), + chirp_common.PowerLevel("High", watts=50)] + +# B-TECH UV-50X3 id string +UV50X3_id = "VGC6600MD" + + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + Log.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _check_for_double_ack(radio): + radio.pipe.timeout = 0.005 + c = radio.pipe.read(1) + radio.pipe.timeout = STIMEOUT + if c and c != '\x06': + _exit_program_mode(radio) + raise errors.RadioError('Expected nothing or ACK, got %r' % c) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + _exit_program_mode(radio) + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + _exit_program_mode(radio) + msg = "Error reading data from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">BHB", ord(cmd), addr, length) + # add the data if set + if len(data) != 0: + frame += data + # return the data + return frame + + +def _recv(radio, addr, length=BLOCK_SIZE): + """Get data from the radio """ + # read 4 bytes of header + hdr = _rawrecv(radio, 4) + + # check for unexpected extra command byte + c, a, l = struct.unpack(">BHB", hdr) + if hdr[0:2] == "WW" and a != addr: + # extra command byte detected + # throw away the 1st byte and add the next byte in the buffer + hdr = hdr[1:] + _rawrecv(radio, 1) + + # read 64 bytes (0x40) of data + data = _rawrecv(radio, (BLOCK_SIZE)) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(hdr + data)) + + c, a, l = struct.unpack(">BHB", hdr) + if a != addr or l != length or c != ord("W"): + _exit_program_mode(radio) + LOG.error("Invalid answer for block 0x%04x:" % addr) + LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, l)) + raise errors.RadioError("Unknown response from the radio") + + return data + + +def _do_ident(radio): + """Put the radio in PROGRAM mode & identify it""" + # set the serial discipline + radio.pipe.baudrate = 115200 + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # flush input buffer + _clean_buffer(radio) + + magic = "V66LINK" + + _rawsend(radio, magic) + + # Ok, get the ident string + ident = _rawrecv(radio, 9) + + # check if ident is OK + if ident != radio.IDENT: + # bad ident + msg = "Incorrect model ID, got this:" + msg += util.hexprint(ident) + LOG.debug(msg) + raise errors.RadioError("Radio identification failed.") + + # DEBUG + LOG.info("Positive ident, got this:") + LOG.debug(util.hexprint(ident)) + + return True + + +def _exit_program_mode(radio): + endframe = "\x45" + _rawsend(radio, endframe) + + +def _download(radio): + """Get the memory map""" + + # put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + frame = _make_frame("R", addr, BLOCK_SIZE) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # sending the read request + _rawsend(radio, frame) + + # now we read + d = _recv(radio, addr) + + # aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + return data + + +def _upload(radio): + """Upload procedure""" + + MEM_SIZE = 0x7000 + + # put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # the fun start here + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # sending the data + data = radio.get_mmap()[addr:addr + BLOCK_SIZE] + + frame = _make_frame("W", addr, BLOCK_SIZE, data) + + _rawsend(radio, frame) + + # receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + _check_for_double_ack(radio) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + rid = data[0x6140:0x6148] + + #if rid in cls._fileid: + if rid in cls.IDENT: + return True + + return False + + +class VGCStyleRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """BTECH's UV-50X3""" + VENDOR = "BTECH" + _air_range = (108000000, 136000000) + _vhf_range = (136000000, 174000000) + _vhf2_range = (174000000, 250000000) + _220_range = (222000000, 225000000) + _gen1_range = (300000000, 400000000) + _uhf_range = (400000000, 480000000) + _gen2_range = (480000000, 520000000) + _upper = 499 + MODEL = "" + IDENT = "" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('The UV-50X3 driver is a beta version.\n' + '\n' + 'Please save an unedited copy of your first successful\n' + 'download to a CHIRP Radio Images(*.img) file.' + ) + rp.pre_download = _(dedent("""\ + Follow this instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.has_sub_devices = self.VARIANT == "" + rf.valid_modes = MODES + rf.valid_characters = VALID_CHARS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_skips = SKIP_VALUES + rf.valid_name_length = NAME_LENGTH + rf.valid_dtcs_codes = DTCS_CODES + rf.valid_tuning_steps = _STEP_LIST + rf.valid_bands = [self._air_range, + self._vhf_range, + self._vhf2_range, + self._220_range, + self._gen1_range, + self._uhf_range, + self._gen2_range] + rf.memory_bounds = (0, self._upper) + return rf + + def get_sub_devices(self): + return [UV50X3Left(self._mmap), UV50X3Right(self._mmap)] + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + try: + _upload(self) + except: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + if val.get_raw() == "\xFF\xFF": + return '', None, None + + val = int(val) + if val >= 12000: + a = val - 12000 + return 'DTCS', a, 'R' + elif val >= 8000: + a = val - 8000 + return 'DTCS', a, 'N' + else: + a = val / 10.0 + return 'Tone', a, None + + def encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval[0].set_raw(0xFF) + memval[1].set_raw(0xFF) + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + flag = 0x80 if pol == 'N' else 0xC0 + memval.set_value(value) + memval[1].set_bits(flag) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def _memory_obj(self, suffix=""): + return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix)) + + def _name_obj(self, suffix=""): + return getattr(self._memobj, "%s_names%s" % (self._vfo, suffix)) + + def _scan_obj(self, suffix=""): + return getattr(self._memobj, "%s_scanflags%s" % (self._vfo, suffix)) + + def _used_obj(self, suffix=""): + return getattr(self._memobj, "%s_usedflags%s" % (self._vfo, suffix)) + + def get_memory(self, number): + """Get the mem representation from the radio image""" + bitpos = (1 << (number % 8)) + bytepos = (number / 8) + + _mem = self._memory_obj()[number] + _names = self._name_obj()[number] + _scn = self._scan_obj()[bytepos] + _usd = self._used_obj()[bytepos] + + isused = bitpos & int(_usd) + isscan = bitpos & int(_scn) + + # Create a high-level memory object to return to the UI + mem = chirp_common.Memory() + + # Memory number + mem.number = number + + if not isused: + mem.empty = True + return mem + + # Freq and offset + mem.freq = int(_mem.rxfreq) * 10 + # tx freq can be blank + if _mem.get_raw()[4] == "\xFF": + # TX freq not set + mem.offset = 0 + mem.duplex = "off" + else: + # TX feq set + offset = (int(_mem.txfreq) * 10) - mem.freq + if offset < 0: + mem.offset = abs(offset) + mem.duplex = "-" + elif offset > 0: + mem.offset = offset + mem.duplex = "+" + else: + mem.offset = 0 + + # skip + if not isscan: + mem.skip = "S" + + # name TAG of the channel + mem.name = str(_names.name).strip("\xFF") + + # power + mem.power = POWER_LEVELS[int(_mem.txp)] + + # wide/narrow + mem.mode = MODES[int(_mem.wn)] + + # tone data + rxtone = txtone = None + txtone = self.decode_tone(_mem.txtone) + rxtone = self.decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + # Extra + mem.extra = RadioSettingGroup("extra", "Extra") + + bcl = RadioSetting("bcl", "Busy channel lockout", + RadioSettingValueBoolean(bool(_mem.bcl))) + mem.extra.append(bcl) + + revert = RadioSetting("revert", "Revert", + RadioSettingValueBoolean(bool(_mem.revert))) + mem.extra.append(revert) + + dname = RadioSetting("dname", "Display name", + RadioSettingValueBoolean(bool(_mem.dname))) + mem.extra.append(dname) + + return mem + + def set_memory(self, mem): + """Set the memory data in the eeprom img from the UI""" + bitpos = (1 << (mem.number % 8)) + bytepos = (mem.number / 8) + + _mem = self._memory_obj()[mem.number] + _names = self._name_obj()[mem.number] + _scn = self._scan_obj()[bytepos] + _usd = self._used_obj()[bytepos] + + if mem.empty: + _usd &= ~bitpos + _scn &= ~bitpos + _mem.set_raw("\xFF" * 16) + _names.name = ("\xFF" * 6) + return + else: + _usd |= bitpos + + # frequency + _mem.rxfreq = mem.freq / 10 + + # duplex + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "off": + for i in _mem.txfreq: + i.set_raw("\xFF") + elif mem.duplex == "split": + _mem.txfreq = mem.offset / 10 + else: + _mem.txfreq = mem.freq / 10 + + # tone data + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self.encode_tone(_mem.txtone, txmode, txtone, txpol) + self.encode_tone(_mem.rxtone, rxmode, rxtone, rxpol) + + # name TAG of the channel + _names.name = mem.name.ljust(6, "\xFF") + + # power level, # default power level is low + _mem.txp = 0 if mem.power is None else POWER_LEVELS.index(mem.power) + + # wide/narrow + _mem.wn = MODES.index(mem.mode) + + if mem.skip == "S": + _scn &= ~bitpos + else: + _scn |= bitpos + + # autoset display to display name if filled + if mem.extra: + # mem.extra only seems to be populated when called from edit panel + dname = mem.extra["dname"] + else: + dname = None + if mem.name: + _mem.dname = True + if dname and not dname.changed(): + dname.value = True + else: + _mem.dname = False + if dname and not dname.changed(): + dname.value = False + + # reseting unknowns, this has to be set by hand + _mem.unknown0 = 0 + _mem.unknown1 = 0 + _mem.unknown2 = 0 + _mem.unknown3 = 0 + + # extra settings + if len(mem.extra) > 0: + # there are setting, parse + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + else: + # there are no extra settings, load defaults + _mem.bcl = 0 + _mem.revert = 0 + _mem.dname = 1 + + def _bbcd2dtmf(self, bcdarr, strlen=16): + # doing bbcd, but with support for ABCD*# + LOG.debug(bcdarr.get_value()) + string = ''.join("%02X" % b for b in bcdarr) + LOG.debug("@_bbcd2dtmf, received: %s" % string) + string = string.replace('E', '*').replace('F', '#') + if strlen <= 16: + string = string[:strlen] + return string + + def _dtmf2bbcd(self, value): + dtmfstr = value.get_value() + dtmfstr = dtmfstr.replace('*', 'E').replace('#', 'F') + dtmfstr = str.ljust(dtmfstr.strip(), 16, "F") + bcdarr = list(bytearray.fromhex(dtmfstr)) + LOG.debug("@_dtmf2bbcd, sending: %s" % bcdarr) + return bcdarr + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + _mem = self._memobj + basic = RadioSettingGroup("basic", "Basic Settings") + other = RadioSettingGroup("other", "Other Settings") + work = RadioSettingGroup("work", "Work Mode Settings") + dtmf = RadioSettingGroup("dtmf", "DTMF Settings") + top = RadioSettings(basic, other, work, dtmf) + + # Basic + + # Audio: A01-A04 + + aftone = RadioSetting("settings.aftone", "AF tone control", + RadioSettingValueList(LIST_AFTONE, LIST_AFTONE[ + _mem.settings.aftone])) + basic.append(aftone) + + spkr = RadioSetting("settings.spkr", "Speaker", + RadioSettingValueList(LIST_SPKR,LIST_SPKR[ + _mem.settings.spkr])) + basic.append(spkr) + + audio = RadioSetting("settings.audio", "Stereo/Mono", + RadioSettingValueList(LIST_AUDIO, LIST_AUDIO[ + _mem.settings.audio])) + basic.append(audio) + + sbmute = RadioSetting("settings.sbmute", "Sub band mute", + RadioSettingValueList(LIST_SBMUTE, LIST_SBMUTE[ + _mem.settings.sbmute])) + basic.append(sbmute) + + # TX/RX: B01-B08 + + mgain = RadioSetting("settings.mgain", "Mic gain", + RadioSettingValueList(LIST_MLNHM, LIST_MLNHM[ + _mem.settings.mgain])) + basic.append(mgain) + + ptt = RadioSetting("settings.ptt", "PTT mode", + RadioSettingValueList(LIST_PTT,LIST_PTT[ + _mem.settings.ptt])) + basic.append(ptt) + + # B03 (per channel) + # B04 (per channel) + + rxexp = RadioSetting("settings.rxexp", "RX expansion", + RadioSettingValueList(LIST_RXEXP,LIST_RXEXP[ + _mem.settings.rxexp])) + basic.append(rxexp) + + vox = RadioSetting("settings.vox", "Vox", + RadioSettingValueList(LIST_VOX, LIST_VOX[ + _mem.settings.vox])) + basic.append(vox) + + voxs = RadioSetting("settings.voxs", "Vox sensitivity", + RadioSettingValueList(LIST_MLNHM, LIST_MLNHM[ + _mem.settings.voxs])) + basic.append(voxs) + + # B08 (per channel) + + # Display: C01-C06 + + display = RadioSetting("settings.display", "Display select", + RadioSettingValueList(LIST_DISPLAY, + LIST_DISPLAY[_mem.settings.display])) + basic.append(display) + + lcdb = RadioSetting("settings.lcdb", "LCD brightness", + RadioSettingValueList(LIST_MINMAX, LIST_MINMAX[ + _mem.settings.lcdb])) + basic.append(lcdb) + + color = RadioSetting("settings.color", "LCD color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _mem.settings.color])) + basic.append(color) + + lcdc = RadioSetting("settings.lcdc", "LCD contrast", + RadioSettingValueList(LIST_MINMAX, LIST_MINMAX[ + _mem.settings.lcdc])) + basic.append(lcdc) + + btime = RadioSetting("settings.btime", "LCD backlight time", + RadioSettingValueList(LIST_BTIME, LIST_BTIME[ + _mem.settings.btime])) + basic.append(btime) + + keyb = RadioSetting("settings.keyb", "Key brightness", + RadioSettingValueList(LIST_MINMAX, LIST_MINMAX[ + _mem.settings.keyb])) + basic.append(keyb) + + # Memory: D01-D04 + + # D01 (per channel) + # D02 (per channel) + + mrscan = RadioSetting("settings.mrscan", "Memory scan type", + RadioSettingValueList(LIST_MRSCAN, LIST_MRSCAN[ + _mem.settings.mrscan])) + basic.append(mrscan) + + # D04 (per channel) + + # Scan: E01-E04 + + dwstop = RadioSetting("settings.dwstop", "Dual watch stop", + RadioSettingValueList(LIST_DWSTOP, LIST_DWSTOP[ + _mem.settings.dwstop])) + basic.append(dwstop) + + scand = RadioSetting("settings.scand", "Scan direction", + RadioSettingValueList(LIST_SCAND,LIST_SCAND[ + _mem.settings.scand])) + basic.append(scand) + + scanr = RadioSetting("settings.scanr", "Scan resume", + RadioSettingValueList(LIST_SCANR,LIST_SCANR[ + _mem.settings.scanr])) + basic.append(scanr) + + scansb = RadioSetting("settings.scansb", "Scan stop beep", + RadioSettingValueBoolean(_mem.settings.scansb)) + basic.append(scansb) + + # System: F01-F09 + + apo = RadioSetting("settings.apo", "Automatic power off [hours]", + RadioSettingValueList(LIST_APO, LIST_APO[ + _mem.settings.apo])) + basic.append(apo) + + ars = RadioSetting("settings.ars", "Automatic repeater shift", + RadioSettingValueBoolean(_mem.settings.ars)) + basic.append(ars) + + beep = RadioSetting("settings.beep", "Beep volume", + RadioSettingValueList(LIST_BEEP,LIST_BEEP[ + _mem.settings.beep])) + basic.append(beep) + + fkey = RadioSetting("settings.fkey", "F key", + RadioSettingValueList(LIST_FKEY,LIST_FKEY[ + _mem.settings.fkey])) + basic.append(fkey) + + pfkey1 = RadioSetting("settings.pfkey1", "Mic P1 key", + RadioSettingValueList(LIST_PFKEY, LIST_PFKEY[ + _mem.settings.pfkey1])) + basic.append(pfkey1) + + pfkey2 = RadioSetting("settings.pfkey2", "Mic P2 key", + RadioSettingValueList(LIST_PFKEY, LIST_PFKEY[ + _mem.settings.pfkey2])) + basic.append(pfkey2) + + pfkey3 = RadioSetting("settings.pfkey3", "Mic P3 key", + RadioSettingValueList(LIST_PFKEY, LIST_PFKEY[ + _mem.settings.pfkey3])) + basic.append(pfkey3) + + pfkey4 = RadioSetting("settings.pfkey4", "Mic P4 key", + RadioSettingValueList(LIST_PFKEY, LIST_PFKEY[ + _mem.settings.pfkey4])) + basic.append(pfkey4) + + omode = RadioSetting("settings.omode", "Operation mode", + RadioSettingValueList(LIST_AB,LIST_AB[ + _mem.settings.omode])) + basic.append(omode) + + rxcoverm = RadioSetting("settings.rxcoverm", "RX coverage - memory", + RadioSettingValueList(LIST_COVERAGE, + LIST_COVERAGE[_mem.settings.rxcoverm])) + basic.append(rxcoverm) + + rxcoverv = RadioSetting("settings.rxcoverv", "RX coverage - VFO", + RadioSettingValueList(LIST_COVERAGE, + LIST_COVERAGE[_mem.settings.rxcoverv])) + basic.append(rxcoverv) + + tot = RadioSetting("settings.tot", "Time out timer [min]", + RadioSettingValueList(LIST_TOT, LIST_TOT[ + _mem.settings.tot])) + basic.append(tot) + + # Timer/Clock: G01-G04 + + # G01 + datefmt = RadioSetting("settings.datefmt", "Date format", + RadioSettingValueList(LIST_DATEFMT, + LIST_DATEFMT[_mem.settings.datefmt])) + basic.append(datefmt) + + timefmt = RadioSetting("settings.timefmt", "Time format", + RadioSettingValueList(LIST_TIMEFMT, + LIST_TIMEFMT[_mem.settings.timefmt])) + basic.append(timefmt) + + timesig = RadioSetting("settings.timesig", "Time signal", + RadioSettingValueBoolean(_mem.settings.timesig)) + basic.append(timesig) + + tz = RadioSetting("settings.tz", "Time zone", + RadioSettingValueList(LIST_TZ, LIST_TZ[ + _mem.settings.tz])) + basic.append(tz) + + # Signaling: H01-H06 + + bell = RadioSetting("settings.bell", "Bell ringer", + RadioSettingValueList(LIST_BELL, LIST_BELL[ + _mem.settings.bell])) + basic.append(bell) + + # H02 (per channel) + + dtmfmodenc = RadioSetting("settings.dtmfmodenc", "DTMF mode encode", + RadioSettingValueBoolean( + _mem.settings.dtmfmodenc)) + basic.append(dtmfmodenc) + + dtmfmoddec = RadioSetting("settings.dtmfmoddec", "DTMF mode decode", + RadioSettingValueBoolean( + _mem.settings.dtmfmoddec)) + basic.append(dtmfmoddec) + + # H04 (per channel) + + decbandsel = RadioSetting("settings.decbandsel", "DTMF band select", + RadioSettingValueList(LIST_AB,LIST_AB[ + _mem.settings.decbandsel])) + basic.append(decbandsel) + + sqlexp = RadioSetting("settings.sqlexp", "SQL expansion", + RadioSettingValueBoolean(_mem.settings.sqlexp)) + basic.append(sqlexp) + + # Pkt: I01-I03 + + databnd = RadioSetting("settings.databnd", "Packet data band", + RadioSettingValueList(LIST_DATABND,LIST_DATABND[ + _mem.settings.databnd])) + basic.append(databnd) + + dataspd = RadioSetting("settings.dataspd", "Packet data speed", + RadioSettingValueList(LIST_DATASPD,LIST_DATASPD[ + _mem.settings.dataspd])) + basic.append(dataspd) + + datasql = RadioSetting("settings.datasql", "Packet data squelch", + RadioSettingValueList(LIST_DATASQL,LIST_DATASQL[ + _mem.settings.datasql])) + basic.append(datasql) + + # Other + + dw = RadioSetting("settings.dw", "Dual watch", + RadioSettingValueBoolean(_mem.settings.dw)) + other.append(dw) + + cpuclk = RadioSetting("settings.cpuclk", "CPU clock frequency", + RadioSettingValueList(LIST_CPUCLK,LIST_CPUCLK[ + _mem.settings.cpuclk])) + other.append(cpuclk) + + def _filter(name): + filtered = "" + for char in str(name): + if char in VALID_CHARS: + filtered += char + else: + filtered += " " + return filtered + + line16 = RadioSetting("poweron_msg.line16", "Power-on message", + RadioSettingValueString(0, 16, _filter( + _mem.poweron_msg.line16))) + other.append(line16) + + line32 = RadioSetting("embedded_msg.line32", "Embedded message", + RadioSettingValueString(0, 32, _filter( + _mem.embedded_msg.line32))) + other.append(line32) + + # Work + + workmoda = RadioSetting("settings.workmoda", "Work mode A", + RadioSettingValueList(LIST_WORK,LIST_WORK[ + _mem.settings.workmoda])) + work.append(workmoda) + + workmodb = RadioSetting("settings.workmodb", "Work mode B", + RadioSettingValueList(LIST_WORK,LIST_WORK[ + _mem.settings.workmodb])) + work.append(workmodb) + + wbanda = RadioSetting("settings.wbanda", "Work band A", + RadioSettingValueList(LIST_WBANDA, LIST_WBANDA[ + (_mem.settings.wbanda) - 1])) + work.append(wbanda) + + wbandb = RadioSetting("settings.wbandb", "Work band B", + RadioSettingValueList(LIST_WBANDB, LIST_WBANDB[ + (_mem.settings.wbandb) - 4])) + work.append(wbandb) + + sqla = RadioSetting("settings.sqla", "Squelch A", + RadioSettingValueList(LIST_SQL, LIST_SQL[ + _mem.settings.sqla])) + work.append(sqla) + + sqlb = RadioSetting("settings.sqlb", "Squelch B", + RadioSettingValueList(LIST_SQL, LIST_SQL[ + _mem.settings.sqlb])) + work.append(sqlb) + + stepa = RadioSetting("settings.stepa", "Auto step A", + RadioSettingValueList(LIST_STEP,LIST_STEP[ + _mem.settings.stepa])) + work.append(stepa) + + stepb = RadioSetting("settings.stepb", "Auto step B", + RadioSettingValueList(LIST_STEP,LIST_STEP[ + _mem.settings.stepb])) + work.append(stepb) + + mrcha = RadioSetting("settings.mrcha", "Current channel A", + RadioSettingValueInteger(0, 499, + _mem.settings.mrcha)) + work.append(mrcha) + + mrchb = RadioSetting("settings.mrchb", "Current channel B", + RadioSettingValueInteger(0, 499, + _mem.settings.mrchb)) + work.append(mrchb) + + val = _mem.settings.offseta / 100.00 + offseta = RadioSetting("settings.offseta", "Offset A (0-37.95)", + RadioSettingValueFloat(0, 38.00, val, 0.05, 2)) + work.append(offseta) + + val = _mem.settings.offsetb / 100.00 + offsetb = RadioSetting("settings.offsetb", "Offset B (0-79.95)", + RadioSettingValueFloat(0, 80.00, val, 0.05, 2)) + work.append(offsetb) + + wpricha = RadioSetting("settings.wpricha", "Priority channel A", + RadioSettingValueInteger(0, 499, + _mem.settings.wpricha)) + work.append(wpricha) + + wprichb = RadioSetting("settings.wprichb", "Priority channel B", + RadioSettingValueInteger(0, 499, + _mem.settings.wprichb)) + work.append(wprichb) + + smode = RadioSetting("settings.smode", "Smart function mode", + RadioSettingValueList(LIST_SMODE,LIST_SMODE[ + _mem.settings.smode])) + work.append(smode) + + # dtmf + + ttdkey = RadioSetting("dtmf.ttdkey", "D key function", + RadioSettingValueList(LIST_TTDKEY, LIST_TTDKEY[ + _mem.dtmf.ttdkey])) + dtmf.append(ttdkey) + + ttdgt = RadioSetting("dtmf.ttdgt", "Digit time", + RadioSettingValueList(LIST_TT200, LIST_TT200[ + (_mem.dtmf.ttdgt) - 5])) + dtmf.append(ttdgt) + + ttint = RadioSetting("dtmf.ttint", "Interval time", + RadioSettingValueList(LIST_TT200, LIST_TT200[ + (_mem.dtmf.ttint) - 5])) + dtmf.append(ttint) + + tt1stdgt = RadioSetting("dtmf.tt1stdgt", "1st digit time", + RadioSettingValueList(LIST_TT200, LIST_TT200[ + (_mem.dtmf.tt1stdgt) - 5])) + dtmf.append(tt1stdgt) + + tt1stdly = RadioSetting("dtmf.tt1stdly", "1st digit delay time", + RadioSettingValueList(LIST_TT1000, LIST_TT1000[ + (_mem.dtmf.tt1stdly) - 2])) + dtmf.append(tt1stdly) + + ttdlyqt = RadioSetting("dtmf.ttdlyqt", "Digit delay when use qt", + RadioSettingValueList(LIST_TT1000, LIST_TT1000[ + (_mem.dtmf.ttdlyqt) - 2])) + dtmf.append(ttdlyqt) + + ttsig = RadioSetting("dtmf2.ttsig", "Signal", + RadioSettingValueList(LIST_TTSIG, LIST_TTSIG[ + _mem.dtmf2.ttsig])) + dtmf.append(ttsig) + + ttautorst = RadioSetting("dtmf2.ttautorst", "Auto reset time", + RadioSettingValueList(LIST_TTAUTORST, + LIST_TTAUTORST[_mem.dtmf2.ttautorst])) + dtmf.append(ttautorst) + + if _mem.dtmf2.ttgrpcode > 0x06: + val = 0x00 + else: + val = _mem.dtmf2.ttgrpcode + ttgrpcode = RadioSetting("dtmf2.ttgrpcode", "Group code", + RadioSettingValueList(LIST_TTGRPCODE, + LIST_TTGRPCODE[val])) + dtmf.append(ttgrpcode) + + ttintcode = RadioSetting("dtmf2.ttintcode", "Interval code", + RadioSettingValueList(LIST_TTINTCODE, + LIST_TTINTCODE[_mem.dtmf2.ttintcode])) + dtmf.append(ttintcode) + + if _mem.dtmf2.ttalert > 0x04: + val = 0x00 + else: + val = _mem.dtmf2.ttalert + ttalert = RadioSetting("dtmf2.ttalert", "Alert tone/transpond", + RadioSettingValueList(LIST_TTALERT, + LIST_TTALERT[val])) + dtmf.append(ttalert) + + ttautod = RadioSetting("dtmf.ttautod", "Auto dial group", + RadioSettingValueList(LIST_TTAUTOD, + LIST_TTAUTOD[_mem.dtmf.ttautod])) + dtmf.append(ttautod) + + # setup 9 dtmf autodial entries + for i in map(str, range(1, 10)): + objname = "code" + i + strname = "Code " + str(i) + dtmfsetting = getattr(_mem.dtmfcode, objname) + dtmflen = getattr(_mem.dtmfcode, objname + "_len") + dtmfstr = self._bbcd2dtmf(dtmfsetting, dtmflen) + code = RadioSettingValueString(0, 16, dtmfstr) + code.set_charset(DTMF_CHARS + list(" ")) + rs = RadioSetting("dtmfcode." + objname, strname, code) + dtmf.append(rs) + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "line16": + setattr(obj, setting, str(element.value).rstrip( + " ").ljust(16, "\xFF")) + elif setting == "line32": + setattr(obj, setting, str(element.value).rstrip( + " ").ljust(32, "\xFF")) + elif setting == "wbanda": + setattr(obj, setting, int(element.value) + 1) + elif setting == "wbandb": + setattr(obj, setting, int(element.value) + 4) + elif setting in ["offseta", "offsetb"]: + val = element.value + value = int(val.get_value() * 100) + setattr(obj, setting, value) + elif setting in ["ttdgt", "ttint", "tt1stdgt"]: + setattr(obj, setting, int(element.value) + 5) + elif setting in ["tt1stdly", "ttdlyqt"]: + setattr(obj, setting, int(element.value) + 2) + elif re.match('code\d', setting): + # set dtmf length field and then get bcd dtmf + dtmfstrlen = len(str(element.value).strip()) + setattr(_mem.dtmfcode, setting + "_len", dtmfstrlen) + dtmfstr = self._dtmf2bbcd(element.value) + setattr(_mem.dtmfcode, setting, dtmfstr) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + + @classmethod + def match_model(cls, filedata, filename): + match_size = False + match_model = False + + # testing the file data size + if len(filedata) == MEM_SIZE: + match_size = True + + # testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +@directory.register +class UV50X3(VGCStyleRadio): + """BTech UV-50X3""" + MODEL = "UV-50X3" + IDENT = UV50X3_id + + +class UV50X3Left(UV50X3): + VARIANT = "Left" + _vfo = "left" + + +class UV50X3Right(UV50X3): + VARIANT = "Right" + _vfo = "right" diff --git a/chirp/drivers/vx170.py b/chirp/drivers/vx170.py new file mode 100644 index 0000000..b5aaa26 --- /dev/null +++ b/chirp/drivers/vx170.py @@ -0,0 +1,132 @@ +# Copyright 2014 Jens Jensen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone, ft7800 +from chirp import chirp_common, directory, memmap, bitwise, errors +from textwrap import dedent +import time +import os + +MEM_FORMAT = """ +#seekto 0x018A; +struct { + u16 in_use; +} bank_used[24]; + +#seekto 0x0214; +u16 banksoff1; +#seekto 0x0294; +u16 banksoff2; + +#seekto 0x097A; +struct { + u8 name[6]; +} bank_names[24]; + +#seekto 0x0C0A; +struct { + u16 channels[100]; +} banks[24]; + +#seekto 0x0168; +struct { + u8 used:1, + unknown1:1, + mode:1, + unknown2:2, + duplex:3; + bbcd freq[3]; + u8 clockshift:1, + tune_step:3, + unknown5:1, + tmode:3; + bbcd split[3]; + u8 power:2, + tone:6; + u8 unknown6:1, + dtcs:7; + u8 unknown7[2]; + u8 offset; + u8 unknown9[3]; +} memory [200]; + +#seekto 0x0F28; +struct { + char name[6]; + u8 enabled:1, + unknown1:7; + u8 used:1, + unknown2:7; +} names[200]; + +#seekto 0x1768; +struct { + u8 skip3:2, + skip2:2, + skip1:2, + skip0:2; +} flags[50]; +""" + + +@directory.register +class VX170Radio(ft7800.FTx800Radio): + """Yaesu VX-170""" + MODEL = "VX-170" + _model = "AH022" + _memsize = 6057 + _block_lengths = [8, 6048, 1] + _block_size = 32 + + POWER_LEVELS_VHF = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("Med", watts=2.00), + chirp_common.PowerLevel("Lo", watts=0.50)] + + MODES = ["FM", "NFM"] + TMODES = ["", "Tone", "TSQL", "DTCS"] + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [moni] key while turning the radio on. +4. Select CLONE in menu, then press F. Radio restarts in clone mode. + ("CLONE" will appear on the display). +5. After clicking OK, breifly hold [PTT] key to send image. + ("-TX-" will appear on the LCD). """)) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +3. Press and hold in the [moni] key while turning the radio on. +4. Select CLONE in menu, then press F. Radio restarts in clone mode. + ("CLONE" will appear on the display). +5. Press the [moni] key ("-RX-" will appear on the LCD).""")) + return rp + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, self._memsize - 2)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_features(self): + rf = super(VX170Radio, self).get_features() + rf.has_bank = False + rf.has_bank_names = False + rf.valid_modes = self.MODES + rf.memory_bounds = (1, 200) + rf.valid_bands = [(137000000, 174000000)] + return rf diff --git a/chirp/drivers/vx2.py b/chirp/drivers/vx2.py new file mode 100644 index 0000000..91867b6 --- /dev/null +++ b/chirp/drivers/vx2.py @@ -0,0 +1,751 @@ +# Copyright 2013 Jens Jensen +# based on modification of Dan Smith's and Rick Farina's original work +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +import os +import traceback +import re +import logging + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x7F52; +u8 checksum; + +#seekto 0x005A; +u8 banksoff1; +#seekto 0x00DA; +u8 banksoff2; + +#seekto 0x0068; +u16 prioritychan1; + +#seekto 0x00E8; +u16 prioritychan2; + +#seekto 0x110; +struct { + u8 unk1; + u8 unk2; + u8 nfm_sql; + u8 wfm_sql; + u8 rfsql; + u8 vfomode:1, + cwid_en:1, + scan_lamp:1, + unk3:1, + ars:1, + beep:1, + split:1, + dtmfmode:1; + u8 busyled:1, + unk4:2, + bclo:1, + edgebeep:1, + unk5:2, + txsave:1; + u8 unk6:2, + smartsearch:1, + unk7:1, + artsinterval:1, + unk8:1, + hmrv:1, + moni_tcall:1; + u8 unk9:5, + dcsrev:1, + unk10:1, + mwmode:1; + u8 internet_mode:1, + internet_key:1, + wx_alert:1, + unk11:2, + att:1, + unk12:2; + u8 lamp; + u8 dimmer; + u8 rxsave; + u8 resume; + u8 chcounter; + u8 openmsgmode; + u8 openmsg[6]; + u8 cwid[16]; + u8 unk13[16]; + u8 artsbeep; + u8 bell; + u8 apo; + u8 tot; + u8 lock; + u8 mymenu; + u8 unk14[4]; + u8 emergmode; + +} settings; + +#seekto 0x0192; +struct { + u8 digits[16]; +} dtmf[9]; + +#seekto 0x016A; +struct { + u16 in_use; +} bank_used[20]; + +#seekto 0x0396; +struct { + u8 name[6]; +} wxchannels[10]; + +#seekto 0x05C2; +struct { + u16 channels[100]; +} banks[20]; + +#seekto 0x1562; +struct { + u8 even_pskip:1, + even_skip:1, + even_valid:1, + even_masked:1, + odd_pskip:1, + odd_skip:1, + odd_valid:1, + odd_masked:1; +} flags[500]; + +struct mem_struct { + u8 unknown1:2, + txnarrow:1, + clk:1, + unknown2:4; + u8 mode:2, + duplex:2, + tune_step:4; + bbcd freq[3]; + u8 power:2, + unknown3:4, + tmode:2; + u8 name[6]; + bbcd offset[3]; + u8 unknown4:2, + tone:6; + u8 unknown5:1, + dcs:7; + u8 unknown6; +}; + +#seekto 0x17C2; +struct mem_struct memory[1000]; +struct { + struct mem_struct lower; + struct mem_struct upper; +} pms[50]; + + +#seekto 0x03D2; +struct mem_struct home[12]; + +#seekto 0x04E2; +struct mem_struct vfo[12]; + + +""" + +VX2_DUPLEX = ["", "-", "+", "split"] +# NFM handled specially in radio +VX2_MODES = ["FM", "AM", "WFM", "Auto", "NFM"] +VX2_TMODES = ["", "Tone", "TSQL", "DTCS"] + +VX2_STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0, 9.0] + +CHARSET = list("0123456789" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ " + + "+-/\x00[](){}\x00\x00_" + + ("\x00" * 13) + "*" + "\x00\x00,'|\x00\x00\x00\x00" + + ("\x00" * 64)) + +DTMFCHARSET = list("0123456789ABCD*#") + +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=1.50), + chirp_common.PowerLevel("Low", watts=0.10)] + + +class VX2BankModel(chirp_common.BankModel): + """A VX-2 bank model""" + + def get_num_mappings(self): + return len(self.get_mappings()) + + def get_mappings(self): + banks = self._radio._memobj.banks + bank_mappings = [] + for index, _bank in enumerate(banks): + bank = chirp_common.Bank(self, "%i" % index, "b%i" % (index + 1)) + bank.index = index + bank_mappings.append(bank) + + return bank_mappings + + def _get_channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xFFFF: + return set() + + _members = self._radio._memobj.banks[bank.index] + return set([int(ch) + 1 for ch in _members.channels if ch != 0xFFFF]) + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.banks[bank.index] + if len(channels_in_bank) > len(_members.channels): + raise Exception("Too many entries in bank %d" % bank.index) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channels[index] = channel_number - 1 + empty = index + 1 + for index in range(empty, len(_members.channels)): + _members.channels[index] = 0xFFFF + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0x0000 + + # also needed for unit to recognize banks? + self._radio._memobj.banksoff1 = 0x00 + self._radio._memobj.banksoff2 = 0x00 + # todo: turn back off (0xFF) when all banks are empty? + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + if not channels_in_bank: + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0xFFFF + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._get_channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._get_channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +def _wipe_memory(mem): + mem.set_raw("\x00" * (mem.size() // 8)) + + +@directory.register +class VX2Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu VX-2""" + MODEL = "VX-2" + _model = "AH015" + BAUD_RATE = 19200 + _block_lengths = [10, 8, 32577] + _memsize = 32595 + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = True + rf.has_settings = True + rf.has_dtcs_polarity = False + rf.valid_modes = list(set(VX2_MODES)) + rf.valid_tmodes = list(VX2_TMODES) + rf.valid_duplexes = list(VX2_DUPLEX) + rf.valid_tuning_steps = list(VX2_STEPS) + rf.valid_bands = [(500000, 999000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 6 + rf.memory_bounds = (1, 1000) + rf.can_odd_split = True + rf.has_ctone = False + return rf + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x7F51)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number-1] + _flag = self._memobj.flags[(number-1)/2] + + nibble = ((number-1) % 2) and "even" or "odd" + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + pskip = _flag["%s_pskip" % nibble] + skip = _flag["%s_skip" % nibble] + + mem = chirp_common.Memory() + mem.number = number + + if not used: + mem.empty = True + if not valid: + mem.empty = True + mem.power = POWER_LEVELS[0] + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 1000 + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + mem.tmode = VX2_TMODES[_mem.tmode] + mem.duplex = VX2_DUPLEX[_mem.duplex] + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + if _mem.txnarrow and _mem.mode == VX2_MODES.index("FM"): + # narrow + FM + mem.mode = "NFM" + else: + mem.mode = VX2_MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.tuning_step = VX2_STEPS[_mem.tune_step] + mem.skip = pskip and "P" or skip and "S" or "" + mem.power = POWER_LEVELS[~_mem.power & 0x01] + + for i in _mem.name: + if i == 0xFF: + break + mem.name += CHARSET[i & 0x7F] + mem.name = mem.name.rstrip() + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + _flag = self._memobj.flags[(mem.number-1)/2] + + nibble = ((mem.number-1) % 2) and "even" or "odd" + + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + + if not mem.empty and not valid: + _wipe_memory(_mem) + + if mem.empty and valid and not used: + _flag["%s_valid" % nibble] = False + return + _flag["%s_masked" % nibble] = not mem.empty + + if mem.empty: + return + + _flag["%s_valid" % nibble] = True + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 1000 + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.tmode = VX2_TMODES.index(mem.tmode) + _mem.duplex = VX2_DUPLEX.index(mem.duplex) + if mem.mode == "NFM": + _mem.mode = VX2_MODES.index("FM") + _mem.txnarrow = True + else: + _mem.mode = VX2_MODES.index(mem.mode) + _mem.txnarrow = False + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = VX2_STEPS.index(mem.tuning_step) + if mem.power == POWER_LEVELS[1]: # Low + _mem.power = 0x00 + else: # Default to High + _mem.power = 0x03 + + _flag["%s_pskip" % nibble] = mem.skip == "P" + _flag["%s_skip" % nibble] = mem.skip == "S" + + for i in range(0, 6): + _mem.name[i] = CHARSET.index(mem.name.ljust(6)[i]) + if mem.name.strip(): + # empty name field, disable name display + # leftmost bit of name chararr is: + # 1 = display freq, 0 = display name + _mem.name[0] |= 0x80 + + # for now, clear unknown fields + for i in range(1, 7): + setattr(_mem, "unknown%i" % i, 0) + + def validate_memory(self, mem): + msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem) + return msgs + + def get_bank_model(self): + return VX2BankModel(self) + + def _decode_chars(self, inarr): + LOG.debug("@_decode_chars, type: %s" % type(inarr)) + LOG.debug(inarr) + outstr = "" + for i in inarr: + if i == 0xFF: + break + outstr += CHARSET[i & 0x7F] + return outstr.rstrip() + + def _encode_chars(self, instr, length=16): + LOG.debug("@_encode_chars, type: %s" % type(instr)) + LOG.debug(instr) + outarr = [] + instr = str(instr) + for i in range(0, length): + if i < len(instr): + outarr.append(CHARSET.index(instr[i])) + else: + outarr.append(0xFF) + return outarr + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + dtmf = RadioSettingGroup("dtmf", "DTMF") + arts = RadioSettingGroup("arts", "ARTS") + top = RadioSettings(basic, arts, dtmf) + + options = ["off", "30m", "1h", "3h", "5h", "8h"] + rs = RadioSetting( + "apo", "APO time (hrs)", + RadioSettingValueList(options, options[_settings.apo])) + basic.append(rs) + + rs = RadioSetting( + "ars", "Auto Repeater Shift", + RadioSettingValueBoolean(_settings.ars)) + basic.append(rs) + + rs = RadioSetting( + "att", "Attenuation", + RadioSettingValueBoolean(_settings.att)) + basic.append(rs) + + rs = RadioSetting( + "bclo", "Busy Channel Lockout", + RadioSettingValueBoolean(_settings.bclo)) + basic.append(rs) + + rs = RadioSetting( + "beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + options = ["off", "1", "3", "5", "8", "cont"] + rs = RadioSetting( + "bell", "Bell", + RadioSettingValueList(options, options[_settings.bell])) + basic.append(rs) + + rs = RadioSetting( + "busyled", "Busy LED", + RadioSettingValueBoolean(_settings.busyled)) + basic.append(rs) + + options = ["5", "10", "50", "100"] + rs = RadioSetting( + "chcounter", "Channel Counter (MHz)", + RadioSettingValueList(options, options[_settings.chcounter])) + basic.append(rs) + + rs = RadioSetting( + "dcsrev", "DCS Reverse", + RadioSettingValueBoolean(_settings.dcsrev)) + basic.append(rs) + + options = list(map(str, list(range(0, 12+1)))) + rs = RadioSetting( + "dimmer", "Dimmer", + RadioSettingValueList(options, options[_settings.dimmer])) + basic.append(rs) + + rs = RadioSetting( + "edgebeep", "Edge Beep", + RadioSettingValueBoolean(_settings.edgebeep)) + basic.append(rs) + + options = ["beep", "strobe", "bp+str", "beam", + "bp+beam", "cw", "bp+cw"] + rs = RadioSetting( + "emergmode", "Emergency Mode", + RadioSettingValueList(options, options[_settings.emergmode])) + basic.append(rs) + + options = ["Home", "Reverse"] + rs = RadioSetting( + "hmrv", "HM/RV key", + RadioSettingValueList(options, options[_settings.hmrv])) + basic.append(rs) + + options = ["My Menu", "Internet"] + rs = RadioSetting( + "internet_key", "Internet key", + RadioSettingValueList( + options, options[_settings.internet_key])) + basic.append(rs) + + options = ["1 APO", "2 AR BEP", "3 AR INT", "4 ARS", "5 ATT", + "6 BCLO", "7 BEEP", "8 BELL", "9 BSYLED", "10 CH CNT", + "11 CK SFT", "12 CW ID", "13 DC VLT", "14 DCS CD", + "15 DCS RV", "16 DIMMER", "17 DTMF", "18 DTMF S", + "19 EDG BP", "20 EMG S", "21 HLFDEV", "22 HM/RV", + "23 INT MD", "24 LAMP", "25 LOCK", "26 M/T-CL", + "27 MW MD", "28 NAME", "29 NM SET", "30 OPNMSG", + "31 RESUME", "32 RF SQL", "33 RPT", "34 RX MD", + "35 RXSAVE", "36 S SCH", "37 SCNLMP", "38 SHIFT", + "39 SKIP", "40 SPLIT", "41 SQL", "42 SQL TYP", + "43 STEP", "44 TN FRQ", "45 TOT", "46 TXSAVE", + "47 VFO MD", "48 TR SQL (JAPAN)", "48 WX ALT"] + + rs = RadioSetting( + "mymenu", "My Menu function", + RadioSettingValueList(options, options[_settings.mymenu - 9])) + basic.append(rs) + + options = ["wires", "link"] + rs = RadioSetting( + "internet_mode", "Internet mode", + RadioSettingValueList( + options, options[_settings.internet_mode])) + basic.append(rs) + + options = ["key", "cont", "off"] + rs = RadioSetting( + "lamp", "Lamp mode", + RadioSettingValueList(options, options[_settings.lamp])) + basic.append(rs) + + options = ["key", "dial", "key+dial", "ptt", + "key+ptt", "dial+ptt", "all"] + rs = RadioSetting( + "lock", "Lock mode", + RadioSettingValueList(options, options[_settings.lock])) + basic.append(rs) + + options = ["monitor", "tone call"] + rs = RadioSetting( + "moni_tcall", "MONI key", + RadioSettingValueList(options, options[_settings.moni_tcall])) + basic.append(rs) + + options = ["lower", "next"] + rs = RadioSetting( + "mwmode", "Memory write mode", + RadioSettingValueList(options, options[_settings.mwmode])) + basic.append(rs) + + options = list(map(str, list(range(0, 15+1)))) + rs = RadioSetting( + "nfm_sql", "NFM Sql", + RadioSettingValueList(options, options[_settings.nfm_sql])) + basic.append(rs) + + options = list(map(str, list(range(0, 8+1)))) + rs = RadioSetting( + "wfm_sql", "WFM Sql", + RadioSettingValueList(options, options[_settings.wfm_sql])) + basic.append(rs) + + options = ["off", "dc", "msg"] + rs = RadioSetting( + "openmsgmode", "Opening message", + RadioSettingValueList(options, options[_settings.openmsgmode])) + basic.append(rs) + + openmsg = RadioSettingValueString( + 0, 6, self._decode_chars(_settings.openmsg.get_value())) + openmsg.set_charset(CHARSET) + rs = RadioSetting("openmsg", "Opening Message", openmsg) + basic.append(rs) + + options = ["3s", "5s", "10s", "busy", "hold"] + rs = RadioSetting( + "resume", "Resume", + RadioSettingValueList(options, options[_settings.resume])) + basic.append(rs) + + options = ["off"] + list(map(str, list(range(1, 9+1)))) + rs = RadioSetting( + "rfsql", "RF Sql", + RadioSettingValueList(options, options[_settings.rfsql])) + basic.append(rs) + + options = ["off", "200ms", "300ms", "500ms", "1s", "2s"] + rs = RadioSetting( + "rxsave", "RX pwr save", + RadioSettingValueList(options, options[_settings.rxsave])) + basic.append(rs) + + options = ["single", "cont"] + rs = RadioSetting( + "smartsearch", "Smart search", + RadioSettingValueList(options, options[_settings.smartsearch])) + basic.append(rs) + + rs = RadioSetting( + "scan_lamp", "Scan lamp", + RadioSettingValueBoolean(_settings.scan_lamp)) + basic.append(rs) + + rs = RadioSetting( + "split", "Split", + RadioSettingValueBoolean(_settings.split)) + basic.append(rs) + + options = ["off", "1", "3", "5", "10"] + rs = RadioSetting( + "tot", "TOT (mins)", + RadioSettingValueList(options, options[_settings.tot])) + basic.append(rs) + + rs = RadioSetting( + "txsave", "TX pwr save", + RadioSettingValueBoolean(_settings.txsave)) + basic.append(rs) + + options = ["all", "band"] + rs = RadioSetting( + "vfomode", "VFO mode", + RadioSettingValueList(options, options[_settings.vfomode])) + basic.append(rs) + + rs = RadioSetting( + "wx_alert", "WX Alert", + RadioSettingValueBoolean(_settings.wx_alert)) + basic.append(rs) + + # todo: priority channel + + # todo: handle WX ch labels + + # arts settings (ar beep, ar int, cwid en, cwid field) + options = ["15s", "25s"] + rs = RadioSetting( + "artsinterval", "ARTS Interval", + RadioSettingValueList( + options, options[_settings.artsinterval])) + arts.append(rs) + + options = ["off", "in range", "always"] + rs = RadioSetting( + "artsbeep", "ARTS Beep", + RadioSettingValueList(options, options[_settings.artsbeep])) + arts.append(rs) + + rs = RadioSetting( + "cwid_en", "CWID Enable", + RadioSettingValueBoolean(_settings.cwid_en)) + arts.append(rs) + + cwid = RadioSettingValueString( + 0, 16, self._decode_chars(_settings.cwid.get_value())) + cwid.set_charset(CHARSET) + rs = RadioSetting("cwid", "CWID", cwid) + arts.append(rs) + + # setup dtmf + options = ["manual", "auto"] + rs = RadioSetting( + "dtmfmode", "DTMF mode", + RadioSettingValueList(options, options[_settings.dtmfmode])) + dtmf.append(rs) + + for i in range(0, 8+1): + name = "dtmf" + str(i+1) + dtmfsetting = self._memobj.dtmf[i] + # dtmflen = getattr(_settings, objname + "_len") + dtmfstr = "" + for c in dtmfsetting.digits: + if c < len(DTMFCHARSET): + dtmfstr += DTMFCHARSET[c] + LOG.debug(dtmfstr) + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + dtmfentry.set_charset(DTMFCHARSET + list(" ")) + rs = RadioSetting(name, name.upper(), dtmfentry) + dtmf.append(rs) + + return top + + def set_settings(self, uisettings): + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + _settings = self._memobj.settings + if re.match('dtmf\d', setting): + # set dtmf fields + dtmfstr = str(element.value).strip() + newval = [] + for i in range(0, 16): + if i < len(dtmfstr): + newval.append(DTMFCHARSET.index(dtmfstr[i])) + else: + newval.append(0xFF) + LOG.debug(newval) + idx = int(setting[-1:]) - 1 + _settings = self._memobj.dtmf[idx] + _settings.digits = newval + continue + if setting == "prioritychan": + # prioritychan is top-level member, fix 0 index + element.value -= 1 + _settings = self._memobj + if setting == "mymenu": + opts = element.value.get_options() + optsidx = opts.index(element.value.get_value()) + idx = optsidx + 9 + setattr(_settings, "mymenu", idx) + continue + oldval = getattr(_settings, setting) + newval = element.value + if setting == "cwid": + newval = self._encode_chars(newval) + if setting == "openmsg": + newval = self._encode_chars(newval, 6) + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception as e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/vx3.py b/chirp/drivers/vx3.py new file mode 100644 index 0000000..0eba272 --- /dev/null +++ b/chirp/drivers/vx3.py @@ -0,0 +1,978 @@ +# Copyright 2011 Rick Farina +# based on modification of Dan Smith's original work +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +from textwrap import dedent +import os +import re +import logging + +LOG = logging.getLogger(__name__) + +# interesting offsets which may be checksums needed later +# 0x0393 checksum1? +# 0x0453 checksum1a? +# 0x0409 checksum2? +# 0x04C9 checksum2a? + +MEM_FORMAT = """ +#seekto 0x7F4A; +u8 checksum; + +#seekto 0x024A; +struct { + u8 unk01_1:3, + att_broadcast:1, + att_marine:1, + unk01_2:2 + att_wx:1; + u8 unk02; + u8 apo; + u8 arts_beep; + u8 unk04_1; + u8 beep_level; + u8 beep_mode; + u8 unk04_2; + u8 arts_cwid[16]; + u8 unk05[10]; + u8 channel_counter; + u8 unk06_1[2]; + u8 dtmf_delay; + u8 dtmf_chan_active; + u8 unk06_2[5]; + u8 emergency_eai_time; + u8 emergency_signal; + u8 unk07[30]; + u8 fw_key_timer; + u8 internet_key; + u8 lamp; + u8 lock_mode; + u8 my_key; + u8 mic_gain; + u8 mem_ch_step; + u8 unk08[3]; + u8 sql_fm; + u8 sql_wfm; + u8 radio_am_sql; + u8 radio_fm_sql; + u8 on_timer; + u8 openmsg_mode; + u8 openmsg[6]; + u8 pager_rxtone1; + u8 pager_rxtone2; + u8 pager_txtone1; + u8 pager_txtone2; + u8 password[4]; + u8 unk10; + u8 priority_time; + u8 ptt_delay; + u8 rx_save; + u8 scan_resume; + u8 scan_restart; + u8 sub_rx_timer; + u8 unk11[7]; + u8 tot; + u8 wake_up; + u8 unk12[2]; + u8 vfo_mode:1, + arts_cwid_enable:1, + scan_lamp:1, + fast_tone_search:1, + ars:1, + dtmf_speed:1, + split_tone:1, + dtmf_autodialer:1; + u8 busy_led:1, + tone_search_mute:1, + unk14_1:1, + bclo:1, + band_edge_beep:1, + unk14_2:2, + txsave:1; + u8 unk15_1:2, + smart_search:1, + emergency_eai:1, + unk15_2:2, + hm_rv:1, + moni_tcall:1; + u8 lock:1, + unk16_1:1, + arts:1, + arts_interval:1, + unk16_2:1, + protect_memory:1, + unk16_3:1, + mem_storage:1; + u8 vol_key_mode:1, + unk17_1:2, + wx_alert:1, + temp_unit:1, + unk17_2:2, + password_active:1; + u8 fm_broadcast_mode:1, + fm_antenna:1, + am_antenna:1, + fm_speaker_out:1, + home_vfo:1, + unk18_1:2, + priority_revert:1; +} settings; + +// banks? +#seekto 0x034D; +u8 banks_unk1; + +#seekto 0x0356; +struct { + u32 unmask; +} banks_unmask1; + +#seekto 0x0409; +u8 banks_unk3; + +#seekto 0x0416; +struct { + u32 unmask; +} banks_unmask2; + +#seekto 0x04CA; +struct { + u8 memory[16]; +} dtmf[10]; + +#seekto 0x0B7A; +struct { + u8 name[6]; +} bank_names[24]; + +#seekto 0x0E0A; +struct { + u16 channels[100]; +} banks[24]; + +#seekto 0x02EE; +struct { + u16 in_use; +} bank_used[24]; + +#seekto 0x03FE; +struct { + u8 speaker; + u8 earphone; +} volumes; + +#seekto 0x20CA; +struct { + u8 even_pskip:1, + even_skip:1, + even_valid:1, // TODO: should be "showname", i.e., show alpha name + even_masked:1, + odd_pskip:1, + odd_skip:1, + odd_valid:1, + odd_masked:1; +} flags[999]; + +#seekto 0x244A; +struct { + u8 unknown1a:2, + txnarrow:1, + clockshift:1, + unknown1b:4; + u8 mode:2, + duplex:2, + tune_step:4; + bbcd freq[3]; + u8 power:2, + unknown2:4, + tmode:2; // TODO: tmode should be 6 bits (extended tone modes) + u8 name[6]; + bbcd offset[3]; + u8 unknown3:2, + tone:6; + u8 unknown4:1, + dcs:7; + u8 unknown5; + u8 smetersquelch; + u8 unknown7a:2, + attenuate:1, + unknown7b:1, + automode:1, + unknown8:1, + bell:2; +} memory[999]; +""" + +# fix auto mode setting and auto step setting + +DUPLEX = ["", "-", "+", "split"] +MODES = ["FM", "AM", "WFM", "Auto", "NFM"] # NFM handled specially in radio +TMODES = ["", "Tone", "TSQL", "DTCS"] +# TODO: TMODES = ["", "Tone, "TSQL", "DTCS", "Rev Tone", "User Tone", "Pager", +# "Message", "D Code", "Tone/DTCS", "DTCS/Tone"] + +# still need to verify 9 is correct, and add auto: look at byte 1 and 20 +STEPS = [5.0, 9, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +# STEPS = list(chirp_common.TUNING_STEPS) +# STEPS.remove(6.25) +# STEPS.remove(30.0) +# STEPS.append(100.0) +# STEPS.append(9.0) #this fails because 9 is out of order in the list + +# Empty char should be 0xFF but right now we are coding in a space +CHARSET = list("0123456789" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ " + + "+-/\x00[](){}\x00\x00_" + + ("\x00" * 13) + "*" + "\x00\x00,'|\x00\x00\x00\x00" + + ("\x00" * 64)) + +DTMFCHARSET = list("0123456789ABCD*#") +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=1.50), + chirp_common.PowerLevel("Low", watts=0.10)] + + +class VX3Bank(chirp_common.NamedBank): + """A VX3 Bank""" + def get_name(self): + _bank = self._model._radio._memobj.bank_names[self.index] + name = "" + for i in _bank.name: + if i == 0xFF: + break + name += CHARSET[i & 0x7F] + return name.rstrip() + + def set_name(self, name): + name = name.upper() + _bank = self._model._radio._memobj.bank_names[self.index] + _bank.name = [CHARSET.index(x) for x in name.ljust(6)[:6]] + + +class VX3BankModel(chirp_common.BankModel): + """A VX-3 bank model""" + + def get_num_mappings(self): + return len(self.get_mappings()) + + def get_mappings(self): + banks = self._radio._memobj.banks + bank_mappings = [] + for index, _bank in enumerate(banks): + bank = VX3Bank(self, "%i" % index, "b%i" % (index + 1)) + bank.index = index + bank_mappings.append(bank) + + return bank_mappings + + def _get_channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xFFFF: + return set() + + _members = self._radio._memobj.banks[bank.index] + return set([int(ch) + 1 for ch in _members.channels if ch != 0xFFFF]) + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.banks[bank.index] + if len(channels_in_bank) > len(_members.channels): + raise Exception("Too many entries in bank %d" % bank.index) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channels[index] = channel_number - 1 + empty = index + 1 + for index in range(empty, len(_members.channels)): + _members.channels[index] = 0xFFFF + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = ((len(channels_in_bank) - 1) * 2) + _banks_unmask1 = self._radio._memobj.banks_unmask1 + _banks_unmask2 = self._radio._memobj.banks_unmask2 + _banks_unmask1.unmask = 0x0017FFFF + _banks_unmask2.unmask = 0x0017FFFF + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + _bank_used = self._radio._memobj.bank_used[bank.index] + if channels_in_bank: + _bank_used.in_use = ((len(channels_in_bank) - 1) * 2) + else: + _bank_used.in_use = 0xFFFF + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._get_channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._get_channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +def _wipe_memory(mem): + mem.set_raw("\x00" * (mem.size() // 8)) + # the following settings are set to match the defaults + # on the radio, some of these fields are unknown + mem.name = [0xFF for _i in range(0, 6)] + mem.unknown5 = 0x0D # not sure what this is + mem.unknown7a = 0b0 + mem.unknown7b = 0b1 + mem.automode = 0x01 # autoselect mode + + +@directory.register +class VX3Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu VX-3""" + BAUD_RATE = 19200 + VENDOR = "Yaesu" + MODEL = "VX-3" + + # 41 48 30 32 38 + _model = "AH028" + _memsize = 32587 + _block_lengths = [10, 32577] + # right now this reads in 45 seconds and writes in 41 seconds + _block_size = 32 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [F/W] key while turning the radio on + ("CLONE" will appear on the display). +4. After clicking OK, press the [BAND] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [F/W] key while turning the radio on + ("CLONE" will appear on the display). +4. Press the [V/M] key ("-WAIT-" will appear on the LCD).""")) + return rp + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x7F49)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = True + rf.has_bank_names = True + rf.has_dtcs_polarity = False + rf.valid_modes = list(set(MODES)) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(500000, 999000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 6 + rf.memory_bounds = (1, 999) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + def get_memory(self, number): + _mem = self._memobj.memory[number-1] + _flag = self._memobj.flags[(number-1)/2] + + nibble = ((number-1) % 2) and "even" or "odd" + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + pskip = _flag["%s_pskip" % nibble] + skip = _flag["%s_skip" % nibble] + + mem = chirp_common.Memory() + mem.number = number + + if not used: + mem.empty = True + if not valid: + mem.empty = True + mem.power = POWER_LEVELS[0] + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 1000 + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + if _mem.txnarrow and _mem.mode == MODES.index("FM"): + # FM narrow + mem.mode = "NFM" + else: + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.tuning_step = STEPS[_mem.tune_step] + mem.skip = pskip and "P" or skip and "S" or "" + mem.power = POWER_LEVELS[~_mem.power & 0x01] + + for i in _mem.name: + if i == 0xFF: + break + mem.name += CHARSET[i & 0x7F] + mem.name = mem.name.rstrip() + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + _flag = self._memobj.flags[(mem.number-1)/2] + + nibble = ((mem.number-1) % 2) and "even" or "odd" + + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + + if not mem.empty and not valid: + _wipe_memory(_mem) + + if mem.empty and valid and not used: + _flag["%s_valid" % nibble] = False + return + _flag["%s_masked" % nibble] = not mem.empty + + if mem.empty: + return + + _flag["%s_valid" % nibble] = True + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 1000 + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + if mem.mode == "NFM": + _mem.mode = MODES.index("FM") + _mem.txnarrow = True + else: + _mem.mode = MODES.index(mem.mode) + _mem.txnarrow = False + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = STEPS.index(mem.tuning_step) + if mem.power == POWER_LEVELS[1]: # Low + _mem.power = 0x00 + else: # Default to High + _mem.power = 0x03 + + _flag["%s_pskip" % nibble] = mem.skip == "P" + _flag["%s_skip" % nibble] = mem.skip == "S" + + for i in range(0, 6): + _mem.name[i] = CHARSET.index(mem.name.ljust(6)[i]) + if mem.name.strip(): + _mem.name[0] |= 0x80 + + def validate_memory(self, mem): + msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem) + return msgs + + def get_bank_model(self): + return VX3BankModel(self) + + def _decode_chars(self, inarr): + LOG.debug("@_decode_chars, type: %s" % type(inarr)) + LOG.debug(inarr) + outstr = "" + for i in inarr: + if i == 0xFF: + break + outstr += CHARSET[i & 0x7F] + return outstr.rstrip() + + def _encode_chars(self, instr, length=16): + LOG.debug("@_encode_chars, type: %s" % type(instr)) + LOG.debug(instr) + outarr = [] + instr = str(instr) + for i in range(length): + if i < len(instr): + outarr.append(CHARSET.index(instr[i])) + else: + outarr.append(0xFF) + return outarr + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + sound = RadioSettingGroup("sound", "Sound") + dtmf = RadioSettingGroup("dtmf", "DTMF") + arts = RadioSettingGroup("arts", "ARTS") + eai = RadioSettingGroup("eai", "Emergency") + msg = RadioSettingGroup("msg", "Messages") + + top = RadioSettings(basic, sound, arts, dtmf, eai, msg) + + basic.append(RadioSetting( + "att_wx", "Attenuation WX", + RadioSettingValueBoolean(_settings.att_wx))) + + basic.append(RadioSetting( + "att_marine", "Attenuation Marine", + RadioSettingValueBoolean(_settings.att_marine))) + + basic.append(RadioSetting( + "att_broadcast", "Attenuation Broadcast", + RadioSettingValueBoolean(_settings.att_broadcast))) + + basic.append(RadioSetting( + "ars", "Auto Repeater Shift", + RadioSettingValueBoolean(_settings.ars))) + + basic.append(RadioSetting( + "home_vfo", "Home->VFO", + RadioSettingValueBoolean(_settings.home_vfo))) + + basic.append(RadioSetting( + "bclo", "Busy Channel Lockout", + RadioSettingValueBoolean(_settings.bclo))) + + basic.append(RadioSetting( + "busyled", "Busy LED", + RadioSettingValueBoolean(_settings.busy_led))) + + basic.append(RadioSetting( + "fast_tone_search", "Fast Tone search", + RadioSettingValueBoolean(_settings.fast_tone_search))) + + basic.append(RadioSetting( + "priority_revert", "Priority Revert", + RadioSettingValueBoolean(_settings.priority_revert))) + + basic.append(RadioSetting( + "protect_memory", "Protect memory", + RadioSettingValueBoolean(_settings.protect_memory))) + + basic.append(RadioSetting( + "scan_lamp", "Scan Lamp", + RadioSettingValueBoolean(_settings.scan_lamp))) + + basic.append(RadioSetting( + "split_tone", "Split tone", + RadioSettingValueBoolean(_settings.split_tone))) + + basic.append(RadioSetting( + "tone_search_mute", "Tone search mute", + RadioSettingValueBoolean(_settings.tone_search_mute))) + + basic.append(RadioSetting( + "txsave", "TX save", + RadioSettingValueBoolean(_settings.txsave))) + + basic.append(RadioSetting( + "wx_alert", "WX Alert", + RadioSettingValueBoolean(_settings.wx_alert))) + + opts = ["Bar Int", "Bar Ext"] + basic.append(RadioSetting( + "am_antenna", "AM antenna", + RadioSettingValueList(opts, opts[_settings.am_antenna]))) + + opts = ["Ext Ant", "Earphone"] + basic.append(RadioSetting( + "fm_antenna", "FM antenna", + RadioSettingValueList(opts, opts[_settings.fm_antenna]))) + + opts = ["off"] + ["%0.1f" % (t / 60.0) for t in range(30, 750, 30)] + basic.append(RadioSetting( + "apo", "APO time (hrs)", + RadioSettingValueList(opts, opts[_settings.apo]))) + + opts = ["+/- 5 MHZ", "+/- 10 MHZ", "+/- 50 MHZ", "+/- 100 MHZ"] + basic.append(RadioSetting( + "channel_counter", "Channel counter", + RadioSettingValueList(opts, opts[_settings.channel_counter]))) + + opts = ["0.3", "0.5", "0.7", "1.0", "1.5"] + basic.append(RadioSetting( + "fw_key_timer", "FW key timer (s)", + RadioSettingValueList(opts, opts[_settings.fw_key_timer]))) + + opts = ["Home", "Reverse"] + basic.append(RadioSetting( + "hm_rv", "HM/RV key", + RadioSettingValueList(opts, opts[_settings.hm_rv]))) + + opts = ["%d" % t for t in range(2, 11)] + ["continuous", "off"] + basic.append(RadioSetting( + "lamp", "Lamp Timer (s)", + RadioSettingValueList(opts, opts[_settings.lamp]))) + + basic.append(RadioSetting( + "lock", "Lock", + RadioSettingValueBoolean(_settings.lock))) + + opts = ["key", "ptt", "key+ptt"] + basic.append(RadioSetting( + "lock_mode", "Lock mode", + RadioSettingValueList(opts, opts[_settings.lock_mode]))) + + opts = ["10", "20", "50", "100"] + basic.append(RadioSetting( + "mem_ch_step", "Memory Chan step", + RadioSettingValueList(opts, opts[_settings.mem_ch_step]))) + + opts = ["lower", "next"] + basic.append(RadioSetting( + "mem_storage", "Memory storage mode", + RadioSettingValueList(opts, opts[_settings.mem_storage]))) + + opts = ["%d" % t for t in range(1, 10)] + basic.append(RadioSetting( + "mic_gain", "Mic gain", + RadioSettingValueList(opts, opts[_settings.mic_gain]))) + + opts = ["monitor", "tone call"] + basic.append(RadioSetting( + "moni_tcall", "Moni/TCall button", + RadioSettingValueList(opts, opts[_settings.moni_tcall]))) + + opts = ["off"] + \ + ["%02d:%02d" % (t / 60, t % 60) for t in range(10, 1450, 10)] + basic.append(RadioSetting( + "on_timer", "On Timer (hrs)", + RadioSettingValueList(opts, opts[_settings.on_timer]))) + + opts2 = ["off"] + \ + ["0.%d" % t for t in range(1, 10)] + \ + ["%1.1f" % (t / 10.0) for t in range(10, 105, 5)] + basic.append(RadioSetting( + "priority_time", "Priority time", + RadioSettingValueList(opts2, opts2[_settings.priority_time]))) + + opts = ["off", "20", "50", "100", "200"] + basic.append(RadioSetting( + "ptt_delay", "PTT delay (ms)", + RadioSettingValueList(opts, opts[_settings.ptt_delay]))) + + basic.append(RadioSetting( + "rx_save", "RX save (s)", + RadioSettingValueList(opts2, opts2[_settings.rx_save]))) + + basic.append(RadioSetting( + "scan_restart", "Scan restart (s)", + RadioSettingValueList(opts2, opts2[_settings.scan_restart]))) + + opts = ["%1.1f" % (t / 10.0) for t in range(20, 105, 5)] + \ + ["busy", "hold"] + basic.append(RadioSetting( + "scan_resume", "Scan resume (s)", + RadioSettingValueList(opts, opts[_settings.scan_resume]))) + + opts = ["single", "continuous"] + basic.append(RadioSetting( + "smart_search", "Smart search", + RadioSettingValueList(opts, opts[_settings.smart_search]))) + + opts = ["off"] + ["TRX %d" % t for t in range(1, 11)] + ["hold"] + \ + ["TX %d" % t for t in range(1, 11)] + basic.append(RadioSetting( + "sub_rx_timer", "Sub RX timer", + RadioSettingValueList(opts, opts[_settings.sub_rx_timer]))) + + opts = ["C", "F"] + basic.append(RadioSetting( + "temp_unit", "Temperature unit", + RadioSettingValueList(opts, opts[_settings.temp_unit]))) + + opts = ["off"] + ["%1.1f" % (t / 10.0) for t in range(5, 105, 5)] + basic.append(RadioSetting( + "tot", "Time-out timer (mins)", + RadioSettingValueList(opts, opts[_settings.tot]))) + + opts = ["all", "band"] + basic.append(RadioSetting( + "vfo_mode", "VFO mode", + RadioSettingValueList(opts, opts[_settings.vfo_mode]))) + + opts = ["off"] + ["%d" % t for t in range(5, 65, 5)] + ["EAI"] + basic.append(RadioSetting( + "wake_up", "Wake up (s)", + RadioSettingValueList(opts, opts[_settings.wake_up]))) + + opts = ["hold", "3 secs"] + basic.append(RadioSetting( + "vol_key_mode", "Volume key mode", + RadioSettingValueList(opts, opts[_settings.vol_key_mode]))) + + # subgroup programmable keys + + opts = ["INTNET", "INT MR", "Set Mode (my key)"] + basic.append(RadioSetting( + "internet_key", "Internet key", + RadioSettingValueList(opts, opts[_settings.internet_key]))) + + keys = ["Antenna AM", "Antenna FM", "Antenna Attenuator", + "Auto Power Off", "Auto Repeater Shift", "ARTS Beep", + "ARTS Interval", "Busy Channel Lockout", "Bell Ringer", + "Bell Select", "Bank Name", "Band Edge Beep", "Beep Level", + "Beep Select", "Beep User", "Busy LED", "Channel Counter", + "Clock Shift", "CW ID", "CW Learning", "CW Pitch", + "CW Training", "DC Voltage", "DCS Code", "DCS Reverse", + "DTMF A/M", "DTMF Delay", "DTMF Set", "DTMF Speed", + "EAI Timer", "Emergency Alarm", "Ext Menu", "FW Key", + "Half Deviation", "Home/Reverse", "Home > VFO", "INT Code", + "INT Conn Mode", "INT A/M", "INT Set", "INT Key", "INTNET", + "Lamp", "LED Light", "Lock", "Moni/T-Call", "Mic Gain", + "Memory Display", "Memory Write Mode", "Memory Channel Step", + "Memory Name Write", "Memory Protect", "Memory Skip", + "Message List", "Message Reg", "Message Set", "On Timer", + "Open Message", "Pager Answer Back", "Pager Receive Code", + "Pager Transmit Code", "Pager Frequency", "Priority Revert", + "Priority Timer", "Password", "PTT Delay", + "Repeater Shift Direction", "Repeater Shift", "Receive Mode", + "Smart Search", "Save Rx", "Save Tx", "Scan Lamp", + "Scan Resume", "Scan Restart", "Speaker Out", + "Squelch Level", "Squelch Type", "Squelch S Meter", + "Squelch Split Tone", "Step", "Stereo", "Sub Rx", "Temp", + "Tone Frequency", "Time Out Timer", "Tone Search Mute", + "Tone Search Speed", "VFO Band", "VFO Skip", "Volume Mode", + "Wake Up", "Weather Alert"] + rs = RadioSetting( + "my_key", "My key", + RadioSettingValueList(keys, keys[_settings.my_key - 16])) + # TODO: fix keys list isnt exactly right order + # leave disabled in settings for now + # basic.append(rs) + + # sound tab + + sound.append(RadioSetting( + "band_edge_beep", "Band edge beep", + RadioSettingValueBoolean(_settings.band_edge_beep))) + + opts = ["off", "key+scan", "key"] + sound.append(RadioSetting( + "beep_mode", "Beep mode", + RadioSettingValueList(opts, opts[_settings.beep_mode]))) + + _volumes = self._memobj.volumes + + opts = list(map(str, list(range(0, 33)))) + sound.append(RadioSetting( + "speaker_vol", "Speaker volume", + RadioSettingValueList(opts, opts[_volumes.speaker]))) + + sound.append(RadioSetting( + "earphone_vol", "Earphone volume", + RadioSettingValueList(opts, opts[_volumes.earphone]))) + + opts = ["auto", "speaker"] + sound.append(RadioSetting( + "fm_speaker_out", "FM Speaker out", + RadioSettingValueList(opts, opts[_settings.fm_speaker_out]))) + + opts = ["mono", "stereo"] + sound.append(RadioSetting( + "fm_broadcast_mode", "FM broadcast mode", + RadioSettingValueList( + opts, opts[_settings.fm_broadcast_mode]))) + + opts = list(map(str, list(range(16)))) + sound.append(RadioSetting( + "sql_fm", "Squelch level (FM)", + RadioSettingValueList(opts, opts[_settings.sql_fm]))) + + opts = list(map(str, list(range(9)))) + sound.append(RadioSetting( + "sql_wfm", "Squelch level (WFM)", + RadioSettingValueList(opts, opts[_settings.sql_wfm]))) + + opts = list(map(str, list(range(16)))) + sound.append(RadioSetting( + "radio_am_sql", "Squelch level (Broadcast Radio AM)", + RadioSettingValueList(opts, opts[_settings.radio_am_sql]))) + + opts = list(map(str, list(range(9)))) + sound.append(RadioSetting( + "radio_fm_sql", "Squelch level (Broadcast Radio FM)", + RadioSettingValueList(opts, opts[_settings.radio_fm_sql]))) + + # dtmf tab + + opts = ["manual", "auto"] + dtmf.append(RadioSetting( + "dtmf_autodialer", "DTMF autodialer mode", + RadioSettingValueList(opts, opts[_settings.dtmf_autodialer]))) + + opts = ["50", "250", "450", "750", "1000"] + dtmf.append(RadioSetting( + "dtmf_delay", "DTMF delay (ms)", + RadioSettingValueList(opts, opts[_settings.dtmf_delay]))) + + opts = ["50", "100"] + dtmf.append(RadioSetting( + "dtmf_speed", "DTMF speed (ms)", + RadioSettingValueList(opts, opts[_settings.dtmf_speed]))) + + opts = list(map(str, list(range(10)))) + dtmf.append(RadioSetting( + "dtmf_chan_active", "DTMF active", + RadioSettingValueList( + opts, opts[_settings.dtmf_chan_active]))) + + for i in range(10): + name = "dtmf" + str(i) + dtmfsetting = self._memobj.dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c < len(DTMFCHARSET): + dtmfstr += DTMFCHARSET[c] + LOG.debug(dtmfstr) + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + dtmfentry.set_charset(DTMFCHARSET + list(" ")) + rs = RadioSetting(name, name.upper(), dtmfentry) + dtmf.append(rs) + + # arts tab + arts.append(RadioSetting( + "arts", "ARTS", + RadioSettingValueBoolean(_settings.arts))) + + opts = ["off", "in range", "always"] + arts.append(RadioSetting( + "arts_beep", "ARTS beep", + RadioSettingValueList(opts, opts[_settings.arts_beep]))) + + opts = ["15", "25"] + arts.append(RadioSetting( + "arts_interval", "ARTS interval", + RadioSettingValueList(opts, opts[_settings.arts_interval]))) + + arts.append(RadioSetting( + "arts_cwid_enable", "CW ID", + RadioSettingValueBoolean(_settings.arts_cwid_enable))) + + cwid = RadioSettingValueString( + 0, 16, self._decode_chars(_settings.arts_cwid.get_value())) + cwid.set_charset(CHARSET) + arts.append(RadioSetting("arts_cwid", "CW ID", cwid)) + + # EAI tab + + eai.append(RadioSetting( + "emergency_eai", "EAI", + RadioSettingValueBoolean(_settings.emergency_eai))) + + opts = ["interval %dm" % t for t in range(1, 10)] + \ + ["interval %dm" % t for t in range(10, 55, 5)] + \ + ["continuous %dm" % t for t in range(1, 10)] + \ + ["continuous %dm" % t for t in range(10, 55, 5)] + + eai.append(RadioSetting( + "emergency_eai_time", "EAI time", + RadioSettingValueList( + opts, opts[_settings.emergency_eai_time]))) + + opts = ["beep", "strobe", "beep+strobe", "beam", + "beep+beam", "cw", "beep+cw", "cwt"] + eai.append(RadioSetting( + "emergency_signal", "emergency signal", + RadioSettingValueList( + opts, opts[_settings.emergency_signal]))) + + # msg tab + + opts = ["off", "dc voltage", "message"] + msg.append(RadioSetting( + "openmsg_mode", "Opening message mode", + RadioSettingValueList(opts, opts[_settings.openmsg_mode]))) + + openmsg = RadioSettingValueString( + 0, 6, self._decode_chars(_settings.openmsg.get_value())) + openmsg.set_charset(CHARSET) + msg.append(RadioSetting("openmsg", "Opening Message", openmsg)) + + return top + + def set_settings(self, uisettings): + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + _settings = self._memobj.settings + if re.match('dtmf\d', setting): + # set dtmf fields + dtmfstr = str(element.value).strip() + newval = [] + for i in range(0, 16): + if i < len(dtmfstr): + newval.append(DTMFCHARSET.index(dtmfstr[i])) + else: + newval.append(0xFF) + LOG.debug(newval) + idx = int(setting[-1:]) + _settings = self._memobj.dtmf[idx] + _settings.memory = newval + continue + if re.match('.*_vol$', setting): + # volume fields + voltype = re.sub('_vol$', '', setting) + setattr(self._memobj.volumes, voltype, element.value) + continue + if setting == "my_key": + # my_key is memory is off by 9 from list, beware hacks! + opts = element.value.get_options() + optsidx = opts.index(element.value.get_value()) + idx = optsidx + 16 + setattr(_settings, "my_key", idx) + continue + oldval = getattr(_settings, setting) + newval = element.value + if setting == "arts_cwid": + newval = self._encode_chars(newval) + if setting == "openmsg": + newval = self._encode_chars(newval, 6) + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + setattr(_settings, setting, newval) + except Exception as e: + LOG.debug(element.get_name()) + raise diff --git a/chirp/drivers/vx5.py b/chirp/drivers/vx5.py new file mode 100644 index 0000000..e14c1f2 --- /dev/null +++ b/chirp/drivers/vx5.py @@ -0,0 +1,339 @@ +# Copyright 2011 Dan Smith +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, errors, bitwise +from textwrap import dedent + +MEM_FORMAT = """ +struct mem_struct { + u8 unknown1:2, + half_deviation:1, + cpu_shift:1, + unknown2:1, + sp_rx1:1, + unknown3:1, + sp_rx2:1; + u8 unknown4:4, + tuning_step:4; + bbcd freq[3]; + u8 icon:6, + mode:2; + char name[8]; + bbcd offset[3]; + u8 tmode:4, + power:2, + duplex:2; + u8 unknown5:2, + tone:6; + u8 unknown6:1, + dtcs:7; + u8 unknown7; + u8 unknown8; +}; + +struct flag_struct { + u8 zeros:4, + pskip:1, + skip:1, + visible:1, + used:1; +}; + +#seekto 0x002A; +struct { + u8 current_member; +} bank_used[5]; + +#seekto 0x0032; +struct { + struct { + u8 status; + u8 channel; + } members[24]; +} bank_groups[5]; + +#seekto 0x012A; +struct flag_struct flag[220]; +struct flag_struct specialflag[20]; + +#seekto 0x026A; +struct mem_struct memory[220]; +struct mem_struct special[50]; + +#seekto 0x1D03; +u8 current_bank; +""" + +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+", "split"] +MODES = ["FM", "AM", "WFM"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("L3", watts=2.50), + chirp_common.PowerLevel("L2", watts=1.00), + chirp_common.PowerLevel("L1", watts=0.05)] +SPECIALS = ["%s%d" % (c, i + 1) for i in range(0, 10) for c in ('L', 'U')] + + +class VX5BankModel(chirp_common.BankModel): + def get_num_mappings(self): + return 5 + + def get_mappings(self): + banks = [] + for i in range(0, self.get_num_mappings()): + bank = chirp_common.Bank(self, "%i" % (i+1), "MG%i" % (i+1)) + bank.index = i + banks.append(bank) + return banks + + def add_memory_to_mapping(self, memory, bank): + _members = self._radio._memobj.bank_groups[bank.index].members + _bank_used = self._radio._memobj.bank_used[bank.index] + for i in range(0, len(_members)): + if _members[i].status == 0xFF: + # LOG.debug("empty found, inserting %d at %d" % + # (memory.number, i)) + if self._radio._memobj.current_bank == 0xFF: + self._radio._memobj.current_bank = bank.index + _members[i].status = 0x00 + _members[i].channel = memory.number - 1 + _bank_used.current_member = i + return True + raise Exception(_("{bank} is full").format(bank=bank)) + + def remove_memory_from_mapping(self, memory, bank): + _members = self._radio._memobj.bank_groups[bank.index].members + _bank_used = self._radio._memobj.bank_used[bank.index] + + found = False + remaining_members = 0 + for i in range(0, len(_members)): + if _members[i].status == 0x00: + if _members[i].channel == (memory.number - 1): + _members[i].status = 0xFF + found = True + else: + remaining_members += 1 + + if not found: + raise Exception(_("Memory {num} not in " + "bank {bank}").format(num=memory.number, + bank=bank)) + if not remaining_members: + _bank_used.current_member = 0xFF + + def get_mapping_memories(self, bank): + memories = [] + + _members = self._radio._memobj.bank_groups[bank.index].members + _bank_used = self._radio._memobj.bank_used[bank.index] + + if _bank_used.current_member == 0xFF: + return memories + + for member in _members: + if member.status == 0xFF: + continue + memories.append(self._radio.get_memory(member.channel+1)) + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in [x.number for x in + self.get_mapping_memories(bank)]: + banks.append(bank) + return banks + + +@directory.register +class VX5Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu VX-5""" + BAUD_RATE = 9600 + VENDOR = "Yaesu" + MODEL = "VX-5" + + _model = "" + _memsize = 8123 + _block_lengths = [10, 16, 8097] + _block_size = 8 + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x1FB9, 0x1FBA)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.can_odd_split = True + rf.has_bank = True + rf.has_ctone = False + rf.has_dtcs_polarity = False + rf.valid_modes = MODES + ["NFM"] + rf.valid_tmodes = TMODES + rf.valid_tuning_steps = STEPS + rf.valid_duplexes = DUPLEX + rf.memory_bounds = (1, 220) + rf.valid_bands = [(500000, 16000000), + (48000000, 729000000), + (800000000, 999000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_name_length = 8 + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_special_chans = SPECIALS + + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def get_memory(self, number): + mem = chirp_common.Memory() + if isinstance(number, str): + mem.number = len(self._memobj.memory) + SPECIALS.index(number) + 1 + mem.extd_number = number + _mem = self._memobj.special[mem.number - + len(self._memobj.memory) - 1] + _flg = self._memobj.specialflag[mem.number - + len(self._memobj.memory) - 1] + elif number > len(self._memobj.memory): + mem.number = number + mem.extd_number = SPECIALS[number - len(self._memobj.memory) - 1] + _mem = self._memobj.special[mem.number - + len(self._memobj.memory) - 1] + _flg = self._memobj.specialflag[mem.number - + len(self._memobj.memory) - 1] + else: + mem.number = number + _mem = self._memobj.memory[mem.number - 1] + _flg = self._memobj.flag[mem.number - 1] + + if not _flg.visible: + mem.empty = True + if not _flg.used: + mem.empty = True + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.duplex = DUPLEX[_mem.duplex] + mem.name = self.filter_name(str(_mem.name).rstrip()) + mem.mode = MODES[_mem.mode] + if mem.mode == "FM" and _mem.half_deviation: + mem.mode = "NFM" + mem.tuning_step = STEPS[_mem.tuning_step] + mem.offset = int(_mem.offset) * 1000 + mem.power = POWER_LEVELS[3 - _mem.power] + mem.tmode = TMODES[_mem.tmode & 0x3] # masked so bad mems can be read + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + mem.rtone = mem.ctone = chirp_common.OLD_TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + + mem.skip = _flg.pskip and "P" or _flg.skip and "S" or "" + + return mem + + def set_memory(self, mem): + if mem.number > len(self._memobj.memory): + _mem = self._memobj.special[mem.number - + len(self._memobj.memory) - 1] + _flg = self._memobj.specialflag[mem.number - + len(self._memobj.memory) - 1] + else: + _mem = self._memobj.memory[mem.number - 1] + _flg = self._memobj.flag[mem.number - 1] + + # initialize new channel to safe defaults + if not mem.empty and not _flg.used: + _flg.used = True + _mem.unknown1 = 0x00 + _mem.unknown2 = 0x0 + _mem.unknown3 = 0x0 + _mem.unknown4 = 0x0000 + _mem.unknown5 = 0x0 + _mem.unknown6 = 0x0 + _mem.unknown7 = 0x00 + _mem.unknown8 = 0x00 + _mem.cpu_shift = 0x0 + _mem.sp_rx1 = 0x0 + _mem.sp_rx2 = 0x0 + + _mem.icon = 12 # file cabinet icon + + if mem.empty and _flg.used and not _flg.visible: + _flg.used = False + return + _flg.visible = not mem.empty + if mem.empty: + self._wipe_memory_banks(mem) + return + + _mem.freq = int(mem.freq / 1000) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.name = mem.name.ljust(8) + if mem.mode == "NFM": + _mem.mode = MODES.index("FM") + _mem.half_deviation = 1 + else: + _mem.mode = MODES.index(mem.mode) + _mem.half_deviation = 0 + _mem.tuning_step = STEPS.index(mem.tuning_step) + _mem.offset = int(mem.offset / 1000) + if mem.power: + _mem.power = 3 - POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + _mem.tmode = TMODES.index(mem.tmode) + try: + _mem.tone = chirp_common.OLD_TONES.index(mem.rtone) + except ValueError: + raise errors.UnsupportedToneError( + ("This radio does not support tone %s" % mem.rtone)) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + + _flg.skip = mem.skip == "S" + _flg.pskip = mem.skip == "P" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to MIC/EAR jack. + 3. Press and hold in the [F/W] key while turning the radio on + ("CLONE" will appear on the display). + 4. After clicking OK, press the [VFO(DW)SC] key to receive + the image from the radio.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to MIC/EAR jack. + 3. Press and hold in the [F/W] key while turning the radio on + ("CLONE" will appear on the display). + 4. Press the [MR(SKP)SC] key ("CLONE WAIT" will appear + on the LCD). + 5. Click OK to send image to radio.""")) + return rp + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def get_bank_model(self): + return VX5BankModel(self) diff --git a/chirp/drivers/vx510.py b/chirp/drivers/vx510.py new file mode 100644 index 0000000..49570e2 --- /dev/null +++ b/chirp/drivers/vx510.py @@ -0,0 +1,203 @@ +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise + +# This driver is unfinished and therefore does not register itself with Chirp. +# +# Downloads should work consistently but an upload has not been attempted. +# +# There is a stray byte at 0xC in the radio download that is not present in the +# save file from the CE-21 software. This puts the first few bytes of channel 1 +# in the wrong location. A quick hack to fix the subsequent channels was to +# insert the #seekto dynamically, but this does nothing to fix reading of +# channel 1 (memory[0]). +MEM_FORMAT = """ +u8 unknown1[6]; +u8 prioritych; + +#seekto %d; +struct { + u8 empty:1, + txinhibit:1, + tot:1, + low_power:1, + bclo:1, + btlo:1, + skip:1, + pwrsave:1; + u8 unknown2:5, + narrow:1, + unknown2b:2; + u24 name; + u8 ctone; + u8 rtone; + u8 unknown3; + bbcd freq_rx[3]; + bbcd freq_tx[3]; +} memory[32]; + +char imgname[10]; +""" + +STEPS = [5.0, 6.25] +CHARSET = "".join([chr(x) for x in range(ord("0"), ord("9")+1)] + + [chr(x) for x in range(ord("A"), ord("Z")+1)]) + "<=>*+-\/_ " +TONES = list(chirp_common.TONES) +TONES.remove(165.5) +TONES.remove(171.3) +TONES.remove(177.3) +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("Low", watts=1.0)] + + +# @directory.register +class VX510Radio(yaesu_clone.YaesuCloneModeRadio): + """Vertex VX-510V""" + BAUD_RATE = 9600 + VENDOR = "Vertex Standard" + MODEL = "VX-510V" + + _model = "" + _memsize = 470 + _block_lengths = [10, 460] + _block_size = 8 + + def _checksums(self): + return [] + # These checksums don't pass, so the alg might be different than Yaesu. + # return [yaesu_clone.YaesuChecksum(0, self._memsize - 2)] + # return [yaesu_clone.YaesuChecksum(0, 10), + # yaesu_clone.YaesuChecksum(12, self._memsize - 1)] + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.can_odd_split = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = False + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone", + "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"] + rf.valid_modes = ["FM", "NFM"] + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.memory_bounds = (1, 32) + rf.valid_bands = [(13600000, 174000000)] + rf.valid_skips = ["", "S"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_name_length = 4 + rf.valid_characters = CHARSET + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT % 0xA, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def get_memory(self, number): + mem = chirp_common.Memory() + mem.number = number + + _mem = self._memobj.memory[number-1] + + mem.empty = _mem.empty + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq_rx) * 1000) + + for i in range(0, 4): + index = (_mem.name >> (i*6)) & 0x3F + mem.name += CHARSET[index] + + freq_tx = chirp_common.fix_rounded_step(int(_mem.freq_tx) * 1000) + if _mem.txinhibit: + mem.duplex = "off" + elif mem.freq == freq_tx: + mem.duplex = "" + mem.offset = 0 + elif 144000000 <= mem.freq < 148000000: + mem.duplex = "+" if freq_tx > mem.freq else "-" + mem.offset = abs(mem.freq - freq_tx) + else: + mem.duplex = "split" + mem.offset = freq_tx + + mem.mode = _mem.narrow and "NFM" or "FM" + mem.power = POWER_LEVELS[_mem.low_power] + + rtone = int(_mem.rtone) + ctone = int(_mem.ctone) + tmode_tx = tmode_rx = "" + + if rtone & 0x80: + tmode_tx = "DTCS" + mem.dtcs = chirp_common.DTCS_CODES[int(rtone) - 0x80] + elif rtone: + tmode_tx = "Tone" + mem.rtone = TONES[rtone - 1] + if not ctone: + # not used, but this is a better default than 88.5 + mem.ctone = TONES[rtone - 1] + + if ctone & 0x80: + tmode_rx = "DTCS" + mem.rx_dtcs = chirp_common.DTCS_CODES[int(ctone) - 0x80] + elif ctone: + tmode_rx = "Tone" + mem.ctone = TONES[ctone - 1] + + if tmode_tx == "Tone" and not tmode_rx: + mem.tmode = "Tone" + elif tmode_tx == tmode_rx and tmode_tx == "Tone" and \ + mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif tmode_tx == tmode_rx and tmode_tx == "DTCS" and \ + mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif tmode_rx or tmode_tx: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (tmode_tx, tmode_rx) + + mem.skip = _mem.skip and "S" or "" + + return mem + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + +# @directory.register +class VX510File(VX510Radio, chirp_common.FileBackedRadio): + """Vertex CE-21 File""" + VENDOR = "Vertex Standard" + MODEL = "CE-21 File" + + _model = "" + _memsize = 664 + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0, self._memsize - 1)] + + def process_mmap(self): + # CE-21 file is missing the 0xC byte, probably a checksum. + # It's not a YaesuChecksum. + self._memobj = bitwise.parse(MEM_FORMAT % 0x9, self._mmap) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize diff --git a/chirp/drivers/vx6.py b/chirp/drivers/vx6.py new file mode 100644 index 0000000..2693f56 --- /dev/null +++ b/chirp/drivers/vx6.py @@ -0,0 +1,875 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings +from chirp.settings import RadioSettingValueString, RadioSettingValueList +from chirp.settings import RadioSettingValueBoolean +from textwrap import dedent +import re + +# flags.{even|odd}_pskip: These are actually "preferential *scan* channels". +# Is that what they mean on other radios as well? + +# memory { +# step_changed: Channel step has been changed. Bit stays on even after +# you switch back to default step. Don't know why you would +# care +# half_deviation: 2.5 kHz deviation +# cpu_shifted: CPU freq has been shifted (to move a birdie out of channel) +# power: 0-3: ["L1", "L2", "L3", "Hi"] +# pager: Set if this is a paging memory +# tmodes: 0-7: ["", "Tone", "TSQL", "DTCS", "Rv Tn", "D Code", +# "T DCS", "D Tone"] +# Rv Tn: Reverse CTCSS - mutes receiver on tone +# The final 3 are for split: +# D Code: DCS Encode only +# T DCS: Encodes tone, decodes DCS code +# D Tone: Encodes DCS code, decodes tone +# } +MEM_FORMAT = """ +#seekto 0x010A; +struct { + u8 auto_power_off; + u8 arts_beep; + u8 bell; + u8 beep_level; + u8 arts_cwid_alpha[16]; + u8 unk1[2]; + u8 channel_counter_width; + u8 lcd_dimmer; + u8 last_dtmf; + u8 unk2; + u8 internet_code; + u8 last_internet_dtmf; + u8 unk3[3]; + u8 emergency; + u8 unk4[16]; + u8 lamp; + u8 lock; + u8 unk5; + u8 mic_gain; + u8 unk6[2]; + u8 on_timer; + u8 open_message_mode; + u8 open_message[6]; + u8 unk7; + u8 unk8:6, + pager_answer_back:1, + unk9:1; + u8 pager_rx_tone1; + u8 pager_rx_tone2; + u8 pager_tx_tone1; + u8 pager_tx_tone2; + u8 password[4]; + u8 ptt_delay; + u8 rf_squelch; + u8 rx_save; + u8 resume; + u8 unk10[5]; + u8 tx_timeout; + u8 wakeup; + u8 vfo_mode:1, + arts_cwid:1, + scan_lamp:1, + ts_speed:1, + unk11:1, + beep:1, + unk12:1, + dtmf_autodial:1; + u8 busy_led:1, + tone_search_mute:1, + int_autodial:1, + bclo:1, + edge_beep:1, + unk13:1, + dmr_wrt:1, + tx_saver:1; + u8 unk14:2, + smart_search:1, + unk15:3, + home_rev:1, + moni_tcall:1; + u8 unk16:3, + arts_interval:1, + unk17:3, + memory_method:1; + u8 unk18:2, + internet_mode:1, + wx_alert:1, + unk19:1, + att:1, + unk20:2; +} settings; + +#seekto 0x018A; +struct { + u16 in_use; +} bank_used[24]; + +#seekto 0x01D8; +u8 clock_shift; + +#seekto 0x0214; +u16 banksoff1; + +#seekto 0x0248; +u8 lastsetting1; + +#seekto 0x0294; +u16 banksoff2; + +#seekto 0x0248; +u8 lastsetting2; + +#seekto 0x02CA; +struct { + u8 memory[16]; +} dtmf[10]; + +#seekto 0x03CA; +struct { + u8 memory[8]; + u8 empty_ff[8]; +} internet_dtmf[64]; + +#seekto 0x097A; +struct { + u8 name[6]; +} bank_names[24]; + +#seekto 0x0C0A; +struct { + u16 channels[100]; +} banks[24]; + +#seekto 0x1ECA; +struct { + u8 even_pskip:1, + even_skip:1, + even_valid:1, + even_masked:1, + odd_pskip:1, + odd_skip:1, + odd_valid:1, + odd_masked:1; +} flags[500]; + +#seekto 0x21CA; +struct { + u8 unknown11:1, + step_changed:1, + half_deviation:1, + cpu_shifted:1, + unknown12:4; + u8 mode:2, + duplex:2, + tune_step:4; + bbcd freq[3]; + u8 power:2, + unknown2:2, + pager:1, + tmode:3; + u8 name[6]; + bbcd offset[3]; + u8 tone; + u8 dcs; + u8 unknown5; +} memory[999]; +""" + +DUPLEX = ["", "-", "+", "split"] +MODES = ["FM", "AM", "WFM", "FM"] # last is auto +TMODES = ["", "Tone", "TSQL", "DTCS"] +DTMFCHARSET = list("0123456789ABCD*#-") +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0, + 9.0, 200.0, 5.0] # last is auto, 9.0k and 200.0k are unadvertised + +CHARSET = ["%i" % int(x) for x in range(10)] + \ + [chr(x) for x in range(ord("A"), ord("Z")+1)] + \ + list(" +-/\x00[]__" + ("\x00" * 9) + "$%%\x00**.|=\\\x00@") + \ + list("\x00" * 100) + +PASS_CHARSET = list("0123456789ABCDEF") + +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("L3", watts=2.50), + chirp_common.PowerLevel("L2", watts=1.00), + chirp_common.PowerLevel("L1", watts=0.30)] +POWER_LEVELS_220 = [chirp_common.PowerLevel("Hi", watts=1.50), + chirp_common.PowerLevel("L3", watts=1.00), + chirp_common.PowerLevel("L2", watts=0.50), + chirp_common.PowerLevel("L1", watts=0.20)] + + +class VX6Bank(chirp_common.NamedBank): + """A VX6 Bank""" + def get_name(self): + _bank = self._model._radio._memobj.bank_names[self.index] + name = "" + for i in _bank.name: + if i == 0xFF: + break + name += CHARSET[i & 0x7F] + return name.rstrip() + + def set_name(self, name): + name = name.upper() + _bank = self._model._radio._memobj.bank_names[self.index] + _bank.name = [CHARSET.index(x) for x in name.ljust(6)[:6]] + + +class VX6BankModel(chirp_common.BankModel): + """A VX-6 bank model""" + + def get_num_mappings(self): + return len(self.get_mappings()) + + def get_mappings(self): + banks = self._radio._memobj.banks + bank_mappings = [] + for index, _bank in enumerate(banks): + bank = VX6Bank(self, "%i" % index, "b%i" % (index + 1)) + bank.index = index + bank_mappings.append(bank) + + return bank_mappings + + def _get_channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xFFFF: + return set() + + _members = self._radio._memobj.banks[bank.index] + return set([int(ch) + 1 for ch in _members.channels if ch != 0xFFFF]) + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.banks[bank.index] + if len(channels_in_bank) > len(_members.channels): + raise Exception("Too many entries in bank %d" % bank.index) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channels[index] = channel_number - 1 + empty = index + 1 + for index in range(empty, len(_members.channels)): + _members.channels[index] = 0xFFFF + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0x0000 # enable + + # also needed for unit to recognize any banks? + self._radio._memobj.banksoff1 = 0x0000 + self._radio._memobj.banksoff2 = 0x0000 + # TODO: turn back off (0xFFFF) when all banks are empty? + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._get_channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + if not channels_in_bank: + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0xFFFF # disable bank + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._get_channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._get_channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +@directory.register +class VX6Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu VX-6""" + BAUD_RATE = 19200 + VENDOR = "Yaesu" + MODEL = "VX-6" + + _model = "AH021" + _memsize = 32587 + _block_lengths = [10, 32577] + _block_size = 16 + + _APO = ("OFF", "30 min", "1 hour", "3 hour", "5 hour", "8 hour") + _ARTSBEEP = ("Off", "In Range", "Always") + _ARTS_INT = ("15 S", "25 S") + _BELL = ("OFF", "1", "3", "5", "8", "Continuous") + _BEEP_LEVEL = ["%i" % int(x) for x in range(1, 10)] + _CH_CNT = ("5 MHZ", "10 MHZ", "50 MHZ", "100 MHZ") + _DIM_LEVEL = ["%i" % int(x) for x in range(0, 13)] + _EMERGENCY = ("Beep", "Strobe", "Bp+Str", "Beam", "Bp+Bem", "CW", + "Bp+CW", "CWT") + _HOME_REV = ("HOME", "REV") + _INT_CD = ["%i" % int(x) for x in range(0, 10)] + \ + [chr(x) for x in range(ord("A"), ord("F")+1)] + _INT_MD = ("SRG: Sister Radio Group", "FRG: Friendly Radio Group") + _LAMP = ("Key", "Continuous", "Off") + _LOCK = ("Key", "Dial", "Key+Dial", "PTT", "Key+PTT", "Dial+PTT", "All") + _MAN_AUTO = ("Manual", "Auto") + _MEM_W_MD = ("Lower", "Next") + _MONI_TCALL = ("MONI", "T-CALL") + _NUM_1_9 = ["%i" % int(x) for x in range(1, 10)] + _NUM_0_9 = ["%i" % int(x) for x in range(10)] + _NUM_0_63 = ["%i" % int(x) for x in range(64)] + _NUM_1_50 = ["%i" % int(x) for x in range(1, 51)] + _ON_TIMER = ["OFF"] + \ + ["%02d:%02d" % (t / 60, t % 60) for t in range(10, 1450, 10)] + _OPEN_MSG = ("Off", "DC Voltage", "Message") + _PTT_DELAY = ("OFF", "20MS", "50MS", "100MS", "200MS") + _RF_SQL = ("OFF", "S1", "S2", "S3", "S4", "S5", + "S6", "S7", "S8", "S9", "S9+") + _RX_SAVE = ("OFF", "200 ms", "300 MS", "500 MS", "1 S", "2 S") + _RESUME = ("3 SEC", "5 SEC", "10 SEC", "BUSY", "HOLD") + _SMART_SEARCH = ("SINGLE", "CONT") + _TOT = ("OFF", "1MIN", "3MIN", "5MIN", "10MIN") + _TS_SPEED = ("FAST", "SLOW") + _VFO_MODE = ("ALL", "BAND") + _WAKEUP = ("OFF", "5S", "10S", "20S", "30S", "EAI") + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [F/W] key while turning the radio on + ("CLONE" will appear on the display). +4. After clicking OK, press the [BAND] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [F/W] key while turning the radio on + ("CLONE" will appear on the display). +4. Press the [V/M] key ("-WAIT-" will appear on the LCD).""")) + return rp + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0000, 0x7F49)] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = True + rf.has_bank_names = True + rf.has_dtcs_polarity = False + rf.valid_modes = ["FM", "WFM", "AM", "NFM"] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_duplexes = DUPLEX + rf.valid_tuning_steps = STEPS + rf.valid_power_levels = POWER_LEVELS + rf.memory_bounds = (1, 999) + rf.valid_bands = [(500000, 998990000)] + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 6 + rf.can_odd_split = True + rf.has_ctone = False + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + \ + repr(self._memobj.flags[(number-1)/2]) + + def get_memory(self, number): + _mem = self._memobj.memory[number-1] + _flg = self._memobj.flags[(number-1)/2] + + nibble = ((number-1) % 2) and "even" or "odd" + used = _flg["%s_masked" % nibble] + valid = _flg["%s_valid" % nibble] + pskip = _flg["%s_pskip" % nibble] + skip = _flg["%s_skip" % nibble] + + mem = chirp_common.Memory() + mem.number = number + + if not used: + mem.empty = True + if not valid: + mem.empty = True + mem.power = POWER_LEVELS[0] + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000) + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone & 0x3f] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.mode = MODES[_mem.mode] + if mem.mode == "FM" and _mem.half_deviation: + mem.mode = "NFM" + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs & 0x7f] + mem.tuning_step = STEPS[_mem.tune_step] + mem.skip = pskip and "P" or skip and "S" or "" + + if mem.freq > 220000000 and mem.freq < 225000000: + mem.power = POWER_LEVELS_220[3 - _mem.power] + else: + mem.power = POWER_LEVELS[3 - _mem.power] + + for i in _mem.name: + if i == 0xFF: + break + mem.name += CHARSET[i & 0x7F] + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + _flag = self._memobj.flags[(mem.number-1)/2] + + nibble = ((mem.number-1) % 2) and "even" or "odd" + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + + # initialize new channel to safe defaults + if not mem.empty and not valid: + _flag["%s_valid" % nibble] = True + _mem.unknown11 = 0 + _mem.step_changed = 0 + _mem.cpu_shifted = 0 + _mem.unknown12 = 0 + _mem.unknown2 = 0 + _mem.pager = 0 + _mem.unknown5 = 0 + + if mem.empty and valid and not used: + _flag["%s_valid" % nibble] = False + return + _flag["%s_masked" % nibble] = not mem.empty + + if mem.empty: + return + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 1000 + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + if mem.mode == "NFM": + _mem.mode = MODES.index("FM") + _mem.half_deviation = 1 + else: + _mem.mode = MODES.index(mem.mode) + _mem.half_deviation = 0 + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = STEPS.index(mem.tuning_step) + if mem.power: + _mem.power = 3 - POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + _flag["%s_pskip" % nibble] = mem.skip == "P" + _flag["%s_skip" % nibble] = mem.skip == "S" + + _mem.name = [0xFF] * 6 + for i in range(0, 6): + _mem.name[i] = CHARSET.index(mem.name.ljust(6)[i]) + + if mem.name.strip(): + _mem.name[0] |= 0x80 + + def get_bank_model(self): + return VX6BankModel(self) + + def _decode_chars(self, inarr): + outstr = "" + for i in inarr: + if i == 0xFF: + break + outstr += CHARSET[i & 0x7F] + return outstr.rstrip() + + def _encode_chars(self, instr, length=16): + outarr = [] + instr = str(instr) + for i in range(length): + if i < len(instr): + outarr.append(CHARSET.index(instr[i])) + else: + outarr.append(0xFF) + return outarr + + def _get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + arts = RadioSettingGroup("arts", "ARTS") + dtmf = RadioSettingGroup("dtmf", "DTMF") + wires = RadioSettingGroup("wires", "WIRES") + misc = RadioSettingGroup("misc", "Misc") + top = RadioSettings(basic, arts, dtmf, wires, misc) + + # BASIC + + val = RadioSettingValueList( + self._APO, self._APO[_settings.auto_power_off]) + rs = RadioSetting("auto_power_off", "Auto Power Off", val) + basic.append(rs) + + val = RadioSettingValueList( + self._BEEP_LEVEL, self._BEEP_LEVEL[_settings.beep_level]) + rs = RadioSetting("beep_level", "Beep Level", val) + basic.append(rs) + + val = RadioSettingValueList( + self._DIM_LEVEL, self._DIM_LEVEL[_settings.lcd_dimmer]) + rs = RadioSetting("lcd_dimmer", "Dimmer Level", val) + basic.append(rs) + + val = RadioSettingValueList( + self._LAMP, self._LAMP[_settings.lamp]) + rs = RadioSetting("lamp", "Keypad Lamp", val) + basic.append(rs) + + val = RadioSettingValueList( + self._LOCK, self._LOCK[_settings.lock]) + rs = RadioSetting("lock", "Lock", val) + basic.append(rs) + + val = RadioSettingValueList( + self._NUM_1_9, self._NUM_1_9[_settings.mic_gain]) + rs = RadioSetting("mic_gain", "Mic Gain", val) + basic.append(rs) + + val = RadioSettingValueList( + self._OPEN_MSG, self._OPEN_MSG[_settings.open_message_mode]) + rs = RadioSetting("open_message_mode", + "Open Message Mode", val) + basic.append(rs) + + val = RadioSettingValueString(0, 6, + self._decode_chars( + _settings.open_message)) + val.set_charset(CHARSET) + rs = RadioSetting("open_message", "Opening Message", val) + basic.append(rs) + + passstr = "" + for c in _settings.password: + if c < len(PASS_CHARSET): + passstr += PASS_CHARSET[c] + val = RadioSettingValueString(0, 4, passstr) + val.set_charset(PASS_CHARSET) + rs = RadioSetting("password", "Password", val) + basic.append(rs) + + val = RadioSettingValueList( + self._RESUME, self._RESUME[_settings.resume]) + rs = RadioSetting("resume", "Scan Resume", val) + basic.append(rs) + + val = RadioSettingValueList( + self._MONI_TCALL, self._MONI_TCALL[_settings.moni_tcall]) + rs = RadioSetting("moni_tcall", "MONI/T-CALL switch", val) + basic.append(rs) + + rs = RadioSetting("scan_lamp", "Scan Lamp", + RadioSettingValueBoolean(_settings.scan_lamp)) + basic.append(rs) + + rs = RadioSetting("beep", "Keypad Beep", + RadioSettingValueBoolean(_settings.beep)) + basic.append(rs) + + rs = RadioSetting("busy_led", "Busy LED", + RadioSettingValueBoolean(_settings.busy_led)) + basic.append(rs) + + rs = RadioSetting("bclo", "Busy Channel Lock-Out", + RadioSettingValueBoolean(_settings.bclo)) + basic.append(rs) + + rs = RadioSetting("wx_alert", "WX Alert", + RadioSettingValueBoolean(_settings.wx_alert)) + basic.append(rs) + + rs = RadioSetting("att", "Attenuator", + RadioSettingValueBoolean(_settings.att)) + basic.append(rs) + + # ARTS + + val = RadioSettingValueList( + self._ARTS_INT, self._ARTS_INT[_settings.arts_interval]) + rs = RadioSetting("arts_interval", "ARTS Interval", val) + arts.append(rs) + + val = RadioSettingValueList( + self._ARTSBEEP, self._ARTSBEEP[_settings.arts_beep]) + rs = RadioSetting("arts_beep", "ARTS Beep", val) + arts.append(rs) + + rs = RadioSetting("arts_cwid", "ARTS Send CWID", + RadioSettingValueBoolean(_settings.arts_cwid)) + arts.append(rs) + + val = RadioSettingValueString(0, 16, + self._decode_chars( + _settings.arts_cwid_alpha)) + val.set_charset(CHARSET) + rs = RadioSetting("arts_cwid_alpha", "ARTS CW ID", val) + arts.append(rs) + + # DTMF + + val = RadioSettingValueList( + self._MAN_AUTO, self._MAN_AUTO[_settings.dtmf_autodial]) + rs = RadioSetting("dtmf_autodial", "DTMF Autodial", val) + dtmf.append(rs) + + val = RadioSettingValueList( + self._NUM_0_9, self._NUM_0_9[_settings.last_dtmf]) + rs = RadioSetting("last_dtmf", "Last DTMF Memory Set", val) + dtmf.append(rs) + + for i in range(10): + name = "dtmf_" + str(i) + dtmfsetting = self._memobj.dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c < len(DTMFCHARSET): + dtmfstr += DTMFCHARSET[c] + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + rs = RadioSetting(name, name.upper(), dtmfentry) + dtmf.append(rs) + + # WIRES + + val = RadioSettingValueList( + self._INT_CD, self._INT_CD[_settings.internet_code]) + rs = RadioSetting("internet_code", "Internet Code", val) + wires.append(rs) + + val = RadioSettingValueList( + self._INT_MD, self._INT_MD[_settings.internet_mode]) + rs = RadioSetting("internet_mode", + "Internet Link Connection mode", val) + wires.append(rs) + + val = RadioSettingValueList( + self._MAN_AUTO, self._MAN_AUTO[_settings.int_autodial]) + rs = RadioSetting("int_autodial", "Internet Autodial", val) + wires.append(rs) + + val = RadioSettingValueList( + self._NUM_0_63, self._NUM_0_63[_settings.last_internet_dtmf]) + rs = RadioSetting("last_internet_dtmf", + "Last Internet DTMF Memory Set", val) + wires.append(rs) + + for i in range(64): + name = "wires_dtmf_" + str(i) + dtmfsetting = self._memobj.internet_dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c < len(DTMFCHARSET): + dtmfstr += DTMFCHARSET[c] + dtmfentry = RadioSettingValueString(0, 8, dtmfstr) + rs = RadioSetting(name, name.upper(), dtmfentry) + wires.append(rs) + + # MISC + + val = RadioSettingValueList( + self._BELL, self._BELL[_settings.bell]) + rs = RadioSetting("bell", "CTCSS/DCS Bell", val) + misc.append(rs) + + val = RadioSettingValueList( + self._CH_CNT, self._CH_CNT[_settings.channel_counter_width]) + rs = RadioSetting("channel_counter_width", + "Channel Counter Search Width", val) + misc.append(rs) + + val = RadioSettingValueList( + self._EMERGENCY, self._EMERGENCY[_settings.emergency]) + rs = RadioSetting("emergency", "Emergency alarm", val) + misc.append(rs) + + val = RadioSettingValueList( + self._ON_TIMER, self._ON_TIMER[_settings.on_timer]) + rs = RadioSetting("on_timer", "On Timer", val) + misc.append(rs) + + rs = RadioSetting("pager_answer_back", "Pager Answer Back", + RadioSettingValueBoolean( + _settings.pager_answer_back)) + misc.append(rs) + + val = RadioSettingValueList( + self._NUM_1_50, self._NUM_1_50[_settings.pager_rx_tone1]) + rs = RadioSetting("pager_rx_tone1", "Pager RX Tone 1", val) + misc.append(rs) + + val = RadioSettingValueList( + self._NUM_1_50, self._NUM_1_50[_settings.pager_rx_tone2]) + rs = RadioSetting("pager_rx_tone2", "Pager RX Tone 2", val) + misc.append(rs) + + val = RadioSettingValueList( + self._NUM_1_50, self._NUM_1_50[_settings.pager_tx_tone1]) + rs = RadioSetting("pager_tx_tone1", "Pager TX Tone 1", val) + misc.append(rs) + + val = RadioSettingValueList( + self._NUM_1_50, self._NUM_1_50[_settings.pager_tx_tone2]) + rs = RadioSetting("pager_tx_tone2", "Pager TX Tone 2", val) + misc.append(rs) + + val = RadioSettingValueList( + self._PTT_DELAY, self._PTT_DELAY[_settings.ptt_delay]) + rs = RadioSetting("ptt_delay", "PTT Delay", val) + misc.append(rs) + + val = RadioSettingValueList( + self._RF_SQL, self._RF_SQL[_settings.rf_squelch]) + rs = RadioSetting("rf_squelch", "RF Squelch", val) + misc.append(rs) + + val = RadioSettingValueList( + self._RX_SAVE, self._RX_SAVE[_settings.rx_save]) + rs = RadioSetting("rx_save", "RX Save", val) + misc.append(rs) + + val = RadioSettingValueList( + self._TOT, self._TOT[_settings.tx_timeout]) + rs = RadioSetting("tx_timeout", "TOT", val) + misc.append(rs) + + val = RadioSettingValueList( + self._WAKEUP, self._WAKEUP[_settings.wakeup]) + rs = RadioSetting("wakeup", "Wakeup", val) + misc.append(rs) + + rs = RadioSetting("edge_beep", "Band-Edge Beep", + RadioSettingValueBoolean(_settings.edge_beep)) + misc.append(rs) + + val = RadioSettingValueList( + self._VFO_MODE, self._VFO_MODE[_settings.vfo_mode]) + rs = RadioSetting("vfo_mode", "VFO Band Edge Limiting", val) + misc.append(rs) + + rs = RadioSetting("tone_search_mute", "Tone Search Mute", + RadioSettingValueBoolean(_settings.tone_search_mute)) + misc.append(rs) + + val = RadioSettingValueList( + self._TS_SPEED, self._TS_SPEED[_settings.ts_speed]) + rs = RadioSetting("ts_speed", "Tone Search Speed", val) + misc.append(rs) + + rs = RadioSetting("dmr_wrt", "Direct Memory Recall Overwrite", + RadioSettingValueBoolean(_settings.dmr_wrt)) + misc.append(rs) + + rs = RadioSetting("tx_saver", "TX Battery Saver", + RadioSettingValueBoolean(_settings.tx_saver)) + misc.append(rs) + + val = RadioSettingValueList( + self._SMART_SEARCH, self._SMART_SEARCH[_settings.smart_search]) + rs = RadioSetting("smart_search", "Smart Search", val) + misc.append(rs) + + val = RadioSettingValueList( + self._HOME_REV, self._HOME_REV[_settings.home_rev]) + rs = RadioSetting("home_rev", "HM/RV(EMG)R/H key", val) + misc.append(rs) + + val = RadioSettingValueList( + self._MEM_W_MD, self._MEM_W_MD[_settings.memory_method]) + rs = RadioSetting("memory_method", "Memory Write Method", val) + misc.append(rs) + + return top + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + pring(traceback.format_exc()) + return None + + def set_settings(self, uisettings): + for element in uisettings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + _settings = self._memobj.settings + if re.match('internet_dtmf_\d', setting): + # set dtmf fields + dtmfstr = str(element.value).strip() + newval = [] + for i in range(0, 8): + if i < len(dtmfstr): + newval.append(DTMFCHARSET.index(dtmfstr[i])) + else: + newval.append(0xFF) + idx = int(setting[-1:]) + _settings = self._memobj.internet_dtmf[idx] + _settings.memory = newval + continue + elif re.match('dtmf_\d', setting): + # set dtmf fields + dtmfstr = str(element.value).strip() + newval = [] + for i in range(0, 16): + if i < len(dtmfstr): + newval.append(DTMFCHARSET.index(dtmfstr[i])) + else: + newval.append(0xFF) + idx = int(setting[-1:]) + _settings = self._memobj.dtmf[idx] + _settings.memory = newval + continue + oldval = getattr(_settings, setting) + newval = element.value + if setting == "arts_cwid_alpha": + newval = self._encode_chars(newval) + elif setting == "open_message": + newval = self._encode_chars(newval, 6) + elif setting == "password": + newval = self._encode_chars(newval, 4) + setattr(_settings, setting, newval) + except (Exception, e): + raise diff --git a/chirp/drivers/vx7.py b/chirp/drivers/vx7.py new file mode 100644 index 0000000..1dedfaf --- /dev/null +++ b/chirp/drivers/vx7.py @@ -0,0 +1,362 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from textwrap import dedent +import logging + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x0611; +u8 checksum1; + +#seekto 0x0691; +u8 checksum2; + +#seekto 0x0742; +struct { + u16 in_use; +} bank_used[9]; + +#seekto 0x0EA2; +struct { + u16 members[48]; +} bank_members[9]; + +#seekto 0x3F52; +u8 checksum3; + +#seekto 0x1202; +struct { + u8 even_pskip:1, + even_skip:1, + even_valid:1, + even_masked:1, + odd_pskip:1, + odd_skip:1, + odd_valid:1, + odd_masked:1; +} flags[225]; + +#seekto 0x1322; +struct { + u8 unknown1; + u8 power:2, + duplex:2, + tune_step:4; + bbcd freq[3]; + u8 zeros1:2, + ones:2, + zeros2:2, + mode:2; + u8 name[8]; + u8 zero; + bbcd offset[3]; + u8 zeros3:2, + tone:6; + u8 zeros4:1, + dcs:7; + u8 zeros5:5, + is_split_tone:1, + tmode:2; + u8 charset; +} memory[450]; +""" + +DUPLEX = ["", "-", "+", "split"] +MODES = ["FM", "AM", "WFM", "Auto"] +TMODES = ["", "Tone", "TSQL", "DTCS", "Cross"] +CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone"] +STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0, 9.0] + +CHARSET = ["%i" % int(x) for x in range(0, 10)] + \ + [" "] + \ + [chr(x) for x in range(ord("A"), ord("Z")+1)] + \ + [chr(x) for x in range(ord("a"), ord("z")+1)] + \ + list(".,:;!\"#$%&'()*+-.=<>?@[?]^_\\{|}") + \ + list("\x00" * 100) + +POWER_LEVELS = [chirp_common.PowerLevel("L1", watts=0.05), + chirp_common.PowerLevel("L2", watts=1.00), + chirp_common.PowerLevel("L3", watts=2.50), + chirp_common.PowerLevel("Hi", watts=5.00) + ] +POWER_LEVELS_220 = [chirp_common.PowerLevel("L1", watts=0.05), + chirp_common.PowerLevel("L2", watts=0.30)] + + +def _is220(freq): + return freq >= 222000000 and freq <= 225000000 + + +class VX7BankModel(chirp_common.BankModel): + """A VX-7 Bank model""" + def get_num_mappings(self): + return 9 + + def get_mappings(self): + banks = [] + for i in range(0, self.get_num_mappings()): + bank = chirp_common.Bank(self, "%i" % (i+1), "MG%i" % (i+1)) + bank.index = i + banks.append(bank) + return banks + + def add_memory_to_mapping(self, memory, bank): + _members = self._radio._memobj.bank_members[bank.index] + _bank_used = self._radio._memobj.bank_used[bank.index] + for i in range(0, 48): + if _members.members[i] == 0xFFFF: + _members.members[i] = memory.number - 1 + _bank_used.in_use = 0x0000 + break + + def remove_memory_from_mapping(self, memory, bank): + _members = self._radio._memobj.bank_members[bank.index].members + _bank_used = self._radio._memobj.bank_used[bank.index] + + found = False + remaining_members = 0 + for i in range(0, len(_members)): + if _members[i] == (memory.number - 1): + _members[i] = 0xFFFF + found = True + elif _members[i] != 0xFFFF: + remaining_members += 1 + + if not found: + raise Exception("Memory {num} not in " + + "bank {bank}".format(num=memory.number, + bank=bank)) + if not remaining_members: + _bank_used.in_use = 0xFFFF + + def get_mapping_memories(self, bank): + memories = [] + + _members = self._radio._memobj.bank_members[bank.index].members + _bank_used = self._radio._memobj.bank_used[bank.index] + + if _bank_used.in_use == 0xFFFF: + return memories + + for number in _members: + if number == 0xFFFF: + continue + memories.append(self._radio.get_memory(number+1)) + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in [x.number for x in + self.get_mapping_memories(bank)]: + banks.append(bank) + return banks + + +def _wipe_memory(mem): + mem.set_raw("\x00" * (mem.size() // 8)) + mem.unknown1 = 0x05 + mem.ones = 0x03 + + +@directory.register +class VX7Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu VX-7""" + BAUD_RATE = 19200 + VENDOR = "Yaesu" + MODEL = "VX-7" + + _model = "" + _memsize = 16211 + _block_lengths = [10, 8, 16193] + _block_size = 8 + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [MON-F] key while turning the radio on + ("CLONE" will appear on the display). +4. After clicking OK, press the [BAND] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to MIC/SP jack. +3. Press and hold in the [MON-F] key while turning the radio on + ("CLONE" will appear on the display). +4. Press the [V/M] key ("CLONE WAIT" will appear on the LCD).""")) + return rp + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x0592, 0x0610), + yaesu_clone.YaesuChecksum(0x0612, 0x0690), + yaesu_clone.YaesuChecksum(0x0000, 0x3F51), + ] + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = True + rf.has_dtcs_polarity = False + rf.valid_modes = list(set(MODES)) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(500000, 999000000)] + rf.valid_skips = ["", "S", "P"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 8 + rf.memory_bounds = (1, 450) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_cross = True + rf.valid_cross_modes = list(CROSS_MODES) + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def get_memory(self, number): + _mem = self._memobj.memory[number-1] + _flag = self._memobj.flags[(number-1)/2] + + nibble = ((number-1) % 2) and "even" or "odd" + used = _flag["%s_masked" % nibble] + valid = _flag["%s_valid" % nibble] + pskip = _flag["%s_pskip" % nibble] + skip = _flag["%s_skip" % nibble] + + mem = chirp_common.Memory() + mem.number = number + if not used: + mem.empty = True + if not valid: + mem.empty = True + mem.power = POWER_LEVELS[0] + return mem + + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 1000 + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + if not _mem.is_split_tone: + mem.tmode = TMODES[_mem.tmode] + mem.cross_mode = CROSS_MODES[0] + else: + mem.tmode = "Cross" + mem.cross_mode = CROSS_MODES[int(_mem.tmode)] + mem.duplex = DUPLEX[_mem.duplex] + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.tuning_step = STEPS[_mem.tune_step] + mem.skip = pskip and "P" or skip and "S" or "" + + if _is220(mem.freq): + levels = POWER_LEVELS_220 + else: + levels = POWER_LEVELS + try: + mem.power = levels[_mem.power] + except IndexError: + LOG.error("Radio reported invalid power level %s (in %s)" % + (_mem.power, levels)) + mem.power = levels[0] + + for i in _mem.name: + if i == "\xFF": + break + mem.name += CHARSET[i] + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + _flag = self._memobj.flags[(mem.number-1)/2] + + nibble = ((mem.number-1) % 2) and "even" or "odd" + + valid = _flag["%s_valid" % nibble] + used = _flag["%s_masked" % nibble] + + if not mem.empty and not valid: + _wipe_memory(_mem) + self._wipe_memory_banks(mem) + + if mem.empty and valid and not used: + _flag["%s_valid" % nibble] = False + return + _flag["%s_masked" % nibble] = not mem.empty + + if mem.empty: + return + + _flag["%s_valid" % nibble] = True + + _mem.freq = mem.freq / 1000 + _mem.offset = mem.offset / 1000 + _mem.tone = chirp_common.TONES.index(mem.rtone) + if mem.tmode != "Cross": + _mem.is_split_tone = 0 + _mem.tmode = TMODES.index(mem.tmode) + else: + _mem.is_split_tone = 1 + _mem.tmode = CROSS_MODES.index(mem.cross_mode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.mode = MODES.index(mem.mode) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = STEPS.index(mem.tuning_step) + + if mem.power: + if _is220(mem.freq): + levels = [str(l) for l in POWER_LEVELS_220] + _mem.power = levels.index(str(mem.power)) + else: + _mem.power = POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + _flag["%s_pskip" % nibble] = mem.skip == "P" + _flag["%s_skip" % nibble] = mem.skip == "S" + + for i in range(0, 8): + _mem.name[i] = CHARSET.index(mem.name.ljust(8)[i]) + + def validate_memory(self, mem): + msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem) + + if _is220(mem.freq): + if str(mem.power) not in [str(l) for l in POWER_LEVELS_220]: + msgs.append(chirp_common.ValidationError( + "Power level %s not supported on 220MHz band" % + mem.power)) + + return msgs + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def get_bank_model(self): + return VX7BankModel(self) diff --git a/chirp/drivers/vx8.py b/chirp/drivers/vx8.py new file mode 100644 index 0000000..550d1ca --- /dev/null +++ b/chirp/drivers/vx8.py @@ -0,0 +1,1675 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import logging + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, directory, bitwise +from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings +from chirp.settings import RadioSettingValueInteger, RadioSettingValueString +from chirp.settings import RadioSettingValueList, RadioSettingValueBoolean +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +#seekto 0x047f; +struct { + u8 flag; + u16 unknown; + struct { + u8 padded_yaesu[16]; + } message; +} opening_message; + +#seekto 0x049a; +struct { + u8 vfo_a; + u8 vfo_b; +} squelch; + +#seekto 0x04bf; +struct { + u8 beep; +} beep_select; + +#seekto 0x04cc; +struct { + u8 lcd_dimmer; + u8 dtmf_delay; + u8 unknown0[3]; + u8 unknown1:4 + lcd_contrast:4; + u8 lamp; + u8 unknown2[7]; + u8 scan_restart; + u8 unknown3; + u8 scan_resume; + u8 unknown4[6]; + u8 tot; + u8 unknown5[3]; + u8 unknown6:2, + scan_lamp:1, + unknown7:2, + dtmf_speed:1, + unknown8:1, + dtmf_mode:1; + u8 busy_led:1, + unknown9:7; + u8 unknown10[2]; + u8 vol_mode:1, + unknown11:7; +} scan_settings; + +#seekto 0x54a; +struct { + u16 in_use; +} bank_used[24]; + +#seekto 0x064a; +struct { + u8 unknown0[4]; + u8 frequency_band; + u8 unknown1:6, + manual_or_mr:2; + u8 unknown2:7, + mr_banks:1; + u8 unknown3; + u16 mr_index; + u16 bank_index; + u16 bank_enable; + u8 unknown4[5]; + u8 unknown5:6, + power:2; + u8 unknown6:4, + tune_step:4; + u8 unknown7:6, + duplex:2; + u8 unknown8:6, + tone_mode:2; + u8 unknown9:2, + tone:6; + u8 unknown10; + u8 unknown11:6, + mode:2; + bbcd freq0[4]; + bbcd offset_freq[4]; + u8 unknown12[2]; + char label[16]; + u8 unknown13[6]; + bbcd band_lower[4]; + bbcd band_upper[4]; + bbcd rx_freq[4]; + u8 unknown14[22]; + bbcd freq1[4]; + u8 unknown15[11]; + u8 unknown16:3, + volume:5; + u8 unknown17[18]; + u8 active_menu_item; + u8 checksum; +} vfo_info[6]; + +#seekto 0x094a; +struct { + u8 memory[16]; +} dtmf[10]; + +#seekto 0x135A; +struct { + u8 unknown[2]; + u8 name[16]; +} bank_info[24]; + +#seekto 0x198a; +struct { + u16 channel[100]; +} bank_members[24]; + +#seekto 0x2C4A; +struct { + u8 nosubvfo:1, + unknown:3, + pskip:1, + skip:1, + used:1, + valid:1; +} flag[900]; + +#seekto 0x328A; +struct { + u8 unknown1a:2, + half_deviation:1, + unknown1b:5; + u8 mode:2, + duplex:2, + tune_step:4; + bbcd freq[3]; + u8 power:2, + unknown2:4, + tone_mode:2; + u8 charsetbits[2]; + char label[16]; + bbcd offset[3]; + u8 unknown5:2, + tone:6; + u8 unknown6:1, + dcs:7; + u8 pr_frequency; + u8 unknown7; + u8 unknown8a:3, + unknown8b:1, + rx_mode_auto:1, + unknown8c:3; +} memory[900]; + +#seekto 0xC0CA; +struct { + u8 unknown0:6, + rx_baud:2; + u8 unknown1:4, + tx_delay:4; + u8 custom_symbol; + u8 unknown2; + struct { + char callsign[6]; + u8 ssid; + } my_callsign; + u8 unknown3:4, + selected_position_comment:4; + u8 unknown4; + u8 set_time_manually:1, + tx_interval_beacon:1, + ring_beacon:1, + ring_msg:1, + aprs_mute:1, + unknown6:1, + tx_smartbeacon:1, + af_dual:1; + u8 unknown7:1, + aprs_units_wind_mph:1, + aprs_units_rain_inch:1, + aprs_units_temperature_f:1 + aprs_units_altitude_ft:1, + unknown8:1, + aprs_units_distance_m:1, + aprs_units_position_mmss:1; + u8 unknown9:6, + aprs_units_speed:2; + u8 unknown11:1, + filter_other:1, + filter_status:1, + filter_item:1, + filter_object:1, + filter_weather:1, + filter_position:1, + filter_mic_e:1; + u8 unknown12:2, + timezone:6; + u8 unknown13:4, + beacon_interval:4; + u8 unknown14; + u8 unknown15:7, + latitude_sign:1; + u8 latitude_degree; + u8 latitude_minute; + u8 latitude_second; + u8 unknown16:7, + longitude_sign:1; + u8 longitude_degree; + u8 longitude_minute; + u8 longitude_second; + u8 unknown17:4, + selected_position:4; + u8 unknown18:5, + selected_beacon_status_txt:3; + u8 unknown19:6, + gps_units_altitude_ft:1, + gps_units_position_sss:1; + u8 unknown20:6, + gps_units_speed:2; + u8 unknown21[4]; + struct { + struct { + char callsign[6]; + u8 ssid; + } entry[8]; + } digi_path_7; + u8 unknown22[2]; +} aprs; + +#seekto 0x%04X; +struct { + char padded_string[16]; +} aprs_msg_macro[%d]; + +#seekto 0x%04X; +struct { + u8 unknown23:5, + selected_msg_group:3; + u8 unknown24; + struct { + char padded_string[9]; + } msg_group[8]; + u8 unknown25[4]; + u8 active_smartbeaconing; + struct { + u8 low_speed_mph; + u8 high_speed_mph; + u8 slow_rate_min; + u8 fast_rate_sec; + u8 turn_angle; + u8 turn_slop; + u8 turn_time_sec; + } smartbeaconing_profile[3]; + u8 unknown26:2, + flash_msg:6; + u8 unknown27:2, + flash_grp:6; + u8 unknown28:2, + flash_bln:6; + u8 selected_digi_path; + struct { + struct { + char callsign[6]; + u8 ssid; + } entry[2]; + } digi_path_3_6[4]; + u8 unknown30:6, + selected_my_symbol:2; + u8 unknown31[3]; + u8 unknown32:2, + vibrate_msg:6; + u8 unknown33:2, + vibrate_grp:6; + u8 unknown34:2, + vibrate_bln:6; +} aprs2; + +#seekto 0x%04X; +struct { + bbcd date[3]; + u8 unknown1; + bbcd time[2]; + u8 sequence; + u8 unknown2; + u8 sender_callsign[7]; + u8 data_type; + u8 yeasu_data_type; + u8 unknown3; + u8 unknown4:1, + callsign_is_ascii:1, + unknown5:6; + u8 unknown6; + u16 pkt_len; + u16 in_use; +} aprs_beacon_meta[%d]; + +#seekto 0x%04X; +struct { + u8 dst_callsign[6]; + u8 dst_callsign_ssid; + u8 src_callsign[6]; + u8 src_callsign_ssid; + u8 path_and_body[%d]; +} aprs_beacon_pkt[%d]; + +#seekto 0xf92a; +struct { + char padded_string[60]; +} aprs_beacon_status_txt[5]; + +#seekto 0xFECA; +u8 checksum; +""" + +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+", "split"] +MODES = ["FM", "AM", "WFM", "NFM"] +STEPS = list(chirp_common.TUNING_STEPS) +STEPS.remove(30.0) +STEPS.append(100.0) +STEPS.insert(2, 8.33) # Index 2 is 8.33kHz airband step +SKIPS = ["", "S", "P"] +VX8_DTMF_CHARS = list("0123456789ABCD*#-") + +CHARSET = ["%i" % int(x) for x in range(0, 10)] + \ + [chr(x) for x in range(ord("A"), ord("Z")+1)] + \ + [" "] + \ + [chr(x) for x in range(ord("a"), ord("z")+1)] + \ + list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100) + +POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00), + chirp_common.PowerLevel("L3", watts=2.50), + chirp_common.PowerLevel("L2", watts=1.00), + chirp_common.PowerLevel("L1", watts=0.05)] + + +class VX8Bank(chirp_common.NamedBank): + """A VX-8 bank""" + + def get_name(self): + _bank = self._model._radio._memobj.bank_info[self.index] + _bank_used = self._model._radio._memobj.bank_used[self.index] + + name = "" + for i in _bank.name: + if i == 0xFF: + break + name += CHARSET[i & 0x7F] + return name.rstrip() + + def set_name(self, name): + _bank = self._model._radio._memobj.bank_info[self.index] + _bank.name = [CHARSET.index(x) for x in name.ljust(16)[:16]] + + +class VX8BankModel(chirp_common.BankModel): + """A VX-8 bank model""" + def __init__(self, radio, name='Banks'): + super(VX8BankModel, self).__init__(radio, name) + + _banks = self._radio._memobj.bank_info + self._bank_mappings = [] + for index, _bank in enumerate(_banks): + bank = VX8Bank(self, "%i" % index, "BANK-%i" % index) + bank.index = index + self._bank_mappings.append(bank) + + def get_num_mappings(self): + return len(self._bank_mappings) + + def get_mappings(self): + return self._bank_mappings + + def _channel_numbers_in_bank(self, bank): + _bank_used = self._radio._memobj.bank_used[bank.index] + if _bank_used.in_use == 0xFFFF: + return set() + + _members = self._radio._memobj.bank_members[bank.index] + return set([int(ch) + 1 for ch in _members.channel if ch != 0xFFFF]) + + def update_vfo(self): + chosen_bank = [None, None] + chosen_mr = [None, None] + + flags = self._radio._memobj.flag + + # Find a suitable bank and MR for VFO A and B. + for bank in self.get_mappings(): + for channel in self._channel_numbers_in_bank(bank): + chosen_bank[0] = bank.index + chosen_mr[0] = channel + if not flags[channel].nosubvfo: + chosen_bank[1] = bank.index + chosen_mr[1] = channel + break + if chosen_bank[1]: + break + + for vfo_index in (0, 1): + # 3 VFO info structs are stored as 3 pairs of (master, backup) + vfo = self._radio._memobj.vfo_info[vfo_index * 2] + vfo_bak = self._radio._memobj.vfo_info[(vfo_index * 2) + 1] + + if vfo.checksum != vfo_bak.checksum: + LOG.warn("VFO settings are inconsistent with backup") + else: + if ((chosen_bank[vfo_index] is None) and + (vfo.bank_index != 0xFFFF)): + LOG.info("Disabling banks for VFO %d" % vfo_index) + vfo.bank_index = 0xFFFF + vfo.mr_index = 0xFFFF + vfo.bank_enable = 0xFFFF + elif ((chosen_bank[vfo_index] is not None) and + (vfo.bank_index == 0xFFFF)): + LOG.debug("Enabling banks for VFO %d" % vfo_index) + vfo.bank_index = chosen_bank[vfo_index] + vfo.mr_index = chosen_mr[vfo_index] + vfo.bank_enable = 0x0000 + vfo_bak.bank_index = vfo.bank_index + vfo_bak.mr_index = vfo.mr_index + vfo_bak.bank_enable = vfo.bank_enable + + def _update_bank_with_channel_numbers(self, bank, channels_in_bank): + _members = self._radio._memobj.bank_members[bank.index] + if len(channels_in_bank) > len(_members.channel): + raise Exception("Too many entries in bank %d" % bank.index) + + empty = 0 + for index, channel_number in enumerate(sorted(channels_in_bank)): + _members.channel[index] = channel_number - 1 + empty = index + 1 + for index in range(empty, len(_members.channel)): + _members.channel[index] = 0xFFFF + + def add_memory_to_mapping(self, memory, bank): + channels_in_bank = self._channel_numbers_in_bank(bank) + channels_in_bank.add(memory.number) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0x06 + + self.update_vfo() + + def remove_memory_from_mapping(self, memory, bank): + channels_in_bank = self._channel_numbers_in_bank(bank) + try: + channels_in_bank.remove(memory.number) + except KeyError: + raise Exception("Memory %i is not in bank %s. Cannot remove" % + (memory.number, bank)) + self._update_bank_with_channel_numbers(bank, channels_in_bank) + + if not channels_in_bank: + _bank_used = self._radio._memobj.bank_used[bank.index] + _bank_used.in_use = 0xFFFF + + self.update_vfo() + + def get_mapping_memories(self, bank): + memories = [] + for channel in self._channel_numbers_in_bank(bank): + memories.append(self._radio.get_memory(channel)) + + return memories + + def get_memory_mappings(self, memory): + banks = [] + for bank in self.get_mappings(): + if memory.number in self._channel_numbers_in_bank(bank): + banks.append(bank) + + return banks + + +def _wipe_memory(mem): + mem.set_raw("\x00" * (mem.size() // 8)) + + +@directory.register +class VX8Radio(yaesu_clone.YaesuCloneModeRadio): + """Yaesu VX-8""" + BAUD_RATE = 38400 + VENDOR = "Yaesu" + MODEL = "VX-8R" + + _model = "AH029" + _memsize = 65227 + _block_lengths = [10, 65217] + _block_size = 32 + _mem_params = (0xC128, # APRS message macros + 5, # Number of message macros + 0xC178, # APRS2 + 0xC24A, # APRS beacon metadata address. + 40, # Number of beacons stored. + 0xC60A, # APRS beacon content address. + 194, # Length of beacon data stored. + 40) # Number of beacons stored. + _has_vibrate = False + _has_af_dual = True + + _SG_RE = re.compile(r"(?P[-+NESW]?)(?P[\d]+)[\s\.,]*" + "(?P[\d]*)[\s\']*(?P[\d]*)") + + _RX_BAUD = ("off", "1200 baud", "9600 baud") + _TX_DELAY = ("100ms", "200ms", "300ms", + "400ms", "500ms", "750ms", "1000ms") + _WIND_UNITS = ("m/s", "mph") + _RAIN_UNITS = ("mm", "inch") + _TEMP_UNITS = ("C", "F") + _ALT_UNITS = ("m", "ft") + _DIST_UNITS = ("km", "mile") + _POS_UNITS = ("dd.mmmm'", "dd mm'ss\"") + _SPEED_UNITS = ("km/h", "knot", "mph") + _TIME_SOURCE = ("manual", "GPS") + _TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30", + "-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30", + "-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30", + "-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30", + "-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30", + "+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30", + "+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30", + "+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30", + "+11:00", "+11:30") + _BEACON_TYPE = ("Off", "Interval") + _BEACON_INT = ("15s", "30s", "1m", "2m", "3m", "5m", "10m", "15m", + "30m") + _DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4", + "Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8") + _MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2", + "Message Group 3", "Message Group 4", + "Message Group 5", "Message Group 6", + "Message Group 7", "Message Group 8") + _POSITIONS = ("GPS", "Manual Latitude/Longitude", + "Manual Latitude/Longitude", "P1", "P2", "P3", "P4", + "P5", "P6", "P7", "P8", "P9", "P10") + _FLASH = ("OFF", "ON") + _BEEP_SELECT = ("Off", "Key+Scan", "Key") + _SQUELCH = ["%d" % x for x in range(0, 16)] + _VOLUME = ["%d" % x for x in range(0, 33)] + _OPENING_MESSAGE = ("Off", "DC", "Message", "Normal") + _SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \ + ["Busy", "Hold"] + _SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \ + ["%.1fs" % (0.5 * x) for x in range(2, 21)] + _LAMP_KEY = ["Key %d sec" % x for x in range(2, 11)] + \ + ["Continuous", "OFF"] + _LCD_CONTRAST = ["Level %d" % x for x in range(1, 33)] + _LCD_DIMMER = ["Level %d" % x for x in range(1, 5)] + _TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)] + _OFF_ON = ("Off", "On") + _VOL_MODE = ("Normal", "Auto Back") + _DTMF_MODE = ("Manual", "Auto") + _DTMF_SPEED = ("50ms", "100ms") + _DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms") + _MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected") + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ +1. Turn radio off. +2. Connect cable to DATA jack. +3. Press and hold in the [FW] key while turning the radio on + ("CLONE" will appear on the display). +4. After clicking OK, press the [BAND] key to send image.""")) + rp.pre_upload = _(dedent("""\ +1. Turn radio off. +2. Connect cable to DATA jack. +3. Press and hold in the [FW] key while turning the radio on + ("CLONE" will appear on the display). +4. Press the [MODE] key ("-WAIT-" will appear on the LCD).""")) + return rp + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_dtcs_polarity = False + rf.valid_modes = list(MODES) + rf.valid_tmodes = list(TMODES) + rf.valid_duplexes = list(DUPLEX) + rf.valid_tuning_steps = list(STEPS) + rf.valid_bands = [(500000, 999900000)] + rf.valid_skips = SKIPS + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = "".join(CHARSET) + rf.valid_name_length = 16 + rf.memory_bounds = (1, 900) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_bank_names = True + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number-1]) + + def _checksums(self): + return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8), + yaesu_clone.YaesuChecksum(0x06CA, 0x0748), + yaesu_clone.YaesuChecksum(0x074A, 0x07C8), + yaesu_clone.YaesuChecksum(0x07CA, 0x0848), + yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)] + + @staticmethod + def _add_ff_pad(val, length): + return val.ljust(length, "\xFF")[:length] + + @classmethod + def _strip_ff_pads(cls, messages): + result = [] + for msg_text in messages: + result.append(str(msg_text).rstrip("\xFF")) + return result + + def get_memory(self, number): + flag = self._memobj.flag[number-1] + _mem = self._memobj.memory[number-1] + + mem = chirp_common.Memory() + mem.number = number + if not flag.used: + mem.empty = True + if not flag.valid: + mem.empty = True + return mem + mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000) + mem.offset = int(_mem.offset) * 1000 + mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone] + mem.tmode = TMODES[_mem.tone_mode] + mem.duplex = DUPLEX[_mem.duplex] + if mem.duplex == "split": + mem.offset = chirp_common.fix_rounded_step(mem.offset) + if _mem.mode == "FM" and _mem.half_deviation == 1: + mem.mode = "NFM" + else: + mem.mode = MODES[_mem.mode] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + mem.tuning_step = STEPS[_mem.tune_step] + mem.power = POWER_LEVELS[3 - _mem.power] + mem.skip = flag.pskip and "P" or flag.skip and "S" or "" + + charset = ''.join(CHARSET).ljust(256, '.') + mem.name = str(_mem.label).rstrip("\xFF").translate(charset) + + return mem + + def _debank(self, mem): + bm = self.get_bank_model() + for bank in bm.get_memory_mappings(mem): + bm.remove_memory_from_mapping(mem, bank) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number-1] + flag = self._memobj.flag[mem.number-1] + + self._debank(mem) + + if not mem.empty and not flag.valid: + _wipe_memory(_mem) + + if mem.empty and flag.valid and not flag.used: + flag.valid = False + return + flag.used = not mem.empty + flag.valid = flag.used + + if mem.empty: + return + + if mem.freq < 30000000 or \ + (mem.freq > 88000000 and mem.freq < 108000000) or \ + mem.freq > 580000000: + flag.nosubvfo = True # Masked from VFO B + else: + flag.nosubvfo = False # Available in both VFOs + + _mem.freq = int(mem.freq / 1000) + _mem.offset = int(mem.offset / 1000) + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.tone_mode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + if mem.mode == "NFM": + _mem.mode = 0 # Yaesu's NFM, i.e. regular FM + _mem.half_deviation = 1 # but half bandwidth + else: + _mem.mode = MODES.index(mem.mode) + _mem.half_deviation = 0 + _mem.mode = MODES.index(mem.mode) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tune_step = STEPS.index(mem.tuning_step) + if mem.power: + _mem.power = 3 - POWER_LEVELS.index(mem.power) + else: + _mem.power = 0 + + label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()]) + _mem.label = self._add_ff_pad(label, 16) + # We only speak english here in chirpville + _mem.charsetbits[0] = 0x00 + _mem.charsetbits[1] = 0x00 + + flag.skip = mem.skip == "S" + flag.pskip = mem.skip == "P" + + def get_bank_model(self): + return VX8BankModel(self) + + @classmethod + def _digi_path_to_str(cls, path): + path_cmp = [] + for entry in path.entry: + callsign = str(entry.callsign).rstrip("\xFF") + if not callsign: + break + path_cmp.append("%s-%d" % (callsign, entry.ssid)) + return ",".join(path_cmp) + + @staticmethod + def _latlong_sanity(sign, l_d, l_m, l_s, is_lat): + if sign not in (0, 1): + sign = 0 + if is_lat: + d_max = 90 + else: + d_max = 180 + if l_d < 0 or l_d > d_max: + l_d = 0 + l_m = 0 + l_s = 0 + if l_m < 0 or l_m > 60: + l_m = 0 + l_s = 0 + if l_s < 0 or l_s > 60: + l_s = 0 + return sign, l_d, l_m, l_s + + @classmethod + def _latlong_to_str(cls, sign, l_d, l_m, l_s, is_lat, to_sexigesimal=True): + sign, l_d, l_m, l_s = cls._latlong_sanity(sign, l_d, l_m, l_s, is_lat) + mult = sign and -1 or 1 + if to_sexigesimal: + return "%d,%d'%d\"" % (mult * l_d, l_m, l_s) + return "%0.5f" % (mult * l_d + (l_m / 60.0) + (l_s / (60.0 * 60.0))) + + @classmethod + def _str_to_latlong(cls, lat_long, is_lat): + sign = 0 + result = [0, 0, 0] + + lat_long = lat_long.strip() + + if not lat_long: + return 1, 0, 0, 0 + + try: + # DD.MMMMM is the simple case, try that first. + val = float(lat_long) + if val < 0: + sign = 1 + val = abs(val) + result[0] = int(val) + result[1] = int(val * 60) % 60 + result[2] = int(val * 3600) % 60 + except ValueError: + # Try DD MM'SS" if DD.MMMMM failed. + match = cls._SG_RE.match(lat_long.strip()) + if match: + if match.group("sign") and (match.group("sign") in "SE-"): + sign = 1 + else: + sign = 0 + if match.group("d"): + result[0] = int(match.group("d")) + if match.group("m"): + result[1] = int(match.group("m")) + if match.group("s"): + result[2] = int(match.group("s")) + elif len(lat_long) > 4: + raise Exception("Lat/Long should be DD MM'SS\" or DD.MMMMM") + + return cls._latlong_sanity(sign, result[0], result[1], result[2], + is_lat) + + def _get_aprs_general_settings(self): + menu = RadioSettingGroup("aprs_general", "APRS General") + aprs = self._memobj.aprs + aprs2 = self._memobj.aprs2 + + val = RadioSettingValueString( + 0, 6, str(aprs.my_callsign.callsign).rstrip("\xFF")) + rs = RadioSetting("aprs.my_callsign.callsign", "My Callsign", val) + rs.set_apply_callback(self.apply_callsign, aprs.my_callsign) + menu.append(rs) + + val = RadioSettingValueList( + chirp_common.APRS_SSID, + chirp_common.APRS_SSID[aprs.my_callsign.ssid]) + rs = RadioSetting("aprs.my_callsign.ssid", "My SSID", val) + menu.append(rs) + + val = RadioSettingValueList(self._MY_SYMBOL, + self._MY_SYMBOL[aprs2.selected_my_symbol]) + rs = RadioSetting("aprs2.selected_my_symbol", "My Symbol", val) + menu.append(rs) + + symbols = list(chirp_common.APRS_SYMBOLS) + selected = aprs.custom_symbol + if aprs.custom_symbol >= len(chirp_common.APRS_SYMBOLS): + symbols.append("%d" % aprs.custom_symbol) + selected = len(symbols) - 1 + val = RadioSettingValueList(symbols, symbols[selected]) + rs = RadioSetting("aprs.custom_symbol_text", "User Selected Symbol", + val) + rs.set_apply_callback(self.apply_custom_symbol, aprs) + menu.append(rs) + + val = RadioSettingValueList( + chirp_common.APRS_POSITION_COMMENT, + chirp_common.APRS_POSITION_COMMENT[aprs.selected_position_comment]) + rs = RadioSetting("aprs.selected_position_comment", "Position Comment", + val) + menu.append(rs) + + latitude = self._latlong_to_str(aprs.latitude_sign, + aprs.latitude_degree, + aprs.latitude_minute, + aprs.latitude_second, + True, aprs.aprs_units_position_mmss) + longitude = self._latlong_to_str(aprs.longitude_sign, + aprs.longitude_degree, + aprs.longitude_minute, + aprs.longitude_second, + False, aprs.aprs_units_position_mmss) + + # TODO: Rebuild this when aprs_units_position_mmss changes. + # TODO: Rebuild this when latitude/longitude change. + # TODO: Add saved positions p1 - p10 to memory map. + position_str = list(self._POSITIONS) + # position_str[1] = "%s %s" % (latitude, longitude) + # position_str[2] = "%s %s" % (latitude, longitude) + val = RadioSettingValueList(position_str, + position_str[aprs.selected_position]) + rs = RadioSetting("aprs.selected_position", "My Position", val) + menu.append(rs) + + val = RadioSettingValueString(0, 10, latitude) + rs = RadioSetting("latitude", "Manual Latitude", val) + rs.set_apply_callback(self.apply_lat_long, aprs) + menu.append(rs) + + val = RadioSettingValueString(0, 11, longitude) + rs = RadioSetting("longitude", "Manual Longitude", val) + rs.set_apply_callback(self.apply_lat_long, aprs) + menu.append(rs) + + val = RadioSettingValueList( + self._TIME_SOURCE, self._TIME_SOURCE[aprs.set_time_manually]) + rs = RadioSetting("aprs.set_time_manually", "Time Source", val) + menu.append(rs) + + val = RadioSettingValueList(self._TZ, self._TZ[aprs.timezone]) + rs = RadioSetting("aprs.timezone", "Timezone", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SPEED_UNITS, self._SPEED_UNITS[aprs.aprs_units_speed]) + rs = RadioSetting("aprs.aprs_units_speed", "APRS Speed Units", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SPEED_UNITS, self._SPEED_UNITS[aprs.gps_units_speed]) + rs = RadioSetting("aprs.gps_units_speed", "GPS Speed Units", val) + menu.append(rs) + + val = RadioSettingValueList( + self._ALT_UNITS, self._ALT_UNITS[aprs.aprs_units_altitude_ft]) + rs = RadioSetting("aprs.aprs_units_altitude_ft", "APRS Altitude Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._ALT_UNITS, self._ALT_UNITS[aprs.gps_units_altitude_ft]) + rs = RadioSetting("aprs.gps_units_altitude_ft", "GPS Altitude Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._POS_UNITS, + self._POS_UNITS[aprs.aprs_units_position_mmss]) + rs = RadioSetting("aprs.aprs_units_position_mmss", + "APRS Position Format", val) + menu.append(rs) + + val = RadioSettingValueList( + self._POS_UNITS, self._POS_UNITS[aprs.gps_units_position_sss]) + rs = RadioSetting("aprs.gps_units_position_sss", + "GPS Position Format", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DIST_UNITS, + self._DIST_UNITS[aprs.aprs_units_distance_m]) + rs = RadioSetting("aprs.aprs_units_distance_m", "APRS Distance Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._WIND_UNITS, self._WIND_UNITS[aprs.aprs_units_wind_mph]) + rs = RadioSetting("aprs.aprs_units_wind_mph", "APRS Wind Speed Units", + val) + menu.append(rs) + + val = RadioSettingValueList( + self._RAIN_UNITS, self._RAIN_UNITS[aprs.aprs_units_rain_inch]) + rs = RadioSetting("aprs.aprs_units_rain_inch", "APRS Rain Units", val) + menu.append(rs) + + val = RadioSettingValueList( + self._TEMP_UNITS, + self._TEMP_UNITS[aprs.aprs_units_temperature_f]) + rs = RadioSetting("aprs.aprs_units_temperature_f", + "APRS Temperature Units", val) + menu.append(rs) + + return menu + + def _get_aprs_rx_settings(self): + menu = RadioSettingGroup("aprs_rx", "APRS Receive") + aprs = self._memobj.aprs + aprs2 = self._memobj.aprs2 + + val = RadioSettingValueList(self._RX_BAUD, self._RX_BAUD[aprs.rx_baud]) + rs = RadioSetting("aprs.rx_baud", "Modem RX", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.aprs_mute) + rs = RadioSetting("aprs.aprs_mute", "APRS Mute", val) + menu.append(rs) + + if self._has_af_dual: + val = RadioSettingValueBoolean(aprs.af_dual) + rs = RadioSetting("aprs.af_dual", "AF Dual", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.ring_msg) + rs = RadioSetting("aprs.ring_msg", "Ring on Message RX", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.ring_beacon) + rs = RadioSetting("aprs.ring_beacon", "Ring on Beacon RX", val) + menu.append(rs) + + val = RadioSettingValueList(self._FLASH, + self._FLASH[aprs2.flash_msg]) + rs = RadioSetting("aprs2.flash_msg", "Flash on personal message", val) + menu.append(rs) + + if self._has_vibrate: + val = RadioSettingValueList(self._FLASH, + self._FLASH[aprs2.vibrate_msg]) + rs = RadioSetting("aprs2.vibrate_msg", + "Vibrate on personal message", val) + menu.append(rs) + + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs2.flash_bln]) + rs = RadioSetting("aprs2.flash_bln", "Flash on bulletin message", val) + menu.append(rs) + + if self._has_vibrate: + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs2.vibrate_bln]) + rs = RadioSetting("aprs2.vibrate_bln", + "Vibrate on bulletin message", val) + menu.append(rs) + + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs2.flash_grp]) + rs = RadioSetting("aprs2.flash_grp", "Flash on group message", val) + menu.append(rs) + + if self._has_vibrate: + val = RadioSettingValueList(self._FLASH[:10], + self._FLASH[aprs2.vibrate_grp]) + rs = RadioSetting("aprs2.vibrate_grp", + "Vibrate on group message", val) + menu.append(rs) + + filter_val = [m.padded_string for m in aprs2.msg_group] + filter_val = self._strip_ff_pads(filter_val) + for index, filter_text in enumerate(filter_val): + val = RadioSettingValueString(0, 9, filter_text) + rs = RadioSetting("aprs2.msg_group_%d" % index, + "Message Group %d" % (index + 1), val) + menu.append(rs) + rs.set_apply_callback(self.apply_ff_padded_string, + aprs2.msg_group[index]) + # TODO: Use filter_val as the list entries and update it on edit. + val = RadioSettingValueList( + self._MSG_GROUP_NAMES, + self._MSG_GROUP_NAMES[aprs2.selected_msg_group]) + rs = RadioSetting("aprs2.selected_msg_group", "Selected Message Group", + val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_mic_e) + rs = RadioSetting("aprs.filter_mic_e", "Receive Mic-E Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_position) + rs = RadioSetting("aprs.filter_position", "Receive Position Beacons", + val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_weather) + rs = RadioSetting("aprs.filter_weather", "Receive Weather Beacons", + val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_object) + rs = RadioSetting("aprs.filter_object", "Receive Object Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_item) + rs = RadioSetting("aprs.filter_item", "Receive Item Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_status) + rs = RadioSetting("aprs.filter_status", "Receive Status Beacons", val) + menu.append(rs) + + val = RadioSettingValueBoolean(aprs.filter_other) + rs = RadioSetting("aprs.filter_other", "Receive Other Beacons", val) + menu.append(rs) + + return menu + + def _get_aprs_tx_settings(self): + menu = RadioSettingGroup("aprs_tx", "APRS Transmit") + aprs = self._memobj.aprs + aprs2 = self._memobj.aprs2 + + beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon + val = RadioSettingValueList( + self._BEACON_TYPE, self._BEACON_TYPE[beacon_type]) + rs = RadioSetting("aprs.transmit", "TX Beacons", val) + rs.set_apply_callback(self.apply_beacon_type, aprs) + menu.append(rs) + + val = RadioSettingValueList( + self._TX_DELAY, self._TX_DELAY[aprs.tx_delay]) + rs = RadioSetting("aprs.tx_delay", "TX Delay", val) + menu.append(rs) + + val = RadioSettingValueList( + self._BEACON_INT, self._BEACON_INT[aprs.beacon_interval]) + rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val) + menu.append(rs) + + desc = [] + status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt] + status = self._strip_ff_pads(status) + for index, msg_text in enumerate(status): + val = RadioSettingValueString(0, 60, msg_text) + desc.append("Beacon Status Text %d" % (index + 1)) + rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1], + val) + rs.set_apply_callback(self.apply_ff_padded_string, + self._memobj.aprs_beacon_status_txt[index]) + menu.append(rs) + val = RadioSettingValueList(desc, + desc[aprs.selected_beacon_status_txt]) + rs = RadioSetting("aprs.selected_beacon_status_txt", + "Beacon Status Text", val) + menu.append(rs) + + message_macro = [m.padded_string for m in self._memobj.aprs_msg_macro] + message_macro = self._strip_ff_pads(message_macro) + for index, msg_text in enumerate(message_macro): + val = RadioSettingValueString(0, 16, msg_text) + rs = RadioSetting("aprs_msg_macro_%d" % index, + "Message Macro %d" % (index + 1), val) + rs.set_apply_callback(self.apply_ff_padded_string, + self._memobj.aprs_msg_macro[index]) + menu.append(rs) + + path_str = list(self._DIGI_PATHS) + + path_str[7] = self._digi_path_to_str(aprs.digi_path_7) + val = RadioSettingValueString(0, 88, path_str[7]) + rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7) + menu.append(rs) + + # Show friendly messages for empty slots rather than blanks. + # TODO: Rebuild this when digi_path_[34567] change. + # path_str[3] = path_str[3] or self._DIGI_PATHS[3] + # path_str[4] = path_str[4] or self._DIGI_PATHS[4] + # path_str[5] = path_str[5] or self._DIGI_PATHS[5] + # path_str[6] = path_str[6] or self._DIGI_PATHS[6] + # path_str[7] = path_str[7] or self._DIGI_PATHS[7] + + path_str[7] = self._DIGI_PATHS[7] + val = RadioSettingValueList(path_str, + path_str[aprs2.selected_digi_path]) + rs = RadioSetting("aprs2.selected_digi_path", + "Selected Digi Path", val) + menu.append(rs) + + return menu + + def _get_dtmf_settings(self): + menu = RadioSettingGroup("dtmf_settings", "DTMF") + dtmf = self._memobj.scan_settings + + val = RadioSettingValueList( + self._DTMF_MODE, + self._DTMF_MODE[dtmf.dtmf_mode]) + rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DTMF_SPEED, + self._DTMF_SPEED[dtmf.dtmf_speed]) + rs = RadioSetting("scan_settings.dtmf_speed", + "DTMF AutoDial Speed", val) + menu.append(rs) + + val = RadioSettingValueList( + self._DTMF_DELAY, + self._DTMF_DELAY[dtmf.dtmf_delay]) + rs = RadioSetting("scan_settings.dtmf_delay", + "DTMF AutoDial Delay", val) + menu.append(rs) + + for i in range(10): + name = "dtmf_%02d" % i + dtmfsetting = self._memobj.dtmf[i] + dtmfstr = "" + for c in dtmfsetting.memory: + if c == 0xFF: + break + if c < len(VX8_DTMF_CHARS): + dtmfstr += VX8_DTMF_CHARS[c] + dtmfentry = RadioSettingValueString(0, 16, dtmfstr) + dtmfentry.set_charset(VX8_DTMF_CHARS + list("abcd ")) + rs = RadioSetting(name, name.upper(), dtmfentry) + rs.set_apply_callback(self.apply_dtmf, i) + menu.append(rs) + + return menu + + def _get_misc_settings(self): + menu = RadioSettingGroup("misc_settings", "Misc") + scan_settings = self._memobj.scan_settings + + val = RadioSettingValueList( + self._LCD_DIMMER, + self._LCD_DIMMER[scan_settings.lcd_dimmer]) + rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val) + menu.append(rs) + + val = RadioSettingValueList( + self._LCD_CONTRAST, + self._LCD_CONTRAST[scan_settings.lcd_contrast - 1]) + rs = RadioSetting("scan_settings.lcd_contrast", "LCD Contrast", + val) + rs.set_apply_callback(self.apply_lcd_contrast, scan_settings) + menu.append(rs) + + val = RadioSettingValueList( + self._LAMP_KEY, + self._LAMP_KEY[scan_settings.lamp]) + rs = RadioSetting("scan_settings.lamp", "Lamp", val) + menu.append(rs) + + beep_select = self._memobj.beep_select + + val = RadioSettingValueList( + self._BEEP_SELECT, + self._BEEP_SELECT[beep_select.beep]) + rs = RadioSetting("beep_select.beep", "Beep Select", val) + menu.append(rs) + + opening_message = self._memobj.opening_message + + val = RadioSettingValueList( + self._OPENING_MESSAGE, + self._OPENING_MESSAGE[opening_message.flag]) + rs = RadioSetting("opening_message.flag", "Opening Msg Mode", + val) + menu.append(rs) + + msg = "" + for i in opening_message.message.padded_yaesu: + if i == 0xFF: + break + msg += CHARSET[i & 0x7F] + val = RadioSettingValueString(0, 16, msg) + rs = RadioSetting("opening_message.message.padded_yaesu", + "Opening Message", val) + rs.set_apply_callback(self.apply_ff_padded_yaesu, + opening_message.message) + menu.append(rs) + + return menu + + def _get_scan_settings(self): + menu = RadioSettingGroup("scan_settings", "Scan") + scan_settings = self._memobj.scan_settings + + val = RadioSettingValueList( + self._VOL_MODE, + self._VOL_MODE[scan_settings.vol_mode]) + rs = RadioSetting("scan_settings.vol_mode", "Volume Mode", val) + menu.append(rs) + + vfoa = self._memobj.vfo_info[0] + val = RadioSettingValueList( + self._VOLUME, + self._VOLUME[vfoa.volume]) + rs = RadioSetting("vfo_info[0].volume", "VFO A Volume", val) + rs.set_apply_callback(self.apply_volume, 0) + menu.append(rs) + + vfob = self._memobj.vfo_info[1] + val = RadioSettingValueList( + self._VOLUME, + self._VOLUME[vfob.volume]) + rs = RadioSetting("vfo_info[1].volume", "VFO B Volume", val) + rs.set_apply_callback(self.apply_volume, 1) + menu.append(rs) + + squelch = self._memobj.squelch + val = RadioSettingValueList( + self._SQUELCH, + self._SQUELCH[squelch.vfo_a]) + rs = RadioSetting("squelch.vfo_a", "VFO A Squelch", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SQUELCH, + self._SQUELCH[squelch.vfo_b]) + rs = RadioSetting("squelch.vfo_b", "VFO B Squelch", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SCAN_RESTART, + self._SCAN_RESTART[scan_settings.scan_restart]) + rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val) + menu.append(rs) + + val = RadioSettingValueList( + self._SCAN_RESUME, + self._SCAN_RESUME[scan_settings.scan_resume]) + rs = RadioSetting("scan_settings.scan_resume", "Scan Resume", val) + menu.append(rs) + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.busy_led]) + rs = RadioSetting("scan_settings.busy_led", "Busy LED", val) + menu.append(rs) + + val = RadioSettingValueList( + self._OFF_ON, + self._OFF_ON[scan_settings.scan_lamp]) + rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val) + menu.append(rs) + + val = RadioSettingValueList( + self._TOT_TIME, + self._TOT_TIME[scan_settings.tot]) + rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val) + menu.append(rs) + + return menu + + def _get_settings(self): + top = RadioSettings(self._get_aprs_general_settings(), + self._get_aprs_rx_settings(), + self._get_aprs_tx_settings(), + self._get_dtmf_settings(), + self._get_misc_settings(), + self._get_scan_settings()) + return top + + def get_settings(self): + try: + return self._get_settings() + except: + import traceback + LOG.error("Failed to parse settings: %s", traceback.format_exc()) + return None + + @staticmethod + def apply_custom_symbol(setting, obj): + # Ensure new value falls within known bounds, otherwise leave it as + # it's a custom value from the radio that's outside our list. + if setting.value.get_value() in chirp_common.APRS_SYMBOLS: + setattr(obj, "custom_symbol", + chirp_common.APRS_SYMBOLS.index(setting.value.get_value())) + + @classmethod + def _apply_callsign(cls, callsign, obj, default_ssid=None): + ssid = default_ssid + dash_index = callsign.find("-") + if dash_index >= 0: + ssid = callsign[dash_index + 1:] + callsign = callsign[:dash_index] + try: + ssid = int(ssid) % 16 + except ValueError: + ssid = default_ssid + setattr(obj, "callsign", cls._add_ff_pad(callsign, 6)) + if ssid is not None: + setattr(obj, "ssid", ssid) + + def apply_beacon_type(cls, setting, obj): + beacon_type = str(setting.value.get_value()) + beacon_index = cls._BEACON_TYPE.index(beacon_type) + tx_smartbeacon = beacon_index >> 1 + tx_interval_beacon = beacon_index & 1 + if tx_interval_beacon: + setattr(obj, "tx_interval_beacon", 1) + setattr(obj, "tx_smartbeacon", 0) + elif tx_smartbeacon: + setattr(obj, "tx_interval_beacon", 0) + setattr(obj, "tx_smartbeacon", 1) + else: + setattr(obj, "tx_interval_beacon", 0) + setattr(obj, "tx_smartbeacon", 0) + + @classmethod + def apply_callsign(cls, setting, obj, default_ssid=None): + # Uppercase, strip SSID then FF pad to max string length. + callsign = setting.value.get_value().upper() + cls._apply_callsign(callsign, obj, default_ssid) + + def apply_digi_path(self, setting, obj): + # Parse and map to aprs.digi_path_4_7[0-3] or aprs.digi_path_8 + # and FF terminate. + path = str(setting.value.get_value()) + callsigns = [c.strip() for c in path.split(",")] + for index in range(len(obj.entry)): + try: + self._apply_callsign(callsigns[index], obj.entry[index], 0) + except IndexError: + self._apply_callsign("", obj.entry[index], 0) + if len(callsigns) > len(obj.entry): + raise Exception("This path only supports %d entries" % (index + 1)) + + @classmethod + def apply_ff_padded_string(cls, setting, obj): + # FF pad. + val = setting.value.get_value() + max_len = getattr(obj, "padded_string").size() / 8 + val = str(val).rstrip() + setattr(obj, "padded_string", cls._add_ff_pad(val, max_len)) + + @classmethod + def apply_lat_long(cls, setting, obj): + name = setting.get_name() + is_latitude = name.endswith("latitude") + lat_long = setting.value.get_value().strip() + sign, l_d, l_m, l_s = cls._str_to_latlong(lat_long, is_latitude) + LOG.debug("%s: %d %d %d %d" % (name, sign, l_d, l_m, l_s)) + setattr(obj, "%s_sign" % name, sign) + setattr(obj, "%s_degree" % name, l_d) + setattr(obj, "%s_minute" % name, l_m) + setattr(obj, "%s_second" % name, l_s) + + def set_settings(self, settings): + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + if element.has_apply_callback(): + LOG.debug("Using apply callback") + try: + element.run_apply_callback() + except NotImplementedError as e: + LOG.error("vx8.set_settings: %s", e) + continue + + # Find the object containing setting. + obj = _mem + bits = element.get_name().split(".") + setting = bits[-1] + for name in bits[:-1]: + if name.endswith("]"): + name, index = name.split("[") + index = int(index[:-1]) + obj = getattr(obj, name)[index] + else: + obj = getattr(obj, name) + + try: + old_val = getattr(obj, setting) + LOG.debug("Setting %s(%r) <= %s" % ( + element.get_name(), old_val, element.value)) + setattr(obj, setting, element.value) + except AttributeError as e: + LOG.error("Setting %s is not in the memory map: %s" % + (element.get_name(), e)) + except Exception as e: + LOG.debug(element.get_name()) + raise + + def apply_ff_padded_yaesu(cls, setting, obj): + # FF pad yaesus custom string format. + rawval = setting.value.get_value() + max_len = getattr(obj, "padded_yaesu").size() / 8 + rawval = str(rawval).rstrip() + val = [CHARSET.index(x) for x in rawval] + for x in range(len(val), max_len): + val.append(0xFF) + obj.padded_yaesu = val + + def apply_volume(cls, setting, vfo): + val = setting.value.get_value() + cls._memobj.vfo_info[(vfo*2)].volume = val + cls._memobj.vfo_info[(vfo*2)+1].volume = val + + def apply_lcd_contrast(cls, setting, obj): + rawval = setting.value.get_value() + val = cls._LCD_CONTRAST.index(rawval) + 1 + obj.lcd_contrast = val + + def apply_dtmf(cls, setting, i): + rawval = setting.value.get_value().upper().rstrip() + val = [VX8_DTMF_CHARS.index(x) for x in rawval] + for x in range(len(val), 16): + val.append(0xFF) + cls._memobj.dtmf[i].memory = val + + +@directory.register +class VX8DRadio(VX8Radio): + """Yaesu VX-8DR""" + MODEL = "VX-8DR" + _model = "AH29D" + _mem_params = (0xC128, # APRS message macros + 7, # Number of message macros + 0xC198, # APRS2 + 0xC24A, # APRS beacon metadata address. + 50, # Number of beacons stored. + 0xC6FA, # APRS beacon content address. + 146, # Length of beacon data stored. + 50) # Number of beacons stored. + + _BEACON_TYPE = ("Off", "Interval", "SmartBeaconing") + _SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3") + _POSITIONS = ("GPS", "Manual Latitude/Longitude", + "Manual Latitude/Longitude", "P1", "P2", "P3", "P4", + "P5", "P6", "P7", "P8", "P9") + _FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds", + "10 seconds", "20 seconds", "30 seconds", "60 seconds", + "CONTINUOUS", "every 2 seconds", "every 3 seconds", + "every 4 seconds", "every 5 seconds", "every 6 seconds", + "every 7 seconds", "every 8 seconds", "every 9 seconds", + "every 10 seconds", "every 20 seconds", "every 30 seconds", + "every 40 seconds", "every 50 seconds", "every minute", + "every 2 minutes", "every 3 minutes", "every 4 minutes", + "every 5 minutes", "every 6 minutes", "every 7 minutes", + "every 8 minutes", "every 9 minutes", "every 10 minutes") + _LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)] + _MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected") + + def _get_aprs_tx_settings(self): + menu = RadioSettingGroup("aprs_tx", "APRS Transmit") + aprs = self._memobj.aprs + aprs2 = self._memobj.aprs2 + + beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon + val = RadioSettingValueList( + self._BEACON_TYPE, self._BEACON_TYPE[beacon_type]) + rs = RadioSetting("aprs.transmit", "TX Beacons", val) + rs.set_apply_callback(self.apply_beacon_type, aprs) + menu.append(rs) + + val = RadioSettingValueList( + self._TX_DELAY, self._TX_DELAY[aprs.tx_delay]) + rs = RadioSetting("aprs.tx_delay", "TX Delay", val) + menu.append(rs) + + val = RadioSettingValueList( + self._BEACON_INT, self._BEACON_INT[aprs.beacon_interval]) + rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val) + menu.append(rs) + + desc = [] + status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt] + status = self._strip_ff_pads(status) + for index, msg_text in enumerate(status): + val = RadioSettingValueString(0, 60, msg_text) + desc.append("Beacon Status Text %d" % (index + 1)) + rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1], + val) + rs.set_apply_callback(self.apply_ff_padded_string, + self._memobj.aprs_beacon_status_txt[index]) + menu.append(rs) + val = RadioSettingValueList(desc, + desc[aprs.selected_beacon_status_txt]) + rs = RadioSetting("aprs.selected_beacon_status_txt", + "Beacon Status Text", val) + menu.append(rs) + + message_macro = [m.padded_string for m in self._memobj.aprs_msg_macro] + message_macro = self._strip_ff_pads(message_macro) + for index, msg_text in enumerate(message_macro): + val = RadioSettingValueString(0, 16, msg_text) + rs = RadioSetting("aprs_msg_macro_%d" % index, + "Message Macro %d" % (index + 1), val) + rs.set_apply_callback(self.apply_ff_padded_string, + self._memobj.aprs_msg_macro[index]) + menu.append(rs) + + path_str = list(self._DIGI_PATHS) + path_str[3] = self._digi_path_to_str(aprs2.digi_path_3_6[0]) + val = RadioSettingValueString(0, 22, path_str[3]) + rs = RadioSetting("aprs2.digi_path_3", "Digi Path 4 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[0]) + menu.append(rs) + + path_str[4] = self._digi_path_to_str(aprs2.digi_path_3_6[1]) + val = RadioSettingValueString(0, 22, path_str[4]) + rs = RadioSetting("aprs2.digi_path_4", "Digi Path 5 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[1]) + menu.append(rs) + + path_str[5] = self._digi_path_to_str(aprs2.digi_path_3_6[2]) + val = RadioSettingValueString(0, 22, path_str[5]) + rs = RadioSetting("aprs2.digi_path_5", "Digi Path 6 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[2]) + menu.append(rs) + + path_str[6] = self._digi_path_to_str(aprs2.digi_path_3_6[3]) + val = RadioSettingValueString(0, 22, path_str[6]) + rs = RadioSetting("aprs2.digi_path_6", "Digi Path 7 (2 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[3]) + menu.append(rs) + + path_str[7] = self._digi_path_to_str(aprs.digi_path_7) + val = RadioSettingValueString(0, 88, path_str[7]) + rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val) + rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7) + menu.append(rs) + + # Show friendly messages for empty slots rather than blanks. + # TODO: Rebuild this when digi_path_[34567] change. + # path_str[3] = path_str[3] or self._DIGI_PATHS[3] + # path_str[4] = path_str[4] or self._DIGI_PATHS[4] + # path_str[5] = path_str[5] or self._DIGI_PATHS[5] + # path_str[6] = path_str[6] or self._DIGI_PATHS[6] + # path_str[7] = path_str[7] or self._DIGI_PATHS[7] + path_str[3] = self._DIGI_PATHS[3] + path_str[4] = self._DIGI_PATHS[4] + path_str[5] = self._DIGI_PATHS[5] + path_str[6] = self._DIGI_PATHS[6] + path_str[7] = self._DIGI_PATHS[7] + val = RadioSettingValueList(path_str, + path_str[aprs2.selected_digi_path]) + rs = RadioSetting("aprs2.selected_digi_path", + "Selected Digi Path", val) + menu.append(rs) + + return menu + + def _get_aprs_smartbeacon(self): + menu = RadioSettingGroup("aprs_smartbeacon", "APRS SmartBeacon") + aprs2 = self._memobj.aprs2 + + val = RadioSettingValueList( + self._SMARTBEACON_PROFILE, + self._SMARTBEACON_PROFILE[aprs2.active_smartbeaconing]) + rs = RadioSetting("aprs2.active_smartbeaconing", + "SmartBeacon profile", val) + menu.append(rs) + + for profile in range(3): + pfx = "type%d" % (profile + 1) + path = "aprs2.smartbeaconing_profile[%d]" % profile + prof = aprs2.smartbeaconing_profile[profile] + + low_val = RadioSettingValueInteger(2, 30, prof.low_speed_mph) + high_val = RadioSettingValueInteger(3, 70, prof.high_speed_mph) + low_val.get_max = lambda: min(30, int(high_val.get_value()) - 1) + + rs = RadioSetting("%s.low_speed_mph" % path, + "%s Low Speed (mph)" % pfx, low_val) + menu.append(rs) + + rs = RadioSetting("%s.high_speed_mph" % path, + "%s High Speed (mph)" % pfx, high_val) + menu.append(rs) + + val = RadioSettingValueInteger(1, 100, prof.slow_rate_min) + rs = RadioSetting("%s.slow_rate_min" % path, + "%s Slow rate (minutes)" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(10, 180, prof.fast_rate_sec) + rs = RadioSetting("%s.fast_rate_sec" % path, + "%s Fast rate (seconds)" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(5, 90, prof.turn_angle) + rs = RadioSetting("%s.turn_angle" % path, + "%s Turn angle (degrees)" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(1, 255, prof.turn_slop) + rs = RadioSetting("%s.turn_slop" % path, + "%s Turn slop" % pfx, val) + menu.append(rs) + + val = RadioSettingValueInteger(5, 180, prof.turn_time_sec) + rs = RadioSetting("%s.turn_time_sec" % path, + "%s Turn time (seconds)" % pfx, val) + menu.append(rs) + + return menu + + def _get_settings(self): + top = RadioSettings(self._get_aprs_general_settings(), + self._get_aprs_rx_settings(), + self._get_aprs_tx_settings(), + self._get_aprs_smartbeacon(), + self._get_dtmf_settings(), + self._get_misc_settings(), + self._get_scan_settings()) + return top + + +@directory.register +class VX8GERadio(VX8DRadio): + """Yaesu VX-8GE""" + MODEL = "VX-8GE" + _model = "AH041" + _has_vibrate = True + _has_af_dual = False diff --git a/chirp/drivers/vxa700.py b/chirp/drivers/vxa700.py new file mode 100644 index 0000000..87aeaa0 --- /dev/null +++ b/chirp/drivers/vxa700.py @@ -0,0 +1,317 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common, util, directory, memmap, errors +from chirp import bitwise + +import time +import struct +import logging + +LOG = logging.getLogger(__name__) + + +def _send(radio, data): + LOG.debug("Sending %s" % repr(data)) + radio.pipe.write(data) + radio.pipe.flush() + echo = radio.pipe.read(len(data)) + if len(echo) != len(data): + raise errors.RadioError("Invalid echo") + + +def _spoonfeed(radio, data): + # count = 0 + _debug("Writing %i:\n%s" % (len(data), util.hexprint(data))) + for byte in data: + radio.pipe.write(byte) + radio.pipe.flush() + time.sleep(0.01) + continue + # This is really unreliable for some reason, + # so just blindly send the data + echo = radio.pipe.read(1) + if echo != byte: + LOG.debug("%02x != %02x" % (ord(echo), ord(byte))) + raise errors.RadioError("No echo?") + # count += 1 + + +def _download(radio): + count = 0 + data = "" + while len(data) < radio.get_memsize(): + count += 1 + chunk = radio.pipe.read(133) + if len(chunk) == 0 and len(data) == 0 and count < 30: + continue + if len(chunk) != 132: + raise errors.RadioError("Got short block (length %i)" % len(chunk)) + + checksum = ord(chunk[-1]) + _flag, _length, _block, _data, checksum = \ + struct.unpack("BBB128sB", chunk) + + cs = 0 + for byte in chunk[:-1]: + cs += ord(byte) + if (cs % 256) != checksum: + raise errors.RadioError("Invalid checksum at 0x%02x" % len(data)) + + data += _data + _send(radio, "\x06") + + if radio.status_fn: + status = chirp_common.Status() + status.msg = "Cloning from radio" + status.cur = len(data) + status.max = radio.get_memsize() + radio.status_fn(status) + + return memmap.MemoryMap(data) + + +def _upload(radio): + for i in range(0, radio.get_memsize(), 128): + chunk = radio.get_mmap()[i:i+128] + cs = 0x20 + 130 + (i / 128) + for byte in chunk: + cs += ord(byte) + _spoonfeed(radio, + struct.pack("BBB128sB", + 0x20, + 130, + i / 128, + chunk, + cs % 256)) + radio.pipe.write("") + # This is really unreliable for some reason, so just + # blindly proceed + # ack = radio.pipe.read(1) + ack = "\x06" + time.sleep(0.5) + if ack != "\x06": + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not ack block %i" % (i / 132)) + # radio.pipe.read(1) + if radio.status_fn: + status = chirp_common.Status() + status.msg = "Cloning to radio" + status.cur = i + status.max = radio.get_memsize() + radio.status_fn(status) + +MEM_FORMAT = """ +struct memory_struct { + u8 unknown1; + u8 unknown2:2, + isfm:1, + power:2, + step:3; + u8 unknown5:2, + showname:1, + skip:1, + duplex:2, + unknown6:2; + u8 tmode:2, + unknown7:6; + u8 unknown8; + u8 unknown9:2, + tone:6; + u8 dtcs; + u8 name[8]; + u16 freq; + u8 offset; +}; + +u8 headerbytes[6]; + +#seekto 0x0006; +u8 invisible_bits[13]; +u8 bitfield_pad[3]; +u8 invalid_bits[13]; + +#seekto 0x017F; +struct memory_struct memory[100]; +""" + +CHARSET = "".join(["%i" % i for i in range(0, 10)]) + \ + "".join([chr(ord("A") + i) for i in range(0, 26)]) + \ + "".join([chr(ord("a") + i) for i in range(0, 26)]) + \ + "., :;!\"#$%&'()*+-/=<>?@[?]^_`{|}????~??????????????????????????" + +TMODES = ["", "Tone", "TSQL", "DTCS"] +DUPLEX = ["", "-", "+", ""] +POWER = [chirp_common.PowerLevel("Low1", watts=0.050), + chirp_common.PowerLevel("Low2", watts=1.000), + chirp_common.PowerLevel("Low3", watts=2.500), + chirp_common.PowerLevel("High", watts=5.000)] + + +def _wipe_memory(_mem): + _mem.set_raw("\x00" * (_mem.size() / 8)) + + +@directory.register +class VXA700Radio(chirp_common.CloneModeRadio): + """Vertex Standard VXA-700""" + VENDOR = "Vertex Standard" + MODEL = "VXA-700" + _memsize = 4096 + + def sync_in(self): + try: + self.pipe.timeout = 2 + self._mmap = _download(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate " + + "with the radio: %s" % e) + self.process_mmap() + + def sync_out(self): + # header[4] = 0x00 <- default + # 0xFF <- air band only + # 0x01 <- air band only + # 0x02 <- air band only + try: + self.pipe.timeout = 2 + _upload(self) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate " + + "with the radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_ctone = False + rf.has_dtcs_polarity = False + rf.has_tuning_step = False + rf.valid_tmodes = TMODES + rf.valid_name_length = 8 + rf.valid_characters = CHARSET + rf.valid_skips = ["", "S"] + rf.valid_bands = [(88000000, 165000000)] + rf.valid_tuning_steps = \ + [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0] + rf.valid_modes = ["AM", "FM"] + rf.valid_power_levels = POWER + rf.memory_bounds = (1, 100) + return rf + + def _get_mem(self, number): + return self._memobj.memory[number - 1] + + def get_raw_memory(self, number): + _mem = self._get_mem(number) + return repr(_mem) + util.hexprint(_mem.get_raw()) + + def get_memory(self, number): + _mem = self._get_mem(number) + byte = (number - 1) / 8 + bit = 1 << ((number - 1) % 8) + + mem = chirp_common.Memory() + mem.number = number + + if self._memobj.invisible_bits[byte] & bit: + mem.empty = True + if self._memobj.invalid_bits[byte] & bit: + mem.empty = True + return mem + + if _mem.step & 0x05: # Not sure this is right, but it seems to be + mult = 6250 + else: + mult = 5000 + + mem.freq = int(_mem.freq) * mult + mem.rtone = chirp_common.TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs] + mem.tmode = TMODES[_mem.tmode] + mem.duplex = DUPLEX[_mem.duplex] + mem.offset = int(_mem.offset) * 5000 * 10 + mem.mode = _mem.isfm and "FM" or "AM" + mem.skip = _mem.skip and "S" or "" + mem.power = POWER[_mem.power] + + for char in _mem.name: + try: + mem.name += CHARSET[char] + except IndexError: + break + mem.name = mem.name.rstrip() + + return mem + + def set_memory(self, mem): + _mem = self._get_mem(mem.number) + byte = (mem.number - 1) / 8 + bit = 1 << ((mem.number - 1) % 8) + + if mem.empty and self._memobj.invisible_bits[byte] & bit: + self._memobj.invalid_bits[byte] |= bit + return + if mem.empty: + self._memobj.invisible_bits[byte] |= bit + return + + if self._memobj.invalid_bits[byte] & bit: + _wipe_memory(_mem) + + self._memobj.invisible_bits[byte] &= ~bit + self._memobj.invalid_bits[byte] &= ~bit + + _mem.unknown2 = 0x02 # Channels don't display without this + _mem.unknown7 = 0x01 # some bit in this field is related to + _mem.unknown8 = 0xFF # being able to transmit + + if chirp_common.required_step(mem.freq) == 12.5: + mult = 6250 + _mem.step = 0x05 + else: + mult = 5000 + _mem.step = 0x00 + + _mem.freq = mem.freq / mult + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs) + _mem.tmode = TMODES.index(mem.tmode) + _mem.duplex = DUPLEX.index(mem.duplex) + _mem.offset = mem.offset / 5000 / 10 + _mem.isfm = mem.mode == "FM" + _mem.skip = mem.skip == "S" + try: + _mem.power = POWER.index(mem.power) + except ValueError: + _mem.power = 3 # High + + for i in range(0, 8): + try: + _mem.name[i] = CHARSET.index(mem.name[i]) + except IndexError: + _mem.name[i] = 0x40 + _mem.showname = bool(mem.name.strip()) + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and \ + ord(filedata[5]) == 0x0F diff --git a/chirp/drivers/wouxun.py b/chirp/drivers/wouxun.py new file mode 100644 index 0000000..21a1767 --- /dev/null +++ b/chirp/drivers/wouxun.py @@ -0,0 +1,1567 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Wouxun radios management module""" + +import time +import os +import logging +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettingValueFloat, RadioSettings +from wouxun_common import wipe_memory, do_download, do_upload +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +FREQ_ENCODE_TABLE = [0x7, 0xa, 0x0, 0x9, 0xb, 0x2, 0xe, 0x1, 0x3, 0xf] + + +def encode_freq(freq): + """Convert frequency (4 decimal digits) to wouxun format (2 bytes)""" + enc = 0 + div = 1000 + for i in range(0, 4): + enc <<= 4 + enc |= FREQ_ENCODE_TABLE[(freq/div) % 10] + div /= 10 + return enc + + +def decode_freq(data): + """Convert from wouxun format (2 bytes) to frequency (4 decimal digits)""" + freq = 0 + shift = 12 + for i in range(0, 4): + freq *= 10 + freq += FREQ_ENCODE_TABLE.index((data >> shift) & 0xf) + shift -= 4 + # LOG.debug("data %04x freq %d shift %d" % (data, freq, shift)) + return freq + + +@directory.register +class KGUVD1PRadio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """Wouxun KG-UVD1P,UV2,UV3""" + VENDOR = "Wouxun" + MODEL = "KG-UVD1P" + _model = "KG669V" + + _querymodel = ("HiWOUXUN\x02", "PROGUV6X\x02") + + CHARSET = list("0123456789") + \ + [chr(x + ord("A")) for x in range(0, 26)] + list("?+-") + + POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Low", watts=1.00)] + + valid_freq = [(136000000, 175000000), (216000000, 520000000)] + + _MEM_FORMAT = """ + #seekto 0x0010; + struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 _3_unknown_1:4, + bcl:1, + _3_unknown_2:3; + u8 splitdup:1, + skip:1, + power_high:1, + iswide:1, + _2_unknown_2:4; + u8 unknown; + u8 _0_unknown_1:3, + iswidex:1, + _0_unknown_2:4; + } memory[199]; + + #seekto 0x0842; + u16 fm_presets_0[9]; + + #seekto 0x0882; + u16 fm_presets_1[9]; + + #seekto 0x0970; + struct { + u16 vhf_rx_start; + u16 vhf_rx_stop; + u16 uhf_rx_start; + u16 uhf_rx_stop; + u16 vhf_tx_start; + u16 vhf_tx_stop; + u16 uhf_tx_start; + u16 uhf_tx_stop; + } freq_ranges; + + #seekto 0x0E00; + struct { + char welcome1[6]; + char welcome2[6]; + char single_band[6]; + } strings; + + #seekto 0x0E20; + struct { + u8 unknown_flag_01:6, + vfo_b_ch_disp:2; + u8 unknown_flag_02:5, + vfo_a_fr_step:3; + u8 unknown_flag_03:4, + vfo_a_squelch:4; + u8 unknown_flag_04:7, + power_save:1; + u8 unknown_flag_05:8; + u8 unknown_flag_06:6, + roger_beep:2; + u8 unknown_flag_07:2, + transmit_time_out:6; + u8 unknown_flag_08:4, + vox:4; + u8 unknown_1[4]; + u8 unknown_flag_09:6, + voice:2; + u8 unknown_flag_10:7, + beep:1; + u8 unknown_flag_11:7, + ani_id_enable:1; + u8 unknown_2[2]; + u8 unknown_flag_12:5, + vfo_b_fr_step:3; + u8 unknown_3[1]; + u8 unknown_flag_13:3, + ani_id_tx_delay:5; + u8 unknown_4[1]; + u8 unknown_flag_14:6, + ani_id_sidetone:2; + u8 unknown_flag_15:4, + tx_time_out_alert:4; + u8 unknown_flag_16:6, + vfo_a_ch_disp:2; + u8 unknown_flag_15:6, + scan_mode:2; + u8 unknown_flag_16:7, + kbd_lock:1; + u8 unknown_flag_17:6, + ponmsg:2; + u8 unknown_flag_18:5, + pf1_function:3; + u8 unknown_5[1]; + u8 unknown_flag_19:7, + auto_backlight:1; + u8 unknown_flag_20:7, + sos_ch:1; + u8 unknown_6; + u8 sd_available; + u8 unknown_flag_21:7, + auto_lock_kbd:1; + u8 unknown_flag_22:4, + vfo_b_squelch:4; + u8 unknown_7[1]; + u8 unknown_flag_23:7, + stopwatch:1; + u8 vfo_a_cur_chan; + u8 unknown_flag_24:7, + dual_band_receive:1; + u8 current_vfo:1, + unknown_flag_24:7; + u8 unknown_8[2]; + u8 mode_password[6]; + u8 reset_password[6]; + u8 ani_id_content[6]; + u8 unknown_flag_25:7, + menu_available:1; + u8 unknown_9[1]; + u8 priority_chan; + u8 vfo_b_cur_chan; + } settings; + + #seekto 0x1008; + struct { + u8 unknown[8]; + u8 name[6]; + u8 pad[2]; + } names[199]; + """ + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This version of the Wouxun driver allows you to modify the ' + 'frequency range settings of your radio. This has been tested ' + 'and reports from other users indicate that it is a safe ' + 'thing to do. However, modifications to this value may have ' + 'unintended consequences, including damage to your device. ' + 'You have been warned. Proceed at your own risk!') + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to mic/spkr connector. + 3. Make sure connector is firmly connected. + 4. Turn radio on. + 5. Ensure that the radio is tuned to channel with no activity. + 6. Click OK to download image from device.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to mic/spkr connector. + 3. Make sure connector is firmly connected. + 4. Turn radio on. + 5. Ensure that the radio is tuned to channel with no activity. + 6. Click OK to upload image to device.""")) + return rp + + @classmethod + def _get_querymodel(cls): + if isinstance(cls._querymodel, str): + while True: + yield cls._querymodel + else: + i = 0 + while True: + yield cls._querymodel[i % len(cls._querymodel)] + i += 1 + + def _identify(self): + """Do the original wouxun identification dance""" + query = self._get_querymodel() + for _i in range(0, 10): + self.pipe.write(query.next()) + resp = self.pipe.read(9) + if len(resp) != 9: + LOG.debug("Got:\n%s" % util.hexprint(resp)) + LOG.info("Retrying identification...") + time.sleep(1) + continue + if resp[2:8] != self._model: + raise Exception("I can't talk to this model (%s)" % + util.hexprint(resp)) + return + if len(resp) == 0: + raise Exception("Radio not responding") + else: + raise Exception("Unable to identify radio") + + def _start_transfer(self): + """Tell the radio to go into transfer mode""" + self.pipe.write("\x02\x06") + time.sleep(0.05) + ack = self.pipe.read(1) + if ack != "\x06": + raise Exception("Radio refused transfer mode") + + def _download(self): + """Talk to an original wouxun and do a download""" + try: + self._identify() + self._start_transfer() + return do_download(self, 0x0000, 0x2000, 0x0040) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def _upload(self): + """Talk to an original wouxun and do an upload""" + try: + self._identify() + self._start_transfer() + return do_upload(self, 0x0000, 0x2000, 0x0010) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def sync_in(self): + self._mmap = self._download() + self.process_mmap() + + def sync_out(self): + self._upload() + + def process_mmap(self): + if len(self._mmap.get_packed()) != 8192: + LOG.info("Fixing old-style Wouxun image") + # Originally, CHIRP's wouxun image had eight bytes of + # static data, followed by the first memory at offset + # 0x0008. Between 0.1.11 and 0.1.12, this was fixed to 16 + # bytes of (whatever) followed by the first memory at + # offset 0x0010, like the radio actually stores it. So, + # if we find one of those old ones, convert it to the new + # format, padding 16 bytes of 0xFF in front. + self._mmap = memmap.MemoryMap( + ("\xFF" * 16) + self._mmap.get_packed()[8:8184]) + self._memobj = bitwise.parse(self._MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"] + rf.valid_cross_modes = [ + "Tone->Tone", + "Tone->DTCS", + "DTCS->Tone", + "DTCS->", + "->Tone", + "->DTCS", + "DTCS->DTCS", + ] + rf.valid_modes = ["FM", "NFM"] + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_bands = self.valid_freq + rf.valid_characters = "".join(self.CHARSET) + rf.valid_name_length = 6 + rf.valid_duplexes = ["", "+", "-", "split", "off"] + rf.valid_tuning_steps = [5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] + rf.has_ctone = True + rf.has_rx_dtcs = True + rf.has_cross = True + rf.has_tuning_step = False + rf.has_bank = False + rf.has_settings = True + rf.memory_bounds = (1, 128) + rf.can_odd_split = True + return rf + + def get_settings(self): + freq_ranges = RadioSettingGroup("freq_ranges", "Freq Ranges") + fm_preset = RadioSettingGroup("fm_preset", "FM Presets") + cfg_s = RadioSettingGroup("cfg_settings", "Configuration Settings") + group = RadioSettings(cfg_s, freq_ranges, fm_preset) + + rs = RadioSetting("menu_available", "Menu Available", + RadioSettingValueBoolean( + self._memobj.settings.menu_available)) + cfg_s.append(rs) + + rs = RadioSetting("vhf_rx_start", "1st band RX Lower Limit (MHz)", + RadioSettingValueInteger( + 50, 174, decode_freq( + self._memobj.freq_ranges.vhf_rx_start))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_rx_stop", "1st band RX Upper Limit (MHz)", + RadioSettingValueInteger( + 50, 174, decode_freq( + self._memobj.freq_ranges.vhf_rx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_rx_start", "2nd band RX Lower Limit (MHz)", + RadioSettingValueInteger( + 136, 520, decode_freq( + self._memobj.freq_ranges.uhf_rx_start))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_rx_stop", "2nd band RX Upper Limit (MHz)", + RadioSettingValueInteger( + 136, 520, decode_freq( + self._memobj.freq_ranges.uhf_rx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_tx_start", "1st band TX Lower Limit (MHz)", + RadioSettingValueInteger( + 50, 174, decode_freq( + self._memobj.freq_ranges.vhf_tx_start))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_tx_stop", "1st TX Upper Limit (MHz)", + RadioSettingValueInteger( + 50, 174, decode_freq( + self._memobj.freq_ranges.vhf_tx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_tx_start", "2st band TX Lower Limit (MHz)", + RadioSettingValueInteger( + 136, 520, decode_freq( + self._memobj.freq_ranges.uhf_tx_start))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_tx_stop", "2st band TX Upper Limit (MHz)", + RadioSettingValueInteger( + 136, 520, decode_freq( + self._memobj.freq_ranges.uhf_tx_stop))) + freq_ranges.append(rs) + + # tell the decoded ranges to UI + freq_ranges = self._memobj.freq_ranges + self.valid_freq = \ + [(decode_freq(freq_ranges.vhf_rx_start) * 1000000, + (decode_freq(freq_ranges.vhf_rx_stop) + 1) * 1000000), + (decode_freq(freq_ranges.uhf_rx_start) * 1000000, + (decode_freq(freq_ranges.uhf_rx_stop) + 1) * 1000000)] + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + # add some radio specific settings + options = ["Off", "Welcome", "V bat"] + rs = RadioSetting("ponmsg", "Poweron message", + RadioSettingValueList( + options, options[self._memobj.settings.ponmsg])) + cfg_s.append(rs) + rs = RadioSetting("strings.welcome1", "Power-On Message 1", + RadioSettingValueString( + 0, 6, _filter(self._memobj.strings.welcome1))) + cfg_s.append(rs) + rs = RadioSetting("strings.welcome2", "Power-On Message 2", + RadioSettingValueString( + 0, 6, _filter(self._memobj.strings.welcome2))) + cfg_s.append(rs) + rs = RadioSetting("strings.single_band", "Single Band Message", + RadioSettingValueString( + 0, 6, _filter(self._memobj.strings.single_band))) + cfg_s.append(rs) + options = ["Channel", "ch/freq", "Name", "VFO"] + rs = RadioSetting("vfo_a_ch_disp", "VFO A Channel disp mode", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_a_ch_disp])) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_ch_disp", "VFO B Channel disp mode", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_b_ch_disp])) + cfg_s.append(rs) + options = ["5.0", "6.25", "10.0", "12.5", "25.0", "50.0", "100.0"] + rs = RadioSetting("vfo_a_fr_step", "VFO A Frequency Step", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_a_fr_step])) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_fr_step", "VFO B Frequency Step", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_b_fr_step])) + cfg_s.append(rs) + rs = RadioSetting("vfo_a_squelch", "VFO A Squelch", + RadioSettingValueInteger( + 0, 9, self._memobj.settings.vfo_a_squelch)) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_squelch", "VFO B Squelch", + RadioSettingValueInteger( + 0, 9, self._memobj.settings.vfo_b_squelch)) + cfg_s.append(rs) + rs = RadioSetting("vfo_a_cur_chan", "VFO A current channel", + RadioSettingValueInteger( + 1, 128, self._memobj.settings.vfo_a_cur_chan)) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_cur_chan", "VFO B current channel", + RadioSettingValueInteger( + 1, 128, self._memobj.settings.vfo_b_cur_chan)) + cfg_s.append(rs) + rs = RadioSetting("priority_chan", "Priority channel", + RadioSettingValueInteger( + 0, 199, self._memobj.settings.priority_chan)) + cfg_s.append(rs) + rs = RadioSetting("power_save", "Power save", + RadioSettingValueBoolean( + self._memobj.settings.power_save)) + cfg_s.append(rs) + options = ["Off", "Scan", "Lamp", "SOS", "Radio"] + rs = RadioSetting("pf1_function", "PF1 Function select", + RadioSettingValueList( + options, + options[self._memobj.settings.pf1_function])) + cfg_s.append(rs) + options = ["Off", "Begin", "End", "Both"] + rs = RadioSetting("roger_beep", "Roger beep select", + RadioSettingValueList( + options, + options[self._memobj.settings.roger_beep])) + cfg_s.append(rs) + options = ["%s" % x for x in range(15, 615, 15)] + transmit_time_out = options[self._memobj.settings.transmit_time_out] + rs = RadioSetting("transmit_time_out", "TX Time-out Timer", + RadioSettingValueList( + options, transmit_time_out)) + cfg_s.append(rs) + rs = RadioSetting("tx_time_out_alert", "TX Time-out Alert", + RadioSettingValueInteger( + 0, 10, self._memobj.settings.tx_time_out_alert)) + cfg_s.append(rs) + rs = RadioSetting("vox", "Vox", + RadioSettingValueInteger( + 0, 10, self._memobj.settings.vox)) + cfg_s.append(rs) + options = ["Off", "Chinese", "English"] + rs = RadioSetting("voice", "Voice", + RadioSettingValueList( + options, options[self._memobj.settings.voice])) + cfg_s.append(rs) + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean( + self._memobj.settings.beep)) + cfg_s.append(rs) + rs = RadioSetting("ani_id_enable", "ANI id enable", + RadioSettingValueBoolean( + self._memobj.settings.ani_id_enable)) + cfg_s.append(rs) + rs = RadioSetting("ani_id_tx_delay", "ANI id tx delay", + RadioSettingValueInteger( + 0, 30, self._memobj.settings.ani_id_tx_delay)) + cfg_s.append(rs) + options = ["Off", "Key", "ANI", "Key+ANI"] + rs = RadioSetting("ani_id_sidetone", "ANI id sidetone", + RadioSettingValueList( + options, + options[self._memobj.settings.ani_id_sidetone])) + cfg_s.append(rs) + options = ["Time", "Carrier", "Search"] + rs = RadioSetting("scan_mode", "Scan mode", + RadioSettingValueList( + options, + options[self._memobj.settings.scan_mode])) + cfg_s.append(rs) + rs = RadioSetting("kbd_lock", "Keyboard lock", + RadioSettingValueBoolean( + self._memobj.settings.kbd_lock)) + cfg_s.append(rs) + rs = RadioSetting("auto_lock_kbd", "Auto lock keyboard", + RadioSettingValueBoolean( + self._memobj.settings.auto_lock_kbd)) + cfg_s.append(rs) + rs = RadioSetting("auto_backlight", "Auto backlight", + RadioSettingValueBoolean( + self._memobj.settings.auto_backlight)) + cfg_s.append(rs) + options = ["CH A", "CH B"] + rs = RadioSetting("sos_ch", "SOS CH", + RadioSettingValueList( + options, + options[self._memobj.settings.sos_ch])) + cfg_s.append(rs) + rs = RadioSetting("stopwatch", "Stopwatch", + RadioSettingValueBoolean( + self._memobj.settings.stopwatch)) + cfg_s.append(rs) + rs = RadioSetting("dual_band_receive", "Dual band receive", + RadioSettingValueBoolean( + self._memobj.settings.dual_band_receive)) + cfg_s.append(rs) + options = ["VFO A", "VFO B"] + rs = RadioSetting("current_vfo", "Current VFO", + RadioSettingValueList( + options, + options[self._memobj.settings.current_vfo])) + cfg_s.append(rs) + + options = ["Dual", "Single"] + rs = RadioSetting("sd_available", "Single/Dual Band", + RadioSettingValueList( + options, + options[self._memobj.settings.sd_available])) + cfg_s.append(rs) + + _pwd = self._memobj.settings.mode_password + rs = RadioSetting("mode_password", "Mode password (000000 disabled)", + RadioSettingValueInteger(0, 9, _pwd[0]), + RadioSettingValueInteger(0, 9, _pwd[1]), + RadioSettingValueInteger(0, 9, _pwd[2]), + RadioSettingValueInteger(0, 9, _pwd[3]), + RadioSettingValueInteger(0, 9, _pwd[4]), + RadioSettingValueInteger(0, 9, _pwd[5])) + cfg_s.append(rs) + _pwd = self._memobj.settings.reset_password + rs = RadioSetting("reset_password", "Reset password (000000 disabled)", + RadioSettingValueInteger(0, 9, _pwd[0]), + RadioSettingValueInteger(0, 9, _pwd[1]), + RadioSettingValueInteger(0, 9, _pwd[2]), + RadioSettingValueInteger(0, 9, _pwd[3]), + RadioSettingValueInteger(0, 9, _pwd[4]), + RadioSettingValueInteger(0, 9, _pwd[5])) + cfg_s.append(rs) + + dtmfchars = "0123456789 *#ABCD" + _codeobj = self._memobj.settings.ani_id_content + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 6, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("settings.ani_id_content", "PTT-ID Code", val) + + def apply_ani_id(setting, obj): + value = [] + for j in range(0, 6): + try: + value.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + value.append(0xFF) + obj.ani_id_content = value + rs.set_apply_callback(apply_ani_id, self._memobj.settings) + cfg_s.append(rs) + + for i in range(0, 9): + if self._memobj.fm_presets_0[i] != 0xFFFF: + used = True + preset = self._memobj.fm_presets_0[i] / 10.0 + 76 + else: + used = False + preset = 76 + rs = RadioSetting("fm_presets_0_%1i" % i, + "Team 1 Location %i" % (i + 1), + RadioSettingValueBoolean(used), + RadioSettingValueFloat(76, 108, preset, 0.1, 1)) + fm_preset.append(rs) + for i in range(0, 9): + if self._memobj.fm_presets_1[i] != 0xFFFF: + used = True + preset = self._memobj.fm_presets_1[i] / 10.0 + 76 + else: + used = False + preset = 76 + rs = RadioSetting("fm_presets_1_%1i" % i, + "Team 2 Location %i" % (i + 1), + RadioSettingValueBoolean(used), + RadioSettingValueFloat(76, 108, preset, 0.1, 1)) + fm_preset.append(rs) + + return group + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "freq_ranges": + self._set_freq_settings(element) + elif element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + continue + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + obj = self._memobj + for element in settings: + try: + (bank, index) = \ + (int(a) for a in element.get_name().split("_")[-2:]) + val = element.value + if val[0].get_value(): + value = int(val[1].get_value()*10-760) + else: + value = 0xffff + LOG.debug("Setting fm_presets_%1i[%1i] = %s" % + (bank, index, value)) + if bank == 0: + setting = self._memobj.fm_presets_0 + else: + setting = self._memobj.fm_presets_1 + setting[index] = value + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _set_freq_settings(self, settings): + for element in settings: + try: + setattr(self._memobj.freq_ranges, + element.get_name(), + encode_freq(int(element.value))) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + tpol = False + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + rpol = False + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + # always set it even if no dtcs is used + mem.dtcs_polarity = "%s%s" % (tpol or "N", rpol or "N") + + LOG.debug("Got TX %s (%i) RX %s (%i)" % + (txmode, _mem.tx_tone, rxmode, _mem.rx_tone)) + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.tx_freq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + _nam = self._memobj.names[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw() == ("\xff" * 16): + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + if _mem.splitdup: + mem.duplex = "split" + elif self._is_txinh(_mem): + mem.duplex = "off" + elif int(_mem.rx_freq) < int(_mem.tx_freq): + mem.duplex = "+" + elif int(_mem.rx_freq) > int(_mem.tx_freq): + mem.duplex = "-" + + if mem.duplex == "" or mem.duplex == "off": + mem.offset = 0 + elif mem.duplex == "split": + mem.offset = int(_mem.tx_freq) * 10 + else: + mem.offset = abs(int(_mem.tx_freq) - int(_mem.rx_freq)) * 10 + + if not _mem.skip: + mem.skip = "S" + if not _mem.iswide: + mem.mode = "NFM" + + self._get_tone(_mem, mem) + + mem.power = self.POWER_LEVELS[not _mem.power_high] + + for i in _nam.name: + if i == 0xFF: + break + mem.name += self.CHARSET[i] + + mem.extra = RadioSettingGroup("extra", "Extra") + bcl = RadioSetting("bcl", "BCL", + RadioSettingValueBoolean(bool(_mem.bcl))) + bcl.set_doc("Busy Channel Lockout") + mem.extra.append(bcl) + + options = ["NFM", "FM"] + iswidex = RadioSetting("iswidex", "Mode TX(KG-UV6X)", + RadioSettingValueList( + options, options[_mem.iswidex])) + iswidex.set_doc("Mode TX") + mem.extra.append(iswidex) + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + rx_mode = tx_mode = None + rx_tone = tx_tone = 0xFFFF + + if mem.tmode == "Tone": + tx_mode = "Tone" + rx_mode = None + tx_tone = int(mem.rtone * 10) + elif mem.tmode == "TSQL": + rx_mode = tx_mode = "Tone" + rx_tone = tx_tone = int(mem.ctone * 10) + elif mem.tmode == "DTCS": + tx_mode = rx_mode = "DTCS" + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + rx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[1]) + elif mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + if tx_mode == "DTCS": + tx_tone = _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) + elif tx_mode == "Tone": + tx_tone = int(mem.rtone * 10) + if rx_mode == "DTCS": + rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode == "Tone": + rx_tone = int(mem.ctone * 10) + + _mem.rx_tone = rx_tone + _mem.tx_tone = tx_tone + + LOG.debug("Set TX %s (%i) RX %s (%i)" % + (tx_mode, _mem.tx_tone, rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + _nam = self._memobj.names[mem.number - 1] + + if mem.empty: + wipe_memory(_mem, "\xFF") + return + + if _mem.get_raw() == ("\xFF" * 16): + wipe_memory(_mem, "\x00") + + _mem.rx_freq = int(mem.freq / 10) + if mem.duplex == "split": + _mem.tx_freq = int(mem.offset / 10) + elif mem.duplex == "off": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.tx_freq = int(mem.freq / 10) + int(mem.offset / 10) + elif mem.duplex == "-": + _mem.tx_freq = int(mem.freq / 10) - int(mem.offset / 10) + else: + _mem.tx_freq = int(mem.freq / 10) + _mem.splitdup = mem.duplex == "split" + _mem.skip = mem.skip != "S" + _mem.iswide = mem.mode != "NFM" + + self._set_tone(mem, _mem) + + if mem.power: + _mem.power_high = not self.POWER_LEVELS.index(mem.power) + else: + _mem.power_high = True + + _nam.name = [0xFF] * 6 + for i in range(0, len(mem.name)): + try: + _nam.name[i] = self.CHARSET.index(mem.name[i]) + except IndexError: + raise Exception("Character `%s' not supported") + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + @classmethod + def match_model(cls, filedata, filename): + # New-style image (CHIRP 0.1.12) + if len(filedata) == 8192 and \ + filedata[0x60:0x64] != "2009" and \ + filedata[0x170:0x173] != "LX-" and \ + filedata[0x1f77:0x1f7d] == "\xff\xff\xff\xff\xff\xff" and \ + filedata[0x0d70:0x0d80] == "\xff\xff\xff\xff\xff\xff\xff\xff" \ + "\xff\xff\xff\xff\xff\xff\xff\xff": + # those areas are (seems to be) unused + return True + # Old-style image (CHIRP 0.1.11) + if len(filedata) == 8200 and \ + filedata[0:4] == "\x01\x00\x00\x00": + return True + return False + + +@directory.register +class KGUV6DRadio(KGUVD1PRadio): + """Wouxun KG-UV6 (D and X variants)""" + MODEL = "KG-UV6" + + _querymodel = ("HiWXUVD1\x02", "HiKGUVD1\x02") + + _MEM_FORMAT = """ + #seekto 0x0010; + struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 _3_unknown_1:4, + bcl:1, + _3_unknown_2:3; + u8 splitdup:1, + skip:1, + power_high:1, + iswide:1, + _2_unknown_2:4; + u8 pad; + u8 _0_unknown_1:3, + iswidex:1, + _0_unknown_2:4; + } memory[199]; + + #seekto 0x0F00; + struct { + char welcome1[6]; + char welcome2[6]; + char single_band[6]; + } strings; + + #seekto 0x0F20; + struct { + u8 unknown_flag_01:6, + vfo_b_ch_disp:2; + u8 unknown_flag_02:5, + vfo_a_fr_step:3; + u8 unknown_flag_03:4, + vfo_a_squelch:4; + u8 unknown_flag_04:7, + power_save:1; + u8 unknown_flag_05:5, + pf2_function:3; + u8 unknown_flag_06:6, + roger_beep:2; + u8 unknown_flag_07:2, + transmit_time_out:6; + u8 unknown_flag_08:4, + vox:4; + u8 unknown_1[4]; + u8 unknown_flag_09:6, + voice:2; + u8 unknown_flag_10:7, + beep:1; + u8 unknown_flag_11:7, + ani_id_enable:1; + u8 unknown_2[2]; + u8 unknown_flag_12:5, + vfo_b_fr_step:3; + u8 unknown_3[1]; + u8 unknown_flag_13:3, + ani_id_tx_delay:5; + u8 unknown_4[1]; + u8 unknown_flag_14:6, + ani_id_sidetone:2; + u8 unknown_flag_15:4, + tx_time_out_alert:4; + u8 unknown_flag_16:6, + vfo_a_ch_disp:2; + u8 unknown_flag_15:6, + scan_mode:2; + u8 unknown_flag_16:7, + kbd_lock:1; + u8 unknown_flag_17:6, + ponmsg:2; + u8 unknown_flag_18:5, + pf1_function:3; + u8 unknown_5[1]; + u8 unknown_flag_19:7, + auto_backlight:1; + u8 unknown_flag_20:7, + sos_ch:1; + u8 unknown_6; + u8 sd_available; + u8 unknown_flag_21:7, + auto_lock_kbd:1; + u8 unknown_flag_22:4, + vfo_b_squelch:4; + u8 unknown_7[1]; + u8 unknown_flag_23:7, + stopwatch:1; + u8 vfo_a_cur_chan; + u8 unknown_flag_24:7, + dual_band_receive:1; + u8 current_vfo:1, + unknown_flag_24:7; + u8 unknown_8[2]; + u8 mode_password[6]; + u8 reset_password[6]; + u8 ani_id_content[6]; + u8 unknown_flag_25:7, + menu_available:1; + u8 unknown_9[1]; + u8 priority_chan; + u8 vfo_b_cur_chan; + } settings; + + #seekto 0x0f60; + struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 _3_unknown_3:4, + bcl:1, + _3_unknown_4:3; + u8 splitdup:1, + _2_unknown_3:1, + power_high:1, + iswide:1, + _2_unknown_4:4; + u8 pad[2]; + } vfo_settings[2]; + + #seekto 0x0f82; + u16 fm_presets_0[9]; + + #seekto 0x0ff0; + struct { + u16 vhf_rx_start; + u16 vhf_rx_stop; + u16 uhf_rx_start; + u16 uhf_rx_stop; + u16 vhf_tx_start; + u16 vhf_tx_stop; + u16 uhf_tx_start; + u16 uhf_tx_stop; + } freq_ranges; + + #seekto 0x1010; + struct { + u8 name[6]; + u8 pad[10]; + } names[199]; + + #seekto 0x1f60; + struct { + u8 unknown_flag_26:6, + tx_offset_dir:2; + u8 tx_offset[6]; + u8 pad[9]; + } vfo_offset[2]; + + #seekto 0x1f82; + u16 fm_presets_1[9]; + """ + + def get_features(self): + rf = KGUVD1PRadio.get_features(self) + rf.memory_bounds = (1, 199) + rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, + 100.0] + return rf + + def get_settings(self): + freq_ranges = RadioSettingGroup("freq_ranges", "Freq Ranges") + fm_preset = RadioSettingGroup("fm_preset", "FM Presets") + cfg_s = RadioSettingGroup("cfg_settings", "Configuration Settings") + group = RadioSettings(cfg_s, freq_ranges, fm_preset) + + rs = RadioSetting("menu_available", "Menu Available", + RadioSettingValueBoolean( + self._memobj.settings.menu_available)) + cfg_s.append(rs) + + rs = RadioSetting("vhf_rx_start", "VHF RX Lower Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.vhf_rx_start))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_rx_stop", "VHF RX Upper Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.vhf_rx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_rx_start", "UHF RX Lower Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.uhf_rx_start))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_rx_stop", "UHF RX Upper Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.uhf_rx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_tx_start", "VHF TX Lower Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.vhf_tx_start))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_tx_stop", "VHF TX Upper Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.vhf_tx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_tx_start", "UHF TX Lower Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.uhf_tx_start))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_tx_stop", "UHF TX Upper Limit (MHz)", + RadioSettingValueInteger( + 1, 1000, decode_freq( + self._memobj.freq_ranges.uhf_tx_stop))) + freq_ranges.append(rs) + + # tell the decoded ranges to UI + freq_ranges = self._memobj.freq_ranges + self.valid_freq = \ + [(decode_freq(freq_ranges.vhf_rx_start) * 1000000, + (decode_freq(freq_ranges.vhf_rx_stop) + 1) * 1000000), + (decode_freq(freq_ranges.uhf_rx_start) * 1000000, + (decode_freq(freq_ranges.uhf_rx_stop) + 1) * 1000000)] + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + # add some radio specific settings + options = ["Off", "Welcome", "V bat", "N/A(KG-UV6X)"] + rs = RadioSetting("ponmsg", "Poweron message", + RadioSettingValueList( + options, options[self._memobj.settings.ponmsg])) + cfg_s.append(rs) + rs = RadioSetting("strings.welcome1", "Power-On Message 1", + RadioSettingValueString( + 0, 6, _filter(self._memobj.strings.welcome1))) + cfg_s.append(rs) + rs = RadioSetting("strings.welcome2", "Power-On Message 2", + RadioSettingValueString( + 0, 6, _filter(self._memobj.strings.welcome2))) + cfg_s.append(rs) + rs = RadioSetting("strings.single_band", "Single Band Message", + RadioSettingValueString( + 0, 6, _filter(self._memobj.strings.single_band))) + cfg_s.append(rs) + options = ["Channel", "ch/freq", "Name", "VFO"] + rs = RadioSetting("vfo_a_ch_disp", "VFO A Channel disp mode", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_a_ch_disp])) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_ch_disp", "VFO B Channel disp mode", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_b_ch_disp])) + cfg_s.append(rs) + options = \ + ["2.5", "5.0", "6.25", "10.0", "12.5", "25.0", "50.0", "100.0"] + rs = RadioSetting("vfo_a_fr_step", "VFO A Frequency Step", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_a_fr_step])) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_fr_step", "VFO B Frequency Step", + RadioSettingValueList( + options, + options[self._memobj.settings.vfo_b_fr_step])) + cfg_s.append(rs) + rs = RadioSetting("vfo_a_squelch", "VFO A Squelch", + RadioSettingValueInteger( + 0, 9, self._memobj.settings.vfo_a_squelch)) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_squelch", "VFO B Squelch", + RadioSettingValueInteger( + 0, 9, self._memobj.settings.vfo_b_squelch)) + cfg_s.append(rs) + rs = RadioSetting("vfo_a_cur_chan", "VFO A current channel", + RadioSettingValueInteger( + 1, 199, self._memobj.settings.vfo_a_cur_chan)) + cfg_s.append(rs) + rs = RadioSetting("vfo_b_cur_chan", "VFO B current channel", + RadioSettingValueInteger( + 1, 199, self._memobj.settings.vfo_b_cur_chan)) + cfg_s.append(rs) + rs = RadioSetting("priority_chan", "Priority channel", + RadioSettingValueInteger( + 0, 199, self._memobj.settings.priority_chan)) + cfg_s.append(rs) + rs = RadioSetting("power_save", "Power save", + RadioSettingValueBoolean( + self._memobj.settings.power_save)) + cfg_s.append(rs) + options = ["Off", "Scan", "Lamp", "SOS", "Radio"] + rs = RadioSetting("pf1_function", "PF1 Function select", + RadioSettingValueList( + options, + options[self._memobj.settings.pf1_function])) + cfg_s.append(rs) + options = ["Off", "Radio", "fr/ch", "Rpt", "Stopwatch", "Lamp", "SOS"] + rs = RadioSetting("pf2_function", "PF2 Function select", + RadioSettingValueList( + options, + options[self._memobj.settings.pf2_function])) + cfg_s.append(rs) + options = ["Off", "Begin", "End", "Both"] + rs = RadioSetting("roger_beep", "Roger beep select", + RadioSettingValueList( + options, + options[self._memobj.settings.roger_beep])) + cfg_s.append(rs) + options = ["%s" % x for x in range(15, 615, 15)] + transmit_time_out = options[self._memobj.settings.transmit_time_out] + rs = RadioSetting("transmit_time_out", "TX Time-out Timer", + RadioSettingValueList( + options, transmit_time_out)) + cfg_s.append(rs) + rs = RadioSetting("tx_time_out_alert", "TX Time-out Alert", + RadioSettingValueInteger( + 0, 10, self._memobj.settings.tx_time_out_alert)) + cfg_s.append(rs) + rs = RadioSetting("vox", "Vox", + RadioSettingValueInteger( + 0, 10, self._memobj.settings.vox)) + cfg_s.append(rs) + options = ["Off", "Chinese", "English"] + rs = RadioSetting("voice", "Voice", + RadioSettingValueList( + options, options[self._memobj.settings.voice])) + cfg_s.append(rs) + rs = RadioSetting("beep", "Beep", + RadioSettingValueBoolean( + self._memobj.settings.beep)) + cfg_s.append(rs) + rs = RadioSetting("ani_id_enable", "ANI id enable", + RadioSettingValueBoolean( + self._memobj.settings.ani_id_enable)) + cfg_s.append(rs) + rs = RadioSetting("ani_id_tx_delay", "ANI id tx delay", + RadioSettingValueInteger( + 0, 30, self._memobj.settings.ani_id_tx_delay)) + cfg_s.append(rs) + options = ["Off", "Key", "ANI", "Key+ANI"] + rs = RadioSetting("ani_id_sidetone", "ANI id sidetone", + RadioSettingValueList( + options, + options[self._memobj.settings.ani_id_sidetone])) + cfg_s.append(rs) + options = ["Time", "Carrier", "Search"] + rs = RadioSetting("scan_mode", "Scan mode", + RadioSettingValueList( + options, + options[self._memobj.settings.scan_mode])) + cfg_s.append(rs) + rs = RadioSetting("kbd_lock", "Keyboard lock", + RadioSettingValueBoolean( + self._memobj.settings.kbd_lock)) + cfg_s.append(rs) + rs = RadioSetting("auto_lock_kbd", "Auto lock keyboard", + RadioSettingValueBoolean( + self._memobj.settings.auto_lock_kbd)) + cfg_s.append(rs) + rs = RadioSetting("auto_backlight", "Auto backlight", + RadioSettingValueBoolean( + self._memobj.settings.auto_backlight)) + cfg_s.append(rs) + options = ["CH A", "CH B"] + rs = RadioSetting("sos_ch", "SOS CH", + RadioSettingValueList( + options, options[self._memobj.settings.sos_ch])) + cfg_s.append(rs) + rs = RadioSetting("stopwatch", "Stopwatch", + RadioSettingValueBoolean( + self._memobj.settings.stopwatch)) + cfg_s.append(rs) + rs = RadioSetting("dual_band_receive", "Dual band receive", + RadioSettingValueBoolean( + self._memobj.settings.dual_band_receive)) + cfg_s.append(rs) + options = ["VFO A", "VFO B"] + rs = RadioSetting("current_vfo", "Current VFO", + RadioSettingValueList( + options, + options[self._memobj.settings.current_vfo])) + cfg_s.append(rs) + + options = ["Dual", "Single"] + rs = RadioSetting("sd_available", "Single/Dual Band", + RadioSettingValueList( + options, + options[self._memobj.settings.sd_available])) + cfg_s.append(rs) + + _pwd = self._memobj.settings.mode_password + rs = RadioSetting("mode_password", "Mode password (000000 disabled)", + RadioSettingValueInteger(0, 9, _pwd[0]), + RadioSettingValueInteger(0, 9, _pwd[1]), + RadioSettingValueInteger(0, 9, _pwd[2]), + RadioSettingValueInteger(0, 9, _pwd[3]), + RadioSettingValueInteger(0, 9, _pwd[4]), + RadioSettingValueInteger(0, 9, _pwd[5])) + cfg_s.append(rs) + _pwd = self._memobj.settings.reset_password + rs = RadioSetting("reset_password", "Reset password (000000 disabled)", + RadioSettingValueInteger(0, 9, _pwd[0]), + RadioSettingValueInteger(0, 9, _pwd[1]), + RadioSettingValueInteger(0, 9, _pwd[2]), + RadioSettingValueInteger(0, 9, _pwd[3]), + RadioSettingValueInteger(0, 9, _pwd[4]), + RadioSettingValueInteger(0, 9, _pwd[5])) + cfg_s.append(rs) + + dtmfchars = "0123456789 *#ABCD" + _codeobj = self._memobj.settings.ani_id_content + _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F]) + val = RadioSettingValueString(0, 6, _code, False) + val.set_charset(dtmfchars) + rs = RadioSetting("settings.ani_id_content", "ANI Code", val) + + def apply_ani_id(setting, obj): + value = [] + for j in range(0, 6): + try: + value.append(dtmfchars.index(str(setting.value)[j])) + except IndexError: + value.append(0xFF) + obj.ani_id_content = value + rs.set_apply_callback(apply_ani_id, self._memobj.settings) + cfg_s.append(rs) + + for i in range(0, 9): + if self._memobj.fm_presets_0[i] != 0xFFFF: + used = True + preset = self._memobj.fm_presets_0[i]/10.0+76 + else: + used = False + preset = 76 + rs = RadioSetting("fm_presets_0_%1i" % i, + "Team 1 Location %i" % (i+1), + RadioSettingValueBoolean(used), + RadioSettingValueFloat(76, 108, preset, 0.1, 1)) + fm_preset.append(rs) + for i in range(0, 9): + if self._memobj.fm_presets_1[i] != 0xFFFF: + used = True + preset = self._memobj.fm_presets_1[i]/10.0+76 + else: + used = False + preset = 76 + rs = RadioSetting("fm_presets_1_%1i" % i, + "Team 2 Location %i" % (i+1), + RadioSettingValueBoolean(used), + RadioSettingValueFloat(76, 108, preset, 0.1, 1)) + fm_preset.append(rs) + + return group + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + if element.get_name() == "freq_ranges": + self._set_freq_settings(element) + elif element.get_name() == "fm_preset": + self._set_fm_preset(element) + else: + self.set_settings(element) + else: + try: + if "." in element.get_name(): + bits = element.get_name().split(".") + obj = self._memobj + for bit in bits[:-1]: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = self._memobj.settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + else: + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def _set_fm_preset(self, settings): + obj = self._memobj + for element in settings: + try: + (bank, index) = \ + (int(a) for a in element.get_name().split("_")[-2:]) + val = element.value + if val[0].get_value(): + value = int(val[1].get_value()*10-760) + else: + value = 0xffff + LOG.debug("Setting fm_presets_%1i[%1i] = %s" % + (bank, index, value)) + if bank == 0: + setting = self._memobj.fm_presets_0 + else: + setting = self._memobj.fm_presets_1 + setting[index] = value + except Exception, e: + LOG.debug(element.get_name()) + raise + + @classmethod + def match_model(cls, filedata, filename): + if len(filedata) == 8192 and \ + filedata[0x1f77:0x1f7d] == "WELCOM": + return True + return False + + +@directory.register +class KG816Radio(KGUVD1PRadio, chirp_common.ExperimentalRadio): + """Wouxun KG-816""" + MODEL = "KG-816" + + _querymodel = "HiWOUXUN\x02" + + _MEM_FORMAT = """ + #seekto 0x0010; + struct { + lbcd rx_freq[4]; + lbcd tx_freq[4]; + ul16 rx_tone; + ul16 tx_tone; + u8 _3_unknown_1:4, + bcl:1, + _3_unknown_2:3; + u8 splitdup:1, + skip:1, + power_high:1, + iswide:1, + _2_unknown_2:4; + u8 unknown; + u8 _0_unknown_1:3, + iswidex:1, + _0_unknown_2:4; + } memory[199]; + + #seekto 0x0d70; + struct { + u16 vhf_rx_start; + u16 vhf_rx_stop; + u16 uhf_rx_start; + u16 uhf_rx_stop; + u16 vhf_tx_start; + u16 vhf_tx_stop; + u16 uhf_tx_start; + u16 uhf_tx_stop; + } freq_ranges; + + #seekto 0x1010; + struct { + u8 name[6]; + u8 pad[10]; + } names[199]; + """ + + @classmethod + def get_experimental_warning(cls): + return ('We have not that much information on this model ' + 'up to now we only know it has the same memory ' + 'organization of KGUVD1 but uses 199 memories. ' + 'it has been reported to work but ' + 'proceed at your own risk!') + + def get_features(self): + rf = KGUVD1PRadio.get_features(self) + rf.memory_bounds = (1, 199) # this is the only known difference + return rf + + def get_settings(self): + freq_ranges = RadioSettingGroup("freq_ranges", + "Freq Ranges (read only)") + group = RadioSettings(freq_ranges) + + rs = RadioSetting("vhf_rx_start", "vhf rx start", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.vhf_rx_start))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_rx_stop", "vhf rx stop", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.vhf_rx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_rx_start", "uhf rx start", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.uhf_rx_start))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_rx_stop", "uhf rx stop", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.uhf_rx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_tx_start", "vhf tx start", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.vhf_tx_start))) + freq_ranges.append(rs) + rs = RadioSetting("vhf_tx_stop", "vhf tx stop", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.vhf_tx_stop))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_tx_start", "uhf tx start", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.uhf_tx_start))) + freq_ranges.append(rs) + rs = RadioSetting("uhf_tx_stop", "uhf tx stop", + RadioSettingValueInteger( + 66, 520, decode_freq( + self._memobj.freq_ranges.uhf_tx_stop))) + freq_ranges.append(rs) + + # tell the decoded ranges to UI + self.valid_freq = \ + [(decode_freq(self._memobj.freq_ranges.vhf_rx_start) * 1000000, + (decode_freq(self._memobj.freq_ranges.vhf_rx_stop)+1) * 1000000)] + + return group + + @classmethod + def match_model(cls, filedata, filename): + if len(filedata) == 8192 and \ + filedata[0x60:0x64] != "2009" and \ + filedata[0x170:0x173] != "LX-" and \ + filedata[0xF7E:0xF80] != "\x01\xE2" and \ + filedata[0x1f77:0x1f7d] == "\xff\xff\xff\xff\xff\xff" and \ + filedata[0x0d70:0x0d80] != "\xff\xff\xff\xff\xff\xff\xff\xff" \ + "\xff\xff\xff\xff\xff\xff\xff\xff": + return True + return False + + +@directory.register +class KG818Radio(KG816Radio): + """Wouxun KG-818""" + MODEL = "KG-818" + + @classmethod + def match_model(cls, filedata, filename): + return False diff --git a/chirp/drivers/wouxun_common.py b/chirp/drivers/wouxun_common.py new file mode 100644 index 0000000..ec65d38 --- /dev/null +++ b/chirp/drivers/wouxun_common.py @@ -0,0 +1,80 @@ +# +# Copyright 2012 Filippi Marco +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""vcommon function for wouxun (or similar) radios""" + +import struct +import os +import logging +from chirp import util, chirp_common, memmap + +LOG = logging.getLogger(__name__) + + +def wipe_memory(_mem, byte): + """Cleanup a memory""" + _mem.set_raw(byte * (_mem.size() / 8)) + + +def do_download(radio, start, end, blocksize): + """Initiate a download of @radio between @start and @end""" + image = "" + for i in range(start, end, blocksize): + cmd = struct.pack(">cHb", "R", i, blocksize) + LOG.debug(util.hexprint(cmd)) + radio.pipe.write(cmd) + length = len(cmd) + blocksize + resp = radio.pipe.read(length) + if len(resp) != (len(cmd) + blocksize): + LOG.debug(util.hexprint(resp)) + raise Exception("Failed to read full block (%i!=%i)" % + (len(resp), len(cmd) + blocksize)) + + radio.pipe.write("\x06") + radio.pipe.read(1) + image += resp[4:] + + if radio.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning from radio" + radio.status_fn(status) + + return memmap.MemoryMap(image) + + +def do_upload(radio, start, end, blocksize): + """Initiate an upload of @radio between @start and @end""" + ptr = start + for i in range(start, end, blocksize): + cmd = struct.pack(">cHb", "W", i, blocksize) + chunk = radio.get_mmap()[ptr:ptr+blocksize] + ptr += blocksize + radio.pipe.write(cmd + chunk) + LOG.debug(util.hexprint(cmd + chunk)) + + ack = radio.pipe.read(1) + if not ack == "\x06": + raise Exception("Radio did not ack block %i" % ptr) + # radio.pipe.write(ack) + + if radio.status_fn: + status = chirp_common.Status() + status.cur = i + status.max = end + status.msg = "Cloning to radio" + radio.status_fn(status) diff --git a/chirp/drivers/yaesu_clone.py b/chirp/drivers/yaesu_clone.py new file mode 100644 index 0000000..2fbad4d --- /dev/null +++ b/chirp/drivers/yaesu_clone.py @@ -0,0 +1,286 @@ +# Copyright 2010 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes +import time +import os +import logging +from textwrap import dedent + +from chirp import bitwise +from chirp import chirp_common, util, memmap, errors + +LOG = logging.getLogger(__name__) + +CMD_ACK = 0x06 + + +def _safe_read(pipe, count): + buf = bytes(b"") + first = True + for _i in range(0, 60): + buf += pipe.read(count - len(buf)) + # LOG.debug("safe_read: %i/%i\n" % (len(buf), count)) + if buf: + if first and buf[0] == CMD_ACK: + # LOG.debug("Chewed an ack") + buf = buf[1:] # Chew an echo'd ack if using a 2-pin cable + first = False + if len(buf) == count: + break + LOG.debug(util.hexprint(buf)) + return buf + + +def _chunk_read(pipe, count, status_fn): + timer = time.time() + block = 32 + data = bytes(b"") + while len(data) < count: + # Don't read past the end of our block if we're not on a 32-byte + # boundary + chunk_size = min(block, count - len(data)) + chunk = pipe.read(chunk_size) + if chunk: + timer = time.time() + data += chunk + if data[0] == CMD_ACK: + data = data[1:] # Chew an echo'd ack if using a 2-pin cable + # LOG.debug("Chewed an ack") + if time.time() - timer > 2: + # It's been two seconds since we last saw data from the radio, + # so it's time to give up. + raise errors.RadioError("Timed out reading from radio") + status = chirp_common.Status() + status.msg = "Cloning from radio" + status.max = count + status.cur = len(data) + status_fn(status) + LOG.debug("Read %i/%i" % (len(data), count)) + return data + + +def __clone_in(radio): + pipe = radio.pipe + + status = chirp_common.Status() + status.msg = "Cloning from radio" + status.max = radio.get_memsize() + + start = time.time() + + data = bytes(b"") + blocks = 0 + for block in radio._block_lengths: + blocks += 1 + if blocks == len(radio._block_lengths): + chunk = _chunk_read(pipe, block, radio.status_fn) + else: + chunk = _safe_read(pipe, block) + pipe.write(bytes([CMD_ACK])) + if not chunk: + raise errors.RadioError("No response from radio") + if radio.status_fn: + status.cur = len(data) + radio.status_fn(status) + data += chunk + + if len(data) != radio.get_memsize(): + raise errors.RadioError("Received incomplete image from radio") + + LOG.debug("Clone completed in %i seconds" % (time.time() - start)) + + return memmap.MemoryMapBytes(data) + + +def _clone_in(radio): + try: + return __clone_in(radio) + except Exception as e: + raise errors.RadioError("Failed to communicate with the radio: %s" % e) + + +def _chunk_write(pipe, data, status_fn, block): + delay = 0.03 + count = 0 + for i in range(0, len(data), block): + chunk = data[i:i+block] + pipe.write(chunk) + count += len(chunk) + LOG.debug("@_chunk_write, count: %i, blocksize: %i" % (count, block)) + time.sleep(delay) + + status = chirp_common.Status() + status.msg = "Cloning to radio" + status.max = len(data) + status.cur = count + status_fn(status) + + +def __clone_out(radio): + pipe = radio.pipe + block_lengths = radio._block_lengths + total_written = 0 + + def _status(): + status = chirp_common.Status() + status.msg = "Cloning to radio" + status.max = block_lengths[0] + block_lengths[1] + block_lengths[2] + status.cur = total_written + radio.status_fn(status) + + start = time.time() + + blocks = 0 + pos = 0 + mmap = radio.get_mmap().get_byte_compatible() + for block in radio._block_lengths: + blocks += 1 + if blocks != len(radio._block_lengths): + LOG.debug("Sending %i-%i" % (pos, pos+block)) + pipe.write(mmap[pos:pos+block]) + buf = pipe.read(1) + if buf and buf[0] != CMD_ACK: + buf = pipe.read(block) + if not buf or buf[-1] != CMD_ACK: + raise Exception("Radio did not ack block %i" % blocks) + else: + _chunk_write(pipe, mmap[pos:], + radio.status_fn, radio._block_size) + pos += block + + pipe.read(pos) # Chew the echo if using a 2-pin cable + + LOG.debug("Clone completed in %i seconds" % (time.time() - start)) + + +def _clone_out(radio): + try: + return __clone_out(radio) + except Exception as e: + raise errors.RadioError("Failed to communicate with the radio: %s" % e) + + +class YaesuChecksum: + """A Yaesu Checksum Object""" + def __init__(self, start, stop, address=None): + self._start = start + self._stop = stop + if address: + self._address = address + else: + self._address = stop + 1 + + @staticmethod + def _asbytes(mmap): + if hasattr(mmap, 'get_byte_compatible'): + return mmap.get_byte_compatible() + elif isinstance(mmap, bytes): + # NOTE: this won't work for update(), but nothing should be calling + # this with a literal expecting that to work + return memmap.MemoryMapBytes(bytes(mmap)) + elif isinstance(mmap, str): + # NOTE: this won't work for update(), but nothing should be calling + # this with a literal expecting that to work + return memmap.MemoryMap( + bitwise.string_straight_encode(mmap)).get_byte_compatible() + else: + raise TypeError('Unable to convert %s to bytes' % ( + type(mmap).__name__)) + + def get_existing(self, mmap): + """Return the existing checksum in mmap""" + return self._asbytes(mmap)[self._address][0] + + def get_calculated(self, mmap): + """Return the calculated value of the checksum""" + mmap = self._asbytes(mmap) + cs = 0 + for i in range(self._start, self._stop+1): + # NOTE: mmap[i] returns a slice'd bytes, not an int like a + # bytes does + cs += mmap[i][0] + return cs % 256 + + def update(self, mmap): + """Update the checksum with the data in @mmap""" + mmap = self._asbytes(mmap) + mmap[self._address] = self.get_calculated(mmap) + + def __str__(self): + return "%04X-%04X (@%04X)" % (self._start, + self._stop, + self._address) + + +class YaesuCloneModeRadio(chirp_common.CloneModeRadio): + """Base class for all Yaesu clone-mode radios""" + _block_lengths = [8, 65536] + _block_size = 8 + + VENDOR = "Yaesu" + NEEDS_COMPAT_SERIAL = False + _model = "ABCDE" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect data cable. + 3. Prepare radio for clone. + 4. After clicking OK, press the key to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect data cable. + 3. Prepare radio for clone. + 4. Press the key to receive the image.""")) + return rp + + def _checksums(self): + """Return a list of checksum objects that need to be calculated""" + return [] + + def update_checksums(self): + """Update the radio's checksums from the current memory map""" + for checksum in self._checksums(): + checksum.update(self._mmap) + + def check_checksums(self): + """Validate the checksums stored in the memory map""" + for checksum in self._checksums(): + if checksum.get_existing(self._mmap) != \ + checksum.get_calculated(self._mmap): + raise errors.RadioError("Checksum Failed [%s]" % checksum) + LOG.debug("Checksum %s: OK" % checksum) + + def sync_in(self): + self._mmap = _clone_in(self) + self.check_checksums() + self.process_mmap() + + def sync_out(self): + self.update_checksums() + _clone_out(self) + + @classmethod + def match_model(cls, filedata, filename): + return filedata[:5] == cls._model and len(filedata) == cls._memsize + + def _wipe_memory_banks(self, mem): + """Remove @mem from all the banks it is currently in""" + bm = self.get_bank_model() + for bank in bm.get_memory_mappings(mem): + bm.remove_memory_from_mapping(mem, bank) diff --git a/chirp/elib_intl.py b/chirp/elib_intl.py new file mode 100644 index 0000000..76e3b9b --- /dev/null +++ b/chirp/elib_intl.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2007-2010 Dieter Verfaillie +# +# This file is part of elib.intl. +# +# elib.intl is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# elib.intl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with elib.intl. If not, see . + + +''' +The elib.intl module provides enhanced internationalization (I18N) +services for your Python modules and applications. + +elib.intl wraps Python's :func:`gettext` functionality and adds the +following on Microsoft Windows systems: + + - automatic detection of the current screen language (not necessarily + the same as the installation language) provided by MUI packs, + - makes sure internationalized C libraries which internally invoke + gettext() or dcgettext() can properly locate their message catalogs. + This fixes a known limitation in gettext's Windows support when using + eg. gtk.builder or gtk.glade. + +See http://www.gnu.org/software/gettext/FAQ.html#windows_setenv for more +information. + +The elib.intl module defines the following functions: +''' + +import os +import sys +import locale +import gettext + +from logging import getLogger + +__all__ = ['install', 'install_module'] +__version__ = '0.0.3' +__docformat__ = 'restructuredtext' + +logger = getLogger('elib.intl') + + +def _isofromlcid(lcid): + ''' + :param lcid: Microsoft Windows LCID + :returns: the ISO 639-1 language code for a given lcid. If there is no + ISO 639-1 language code assigned to the language specified + by lcid, the ISO 639-2 language code is returned. If the + language specified by lcid is unknown in the ISO 639-x + database, None is returned. + + More information can be found on the following websites: + - List of ISO 639-1 and ISO 639-2 language codes: + http://www.loc.gov/standards/iso639-2/ + - List of known lcid's: + http://www.microsoft.com/globaldev/reference/lcid-all.mspx + - List of known MUI packs: + http://www.microsoft.com/globaldev/reference/win2k/setup/Langid.mspx + ''' + mapping = {1078: 'af', # frikaans - South Africa + 1052: 'sq', # lbanian - Albania + 1118: 'am', # mharic - Ethiopia + 1025: 'ar', # rabic - Saudi Arabia + 5121: 'ar', # rabic - Algeria + 15361: 'ar', # rabic - Bahrain + 3073: 'ar', # rabic - Egypt + 2049: 'ar', # rabic - Iraq + 11265: 'ar', # rabic - Jordan + 13313: 'ar', # rabic - Kuwait + 12289: 'ar', # rabic - Lebanon + 4097: 'ar', # rabic - Libya + 6145: 'ar', # rabic - Morocco + 8193: 'ar', # rabic - Oman + 16385: 'ar', # rabic - Qatar + 10241: 'ar', # rabic - Syria + 7169: 'ar', # rabic - Tunisia + 14337: 'ar', # rabic - U.A.E. + 9217: 'ar', # rabic - Yemen + 1067: 'hy', # rmenian - Armenia + 1101: 'as', # ssamese + 2092: 'az', # zeri (Cyrillic) + 1068: 'az', # zeri (Latin) + 1069: 'eu', # asque + 1059: 'be', # elarusian + 1093: 'bn', # engali (India) + 2117: 'bn', # engali (Bangladesh) + 5146: 'bs', # osnian (Bosnia/Herzegovina) + 1026: 'bg', # ulgarian + 1109: 'my', # urmese + 1027: 'ca', # atalan + 1116: 'chr', # herokee - United States + 2052: 'zh', # hinese - People's Republic of China + 4100: 'zh', # hinese - Singapore + 1028: 'zh', # hinese - Taiwan + 3076: 'zh', # hinese - Hong Kong SAR + 5124: 'zh', # hinese - Macao SAR + 1050: 'hr', # roatian + 4122: 'hr', # roatian (Bosnia/Herzegovina) + 1029: 'cs', # zech + 1030: 'da', # anish + 1125: 'dv', # ivehi + 1043: 'nl', # utch - Netherlands + 2067: 'nl', # utch - Belgium + 1126: 'bin', # do + 1033: 'en', # nglish - United States + 2057: 'en', # nglish - United Kingdom + 3081: 'en', # nglish - Australia + 10249: 'en', # nglish - Belize + 4105: 'en', # nglish - Canada + 9225: 'en', # nglish - Caribbean + 15369: 'en', # nglish - Hong Kong SAR + 16393: 'en', # nglish - India + 14345: 'en', # nglish - Indonesia + 6153: 'en', # nglish - Ireland + 8201: 'en', # nglish - Jamaica + 17417: 'en', # nglish - Malaysia + 5129: 'en', # nglish - New Zealand + 13321: 'en', # nglish - Philippines + 18441: 'en', # nglish - Singapore + 7177: 'en', # nglish - South Africa + 11273: 'en', # nglish - Trinidad + 12297: 'en', # nglish - Zimbabwe + 1061: 'et', # stonian + 1080: 'fo', # aroese + 1065: None, # ODO: Farsi + 1124: 'fil', # ilipino + 1035: 'fi', # innish + 1036: 'fr', # rench - France + 2060: 'fr', # rench - Belgium + 11276: 'fr', # rench - Cameroon + 3084: 'fr', # rench - Canada + 9228: 'fr', # rench - Democratic Rep. of Congo + 12300: 'fr', # rench - Cote d'Ivoire + 15372: 'fr', # rench - Haiti + 5132: 'fr', # rench - Luxembourg + 13324: 'fr', # rench - Mali + 6156: 'fr', # rench - Monaco + 14348: 'fr', # rench - Morocco + 58380: 'fr', # rench - North Africa + 8204: 'fr', # rench - Reunion + 10252: 'fr', # rench - Senegal + 4108: 'fr', # rench - Switzerland + 7180: 'fr', # rench - West Indies + 1122: 'fy', # risian - Netherlands + 1127: None, # ODO: Fulfulde - Nigeria + 1071: 'mk', # YRO Macedonian + 2108: 'ga', # aelic (Ireland) + 1084: 'gd', # aelic (Scotland) + 1110: 'gl', # alician + 1079: 'ka', # eorgian + 1031: 'de', # erman - Germany + 3079: 'de', # erman - Austria + 5127: 'de', # erman - Liechtenstein + 4103: 'de', # erman - Luxembourg + 2055: 'de', # erman - Switzerland + 1032: 'el', # reek + 1140: 'gn', # uarani - Paraguay + 1095: 'gu', # ujarati + 1128: 'ha', # ausa - Nigeria + 1141: 'haw', # awaiian - United States + 1037: 'he', # ebrew + 1081: 'hi', # indi + 1038: 'hu', # ungarian + 1129: None, # ODO: Ibibio - Nigeria + 1039: 'is', # celandic + 1136: 'ig', # gbo - Nigeria + 1057: 'id', # ndonesian + 1117: 'iu', # nuktitut + 1040: 'it', # talian - Italy + 2064: 'it', # talian - Switzerland + 1041: 'ja', # apanese + 1099: 'kn', # annada + 1137: 'kr', # anuri - Nigeria + 2144: 'ks', # ashmiri + 1120: 'ks', # ashmiri (Arabic) + 1087: 'kk', # azakh + 1107: 'km', # hmer + 1111: 'kok', # onkani + 1042: 'ko', # orean + 1088: 'ky', # yrgyz (Cyrillic) + 1108: 'lo', # ao + 1142: 'la', # atin + 1062: 'lv', # atvian + 1063: 'lt', # ithuanian + 1086: 'ms', # alay - Malaysia + 2110: 'ms', # alay - Brunei Darussalam + 1100: 'ml', # alayalam + 1082: 'mt', # altese + 1112: 'mni', # anipuri + 1153: 'mi', # aori - New Zealand + 1102: 'mr', # arathi + 1104: 'mn', # ongolian (Cyrillic) + 2128: 'mn', # ongolian (Mongolian) + 1121: 'ne', # epali + 2145: 'ne', # epali - India + 1044: 'no', # orwegian (Bokmᅢᆬl) + 2068: 'no', # orwegian (Nynorsk) + 1096: 'or', # riya + 1138: 'om', # romo + 1145: 'pap', # apiamentu + 1123: 'ps', # ashto + 1045: 'pl', # olish + 1046: 'pt', # ortuguese - Brazil + 2070: 'pt', # ortuguese - Portugal + 1094: 'pa', # unjabi + 2118: 'pa', # unjabi (Pakistan) + 1131: 'qu', # uecha - Bolivia + 2155: 'qu', # uecha - Ecuador + 3179: 'qu', # uecha - Peru + 1047: 'rm', # haeto-Romanic + 1048: 'ro', # omanian + 2072: 'ro', # omanian - Moldava + 1049: 'ru', # ussian + 2073: 'ru', # ussian - Moldava + 1083: 'se', # ami (Lappish) + 1103: 'sa', # anskrit + 1132: 'nso', # epedi + 3098: 'sr', # erbian (Cyrillic) + 2074: 'sr', # erbian (Latin) + 1113: 'sd', # indhi - India + 2137: 'sd', # indhi - Pakistan + 1115: 'si', # inhalese - Sri Lanka + 1051: 'sk', # lovak + 1060: 'sl', # lovenian + 1143: 'so', # omali + 1070: 'wen', # orbian + 3082: 'es', # panish - Spain (Modern Sort) + 1034: 'es', # panish - Spain (Traditional Sort) + 11274: 'es', # panish - Argentina + 16394: 'es', # panish - Bolivia + 13322: 'es', # panish - Chile + 9226: 'es', # panish - Colombia + 5130: 'es', # panish - Costa Rica + 7178: 'es', # panish - Dominican Republic + 12298: 'es', # panish - Ecuador + 17418: 'es', # panish - El Salvador + 4106: 'es', # panish - Guatemala + 18442: 'es', # panish - Honduras + 58378: 'es', # panish - Latin America + 2058: 'es', # panish - Mexico + 19466: 'es', # panish - Nicaragua + 6154: 'es', # panish - Panama + 15370: 'es', # panish - Paraguay + 10250: 'es', # panish - Peru + 20490: 'es', # panish - Puerto Rico + 21514: 'es', # panish - United States + 14346: 'es', # panish - Uruguay + 8202: 'es', # panish - Venezuela + 1072: None, # ODO: Sutu + 1089: 'sw', # wahili + 1053: 'sv', # wedish + 2077: 'sv', # wedish - Finland + 1114: 'syr', # yriac + 1064: 'tg', # ajik + 1119: None, # ODO: Tamazight (Arabic) + 2143: None, # ODO: Tamazight (Latin) + 1097: 'ta', # amil + 1092: 'tt', # atar + 1098: 'te', # elugu + 1054: 'th', # hai + 2129: 'bo', # ibetan - Bhutan + 1105: 'bo', # ibetan - People's Republic of China + 2163: 'ti', # igrigna - Eritrea + 1139: 'ti', # igrigna - Ethiopia + 1073: 'ts', # songa + 1074: 'tn', # swana + 1055: 'tr', # urkish + 1090: 'tk', # urkmen + 1152: 'ug', # ighur - China + 1058: 'uk', # krainian + 1056: 'ur', # rdu + 2080: 'ur', # rdu - India + 2115: 'uz', # zbek (Cyrillic) + 1091: 'uz', # zbek (Latin) + 1075: 've', # enda + 1066: 'vi', # ietnamese + 1106: 'cy', # elsh + 1076: 'xh', # hosa + 1144: 'ii', # i + 1085: 'yi', # iddish + 1130: 'yo', # oruba + 1077: 'zu'} # ulu + + return mapping[lcid] + + +def _getscreenlanguage(): + ''' + :returns: the ISO 639-x language code for this session. + + If the LANGUAGE environment variable is set, it's value overrides + the screen language detection. Otherwise the screen language is + determined by the currently selected Microsoft Windows MUI language + pack or the Microsoft Windows installation language. + + Works on Microsoft Windows 2000 and up. + ''' + if sys.platform == 'win32' or sys.platform == 'nt': + # Start with nothing + lang = None + + # Check the LANGUAGE environment variable + lang = os.getenv('LANGUAGE') + + if lang is None: + # Start with nothing + lcid = None + + try: + from ctypes import windll + lcid = windll.kernel32.GetUserDefaultUILanguage() + except: + logger.debug('Failed to get current screen language ' + 'with \'GetUserDefaultUILanguage\'') + finally: + if lcid is None: + lang = 'C' + else: + lang = _isofromlcid(lcid) + + logger.debug('Windows screen language is \'%s\' ' + '(lcid %s)' % (lang, lcid)) + + return lang + + +def _putenv(name, value): + ''' + :param name: environment variable name + :param value: environment variable value + + This function ensures that changes to an environment variable are + applied to each copy of the environment variables used by a process. + Starting from Python 2.4, os.environ changes only apply to the copy + Python keeps (os.environ) and are no longer automatically applied to + the other copies for the process. + + On Microsoft Windows, each process has multiple copies of the + environment variables, one managed by the OS and one managed by the + C library. We also need to take care of the fact that the C library + used by Python is not necessarily the same as the C library used by + pygtk and friends. This because the latest releases of pygtk and + friends are built with mingw32 and are thus linked against + msvcrt.dll. The official gtk+ binaries have always been built in + this way. + ''' + + if sys.platform == 'win32' or sys.platform == 'nt': + from ctypes import windll + from ctypes import cdll + from ctypes.util import find_msvcrt + + # Update Python's copy of the environment variables + os.environ[name] = value + + # Update the copy maintained by Windows (so SysInternals + # Process Explorer sees it) + try: + result = windll.kernel32.SetEnvironmentVariableW(name, value) + if result == 0: + raise Warning + except Exception: + logger.debug('Failed to set environment variable \'%s\' ' + '(\'kernel32.SetEnvironmentVariableW\')' % name) + else: + logger.debug('Set environment variable \'%s\' to \'%s\' ' + '(\'kernel32.SetEnvironmentVariableW\')' % + (name, value)) + + # Update the copy maintained by msvcrt (used by gtk+ runtime) + try: + result = cdll.msvcrt._putenv('%s=%s' % (name, value)) + if result != 0: + raise Warning + except Exception: + logger.debug('Failed to set environment variable \'%s\' ' + '(\'msvcrt._putenv\')' % name) + else: + logger.debug('Set environment variable \'%s\' to \'%s\' ' + '(\'msvcrt._putenv\')' % (name, value)) + + # Update the copy maintained by whatever c runtime is used by Python + try: + msvcrt = find_msvcrt() + msvcrtname = str(msvcrt).split('.')[0] \ + if '.' in msvcrt else str(msvcrt) + result = cdll.LoadLibrary(msvcrt)._putenv('%s=%s' % (name, value)) + if result != 0: + raise Warning + except Exception: + logger.debug('Failed to set environment variable \'%s\' ' + '(\'%s._putenv\')' % (name, msvcrtname)) + else: + logger.debug('Set environment variable \'%s\' to \'%s\' ' + '(\'%s._putenv\')' % (name, value, msvcrtname)) + + +def _dugettext(domain, message): + ''' + :param domain: translation domain + :param message: message to translate + :returns: the translated message + + Unicode version of :func:`gettext.dgettext`. + ''' + try: + t = gettext.translation(domain, gettext._localedirs.get(domain, None), + codeset=gettext._localecodesets.get(domain)) + except IOError: + return message + else: + return t.ugettext(message) + + +def _install(domain, localedir, asglobal=False): + ''' + :param domain: translation domain + :param localedir: locale directory + :param asglobal: if True, installs the function _() in Python’s + builtin namespace. Default is False + + Private function doing all the work for the :func:`elib.intl.install` and + :func:`elib.intl.install_module` functions. + ''' + # prep locale system + if asglobal: + locale.setlocale(locale.LC_ALL, '') + + # on windows systems, set the LANGUAGE environment variable + if sys.platform == 'win32' or sys.platform == 'nt': + _putenv('LANGUAGE', _getscreenlanguage()) + + # The locale module on Max OS X lacks bindtextdomain so we specifically + # test on linux2 here. See commit 4ae8b26fd569382ab66a9e844daa0e01de409ceb + if sys.platform == 'linux2': + locale.bindtextdomain(domain, localedir) + locale.bind_textdomain_codeset(domain, 'UTF-8') + locale.textdomain(domain) + + # initialize Python's gettext interface + gettext.bindtextdomain(domain, localedir) + gettext.bind_textdomain_codeset(domain, 'UTF-8') + + if asglobal: + gettext.textdomain(domain) + + # on windows systems, initialize libintl + if sys.platform == 'win32' or sys.platform == 'nt': + from ctypes import cdll + libintl = cdll.intl + libintl.bindtextdomain(domain, localedir) + libintl.bind_textdomain_codeset(domain, 'UTF-8') + + if asglobal: + libintl.textdomain(domain) + + del libintl + + +def install(domain, localedir): + ''' + :param domain: translation domain + :param localedir: locale directory + + Installs the function _() in Python’s builtin namespace, based on + domain and localedir. Codeset is always UTF-8. + + As seen below, you usually mark the strings in your application that are + candidates for translation, by wrapping them in a call to the _() function, + like this: + + .. sourcecode:: python + + import elib.intl + elib.intl.install('myapplication', '/path/to/usr/share/locale') + print _('This string will be translated.') + + Note that this is only one way, albeit the most convenient way, + to make the _() function available to your application. Because it affects + the entire application globally, and specifically Python’s built-in + namespace, localized modules should never install _(). Instead, you should + use :func:`elib.intl.install_module` to make _() available to your module. + ''' + _install(domain, localedir, True) + gettext.install(domain, localedir, unicode=True) + + +def install_module(domain, localedir): + ''' + :param domain: translation domain + :param localedir: locale directory + :returns: an anonymous function object, based on domain and localedir. + Codeset is always UTF-8. + + You may find this function usefull when writing localized modules. + Use this code to make _() available to your module: + + .. sourcecode:: python + + import elib.intl + _ = elib.intl.install_module('mymodule', '/path/to/usr/share/locale') + print _('This string will be translated.') + + When writing packages, you can usually do this in the package's __init__.py + file and import the _() function from the package namespace as needed. + ''' + _install(domain, localedir, False) + return lambda message: _dugettext(domain, message) diff --git a/chirp/errors.py b/chirp/errors.py new file mode 100644 index 0000000..311a1bb --- /dev/null +++ b/chirp/errors.py @@ -0,0 +1,49 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +class InvalidDataError(Exception): + """The radio driver encountered some invalid data""" + pass + + +class InvalidValueError(Exception): + """An invalid value for a given parameter was used""" + pass + + +class InvalidMemoryLocation(Exception): + """The requested memory location does not exist""" + pass + + +class RadioError(Exception): + """An error occurred while talking to the radio""" + pass + + +class UnsupportedToneError(Exception): + """The radio does not support the specified tone value""" + pass + + +class ImageDetectFailed(Exception): + """The driver for the supplied image could not be determined""" + pass + + +class ImageMetadataInvalidModel(Exception): + """The image contains metadata but no suitable driver is found""" + pass diff --git a/chirp/import_logic.py b/chirp/import_logic.py new file mode 100644 index 0000000..c2ed867 --- /dev/null +++ b/chirp/import_logic.py @@ -0,0 +1,268 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from chirp import chirp_common, errors + +LOG = logging.getLogger(__name__) + + +class ImportError(Exception): + """An import error""" + pass + + +class DestNotCompatible(ImportError): + """Memory is not compatible with the destination radio""" + pass + + +def ensure_has_calls(radio, memory): + """Make sure @radio has the necessary D-STAR callsigns for @memory""" + ulist_changed = rlist_changed = False + + ulist = radio.get_urcall_list() + rlist = radio.get_repeater_call_list() + + if memory.dv_urcall and memory.dv_urcall not in ulist: + for i in range(0, len(ulist)): + if not ulist[i].strip(): + ulist[i] = memory.dv_urcall + ulist_changed = True + break + if not ulist_changed: + raise errors.RadioError("No room to add callsign %s" % + memory.dv_urcall) + + rlist_add = [] + if memory.dv_rpt1call and memory.dv_rpt1call not in rlist: + rlist_add.append(memory.dv_rpt1call) + if memory.dv_rpt2call and memory.dv_rpt2call not in rlist: + rlist_add.append(memory.dv_rpt2call) + + while rlist_add: + call = rlist_add.pop() + for i in range(0, len(rlist)): + if not rlist[i].strip(): + rlist[i] = call + call = None + rlist_changed = True + break + if call: + raise errors.RadioError("No room to add callsign %s" % call) + + if ulist_changed: + radio.set_urcall_list(ulist) + if rlist_changed: + radio.set_repeater_call_list(rlist) + + +# Filter the name according to the destination's rules +def _import_name(dst_radio, _srcrf, mem): + mem.name = dst_radio.filter_name(mem.name) + + +def _import_power(dst_radio, _srcrf, mem): + levels = dst_radio.get_features().valid_power_levels + if not levels: + mem.power = None + return + elif mem.power is None: + # Source radio did not support power levels, so choose the + # first (highest) level from the destination radio. + mem.power = levels[0] + return + + # If both radios support power levels, we need to decide how to + # convert the source power level to a valid one for the destination + # radio. To do that, find the absolute level of the source value + # and calculate the different between it and all the levels of the + # destination, choosing the one that matches most closely. + + deltas = [abs(mem.power - power) for power in levels] + mem.power = levels[deltas.index(min(deltas))] + + +def _import_tone(dst_radio, srcrf, mem): + dstrf = dst_radio.get_features() + + # Some radios keep separate tones for Tone and TSQL modes (rtone and + # ctone). If we're importing to or from radios with differing models, + # do the conversion here. + + if srcrf.has_ctone and not dstrf.has_ctone: + # If copying from a radio with separate rtone/ctone to a radio + # without, and the tmode is TSQL, then use the ctone value + if mem.tmode == "TSQL": + mem.rtone = mem.ctone + elif not srcrf.has_ctone and dstrf.has_ctone: + # If copying from a radio without separate rtone/ctone to a radio + # with it, set the dest ctone to the src rtone + if mem.tmode == "TSQL": + mem.ctone = mem.rtone + + +def _import_dtcs(dst_radio, srcrf, mem): + dstrf = dst_radio.get_features() + + # Some radios keep separate DTCS codes for tx and rx + # If we're importing to or from radios with differing models, + # do the conversion here. + + if srcrf.has_rx_dtcs and not dstrf.has_rx_dtcs: + # If copying from a radio with separate codes to a radio + # without, and the tmode is DTCS, then use the rx_dtcs value + if mem.tmode == "DTCS": + mem.dtcs = mem.rx_dtcs + elif not srcrf.has_rx_dtcs and dstrf.has_rx_dtcs: + # If copying from a radio without separate codes to a radio + # with it, set the dest rx_dtcs to the src dtcs + if mem.tmode == "DTCS": + mem.rx_dtcs = mem.dtcs + + +def _guess_mode_by_frequency(freq): + ranges = [ + (0, 136000000, "AM"), + (136000000, 9999000000, "FM"), + ] + + for lo, hi, mode in ranges: + if freq > lo and freq <= hi: + return mode + + # If we don't know, assume FM + return "FM" + + +def _import_mode(dst_radio, srcrf, mem): + dstrf = dst_radio.get_features() + + # Some radios support an "Auto" mode. If we're importing from one + # that does to one that does not, guess at the proper mode based on the + # frequency + + if mem.mode == "Auto" and mem.mode not in dstrf.valid_modes: + mode = _guess_mode_by_frequency(mem.freq) + if mode not in dstrf.valid_modes: + raise DestNotCompatible("Destination does not support %s" % mode) + mem.mode = mode + + +def _make_offset_with_split(rxfreq, txfreq): + offset = txfreq - rxfreq + + if offset == 0: + return "", offset + elif offset > 0: + return "+", offset + elif offset < 0: + return "-", offset * -1 + + +def _import_duplex(dst_radio, srcrf, mem): + dstrf = dst_radio.get_features() + + # If a radio does not support odd split, we can use an equivalent offset + if mem.duplex == "split" and mem.duplex not in dstrf.valid_duplexes: + mem.duplex, mem.offset = _make_offset_with_split(mem.freq, mem.offset) + + # Enforce maximum offset + ranges = [(0, 500000000, 15000000), + (500000000, 3000000000, 50000000), + ] + for lo, hi, limit in ranges: + if lo < mem.freq <= hi: + if abs(mem.offset) > limit: + raise DestNotCompatible("Unable to create import memory: " + "offset is abnormally large.") + + +def import_mem(dst_radio, src_features, src_mem, overrides={}): + """Perform import logic to create a destination memory from + src_mem that will be compatible with @dst_radio""" + dst_rf = dst_radio.get_features() + + if isinstance(src_mem, chirp_common.DVMemory): + if not isinstance(dst_radio, chirp_common.IcomDstarSupport): + raise DestNotCompatible( + "Destination radio does not support D-STAR") + if dst_rf.requires_call_lists: + ensure_has_calls(dst_radio, src_mem) + + dst_mem = src_mem.dupe() + + for k, v in overrides.items(): + dst_mem.__dict__[k] = v + + helpers = [_import_name, + _import_power, + _import_tone, + _import_dtcs, + _import_mode, + _import_duplex, + ] + + for helper in helpers: + helper(dst_radio, src_features, dst_mem) + + msgs = dst_radio.validate_memory(dst_mem) + errs = [x for x in msgs if isinstance(x, chirp_common.ValidationError)] + if errs: + raise DestNotCompatible("Unable to create import memory: %s" % + ", ".join(errs)) + + return dst_mem + + +def _get_bank_model(radio): + for model in radio.get_mapping_models(): + if isinstance(model, chirp_common.BankModel): + return model + return None + + +def import_bank(dst_radio, src_radio, dst_mem, src_mem): + """Attempt to set the same banks for @mem(by index) in @dst_radio that + it has in @src_radio""" + + dst_bm = _get_bank_model(dst_radio) + if not dst_bm: + return + + dst_banks = dst_bm.get_mappings() + + src_bm = _get_bank_model(src_radio) + if not src_bm: + return + + src_banks = src_bm.get_mappings() + src_mem_banks = src_bm.get_memory_mappings(src_mem) + src_indexes = [src_banks.index(b) for b in src_mem_banks] + + for bank in dst_bm.get_memory_mappings(dst_mem): + dst_bm.remove_memory_from_mapping(dst_mem, bank) + + for index in src_indexes: + try: + bank = dst_banks[index] + LOG.debug("Adding memory to bank %s" % bank) + dst_bm.add_memory_to_mapping(dst_mem, bank) + if isinstance(dst_bm, chirp_common.MappingModelIndexInterface): + dst_bm.set_memory_index(dst_mem, bank, + dst_bm.get_next_mapping_index(bank)) + + except IndexError: + pass diff --git a/chirp/logger.py b/chirp/logger.py new file mode 100644 index 0000000..619927a --- /dev/null +++ b/chirp/logger.py @@ -0,0 +1,186 @@ +# Copyright 2015 Zachary T Welch +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +r""" +The chirp.logger module provides the core logging facilties for CHIRP. +It sets up the console and (optionally) a log file. For early debugging, +it checks the CHIRP_DEBUG, CHIRP_LOG, and CHIRP_LOG_LEVEL environment +variables. +""" + +import os +import sys +import logging +import argparse +from . import platform +from chirp import CHIRP_VERSION + + +def version_string(): + args = (CHIRP_VERSION, + platform.get_platform().os_version_string(), + sys.version.split()[0]) + return "CHIRP %s on %s (Python %s)" % args + + +class VersionAction(argparse.Action): + def __call__(self, parser, namespace, value, option_string=None): + print(version_string()) + sys.exit(1) + + +def add_version_argument(parser): + parser.add_argument("--version", action=VersionAction, nargs=0, + help="Print version and exit") + +#: Map human-readable logging levels to their internal values. +log_level_names = {"critical": logging.CRITICAL, + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, + } + + +class Logger(object): + + log_format = '[%(asctime)s] %(name)s - %(levelname)s: %(message)s' + + def __init__(self): + # create root logger + self.logger = logging.getLogger() + self.logger.setLevel(logging.DEBUG) + + self.LOG = logging.getLogger(__name__) + + # Set CHIRP_DEBUG in environment for early console debugging. + # It can be a number or a name; otherwise, level is set to 'debug' + # in order to maintain backward compatibility. + CHIRP_DEBUG = os.getenv("CHIRP_DEBUG") + self.early_level = logging.WARNING + if CHIRP_DEBUG: + try: + self.early_level = int(CHIRP_DEBUG) + except ValueError: + try: + self.early_level = log_level_names[CHIRP_DEBUG] + except KeyError: + self.early_level = logging.DEBUG + + # If we're on Win32 or MacOS, we don't use the console; instead, + # we create 'debug.log', redirect all output there, and set the + # console logging handler level to DEBUG. To test this on Linux, + # set CHIRP_DEBUG_LOG in the environment. + console_stream = None + console_format = '%(levelname)s: %(message)s' + if 'CHIRP_TESTENV' not in os.environ and ( + hasattr(sys, "frozen") or not os.isatty(0) or + os.getenv("CHIRP_DEBUG_LOG")): + p = platform.get_platform() + log = open(p.config_file("debug.log"), "w") + sys.stdout = log + sys.stderr = log + console_stream = log + console_format = self.log_format + self.early_level = logging.DEBUG + + self.console = logging.StreamHandler(console_stream) + self.console_level = self.early_level + self.console.setLevel(self.early_level) + self.console.setFormatter(logging.Formatter(console_format)) + self.logger.addHandler(self.console) + + # Set CHIRP_LOG in environment to the name of log file. + logname = os.getenv("CHIRP_LOG") + self.logfile = None + if logname is not None: + self.create_log_file(logname) + level = os.getenv("CHIRP_LOG_LEVEL") + if level is not None: + self.set_log_verbosity(level) + else: + self.set_log_level(logging.DEBUG) + + if self.early_level <= logging.DEBUG: + self.LOG.debug(version_string()) + + def create_log_file(self, name): + if self.logfile is None: + self.logname = name + # always truncate the log file + with open(name, "w") as fh: + pass + self.logfile = logging.FileHandler(name) + format_str = self.log_format + self.logfile.setFormatter(logging.Formatter(format_str)) + self.logger.addHandler(self.logfile) + else: + self.logger.error("already logging to " + self.logname) + + def set_verbosity(self, level): + self.LOG.debug("verbosity=%d", level) + if level > logging.CRITICAL: + level = logging.CRITICAL + self.console_level = level + self.console.setLevel(level) + + def set_log_level(self, level): + self.LOG.debug("log level=%d", level) + if level > logging.CRITICAL: + level = logging.CRITICAL + self.logfile.setLevel(level) + + def set_log_level_by_name(self, level): + self.set_log_level(log_level_names[level]) + + instance = None + +Logger.instance = Logger() + + +def is_visible(level): + """Returns True if a message at level will be shown on the console""" + return level >= Logger.instance.console_level + + +def add_arguments(parser): + parser.add_argument("-q", "--quiet", action="count", default=0, + help="Decrease verbosity") + parser.add_argument("-v", "--verbose", action="count", default=0, + help="Increase verbosity") + parser.add_argument("--log", dest="log_file", action="store", default=0, + help="Log messages to a file") + parser.add_argument("--log-level", action="store", default="debug", + help="Log file verbosity (critical, error, warn, " + + "info, debug). Defaults to 'debug'.") + + +def handle_options(options): + logger = Logger.instance + + if options.verbose or options.quiet: + logger.set_verbosity(30 + 10 * (options.quiet - options.verbose)) + + if options.log_file: + logger.create_log_file(options.log_file) + try: + level = int(options.log_level) + logger.set_log_level(level) + except ValueError: + logger.set_log_level_by_name(options.log_level) + + if logger.early_level > logging.DEBUG: + logger.LOG.debug(version_string()) diff --git a/chirp/memmap.py b/chirp/memmap.py new file mode 100644 index 0000000..9a16ee2 --- /dev/null +++ b/chirp/memmap.py @@ -0,0 +1,153 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import six + +from chirp import util + + +class MemoryMapBytes(object): + """ + This is the proper way for MemoryMap to work, which is + in terms of bytes always. + """ + + def __init__(self, data): + assert isinstance(data, bytes) + + self._data = list(data) + + def printable(self, start=None, end=None): + """Return a printable representation of the memory map""" + if not start: + start = 0 + + if not end: + end = len(self._data) + + string = util.hexprint(self._data[start:end]) + + return string + + def get(self, start, length=1): + """Return a chunk of memory of @length bytes from @start""" + if length == -1: + return bytes(self._data[start:]) + else: + end = start + length + d = self._data[start:end] + return bytes(d) + + def set(self, pos, value): + """Set a chunk of memory at @pos to @value""" + + pos = int(pos) + + if isinstance(value, int): + self._data[pos] = value & 0xFF + elif isinstance(value, bytes): + for byte in bytes(value): + self._data[pos] = byte + pos += 1 + elif isinstance(value, str): + if six.PY3: + value = value.encode() + for byte in value: + self._data[pos] = ord(byte) + pos += 1 + else: + raise ValueError("Unsupported type %s for value" % + type(value).__name__) + + def get_packed(self): + """Return the entire memory map as raw data""" + return bytes(self._data) + + def __len__(self): + return len(self._data) + + def __getslice__(self, start, end): + return self.get(start, end-start) + + def __getitem__(self, pos): + if isinstance(pos, slice): + if pos.stop is None: + return self.get(pos.start, -1) + + return self.get(pos.start, pos.stop - pos.start) + else: + return self.get(pos) + + def __setitem__(self, pos, value): + """ + NB: Setting a value of more than one character overwrites + len(value) bytes of the map, unlike a typical array! + """ + self.set(pos, value) + + def __str__(self): + return self.get_packed() + + def __repr__(self): + return self.printable(printit=False) + + def truncate(self, size): + """Truncate the memory map to @size""" + self._data = self._data[:size] + + def get_byte_compatible(self): + return self + + +class MemoryMap(MemoryMapBytes): + """Compatibility version of MemoryMapBytes + + This deals in strings for compatibility with drivers that do. + """ + def __init__(self, data): + # Fix circular dependency + from chirp import bitwise + self._bitwise = bitwise + + if six.PY3 and isinstance(data, bytes): + # Be graceful if py3-enabled code uses this, + # just don't encode it + encode = lambda d: d + else: + encode = self._bitwise.string_straight_encode + super(MemoryMap, self).__init__(encode(data)) + + def get(self, pos, length=1): + return self._bitwise.string_straight_decode( + super(MemoryMap, self).get(pos, length=length)) + + def set(self, pos, value): + if isinstance(value, int): + # Apparently this is a thing that drivers do, so + # be compatible here + value = chr(value) + super(MemoryMap, self).set( + pos, self._bitwise.string_straight_encode(value)) + + def get_packed(self): + return self._bitwise.string_straight_decode( + super(MemoryMap, self).get_packed()) + + def get_byte_compatible(self): + mmb = MemoryMapBytes(bytes(self._data)) + self._data = mmb._data + return mmb diff --git a/chirp/platform.py b/chirp/platform.py new file mode 100644 index 0000000..1fd928b --- /dev/null +++ b/chirp/platform.py @@ -0,0 +1,493 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import glob +import re +import logging +from subprocess import Popen + +import six + +LOG = logging.getLogger(__name__) + + +def win32_comports_bruteforce(): + import win32file + import win32con + + ports = [] + for i in range(1, 257): + portname = "\\\\.\\COM%i" % i + try: + mode = win32con.GENERIC_READ | win32con.GENERIC_WRITE + port = \ + win32file.CreateFile(portname, + mode, + win32con.FILE_SHARE_READ, + None, + win32con.OPEN_EXISTING, + 0, + None) + if portname.startswith("\\"): + portname = portname[4:] + ports.append((portname, "Unknown", "Serial")) + win32file.CloseHandle(port) + port = None + except Exception as e: + pass + + return ports + + +try: + from serial.tools.list_ports import comports +except: + comports = win32_comports_bruteforce + + +def _find_me(): + return sys.modules["chirp.platform"].__file__ + + +def natural_sorted(l): + def convert(text): + return int(text) if text.isdigit() else text.lower() + + def natural_key(key): + return [convert(c) for c in re.split('([0-9]+)', key)] + + return sorted(l, key=natural_key) + + +class Platform: + """Base class for platform-specific functions""" + + def __init__(self, basepath): + self._base = basepath + self._last_dir = self.default_dir() + + def get_last_dir(self): + """Return the last directory used""" + return self._last_dir + + def set_last_dir(self, last_dir): + """Set the last directory used""" + self._last_dir = last_dir + + def config_dir(self): + """Return the preferred configuration file directory""" + return self._base + + def log_dir(self): + """Return the preferred log file directory""" + logdir = os.path.join(self.config_dir(), "logs") + if not os.path.isdir(logdir): + os.mkdir(logdir) + + return logdir + + def filter_filename(self, filename): + """Filter @filename for platform-forbidden characters""" + return filename + + def log_file(self, filename): + """Return the full path to a log file with @filename""" + filename = self.filter_filename(filename + ".txt").replace(" ", "_") + return os.path.join(self.log_dir(), filename) + + def config_file(self, filename): + """Return the full path to a config file with @filename""" + return os.path.join(self.config_dir(), + self.filter_filename(filename)) + + def open_text_file(self, path): + """Spawn the necessary program to open a text file at @path""" + raise NotImplementedError("The base class can't do that") + + def open_html_file(self, path): + """Spawn the necessary program to open an HTML file at @path""" + raise NotImplementedError("The base class can't do that") + + def list_serial_ports(self): + """Return a list of valid serial ports""" + return [] + + def default_dir(self): + """Return the default directory for this platform""" + return "." + + def gui_open_file(self, start_dir=None, types=[]): + """Prompt the user to pick a file to open""" + import gtk + + if not start_dir: + start_dir = self._last_dir + + dlg = gtk.FileChooserDialog("Select a file to open", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + if start_dir and os.path.isdir(start_dir): + dlg.set_current_folder(start_dir) + + for desc, spec in types: + ff = gtk.FileFilter() + ff.set_name(desc) + ff.add_pattern(spec) + dlg.add_filter(ff) + + res = dlg.run() + fname = dlg.get_filename() + dlg.destroy() + + if res == gtk.RESPONSE_OK: + self._last_dir = os.path.dirname(fname) + return fname + else: + return None + + def gui_save_file(self, start_dir=None, default_name=None, types=[]): + """Prompt the user to pick a filename to save""" + import gtk + + if not start_dir: + start_dir = self._last_dir + + dlg = gtk.FileChooserDialog("Save file as", + None, + gtk.FILE_CHOOSER_ACTION_SAVE, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + if start_dir and os.path.isdir(start_dir): + dlg.set_current_folder(start_dir) + + if default_name: + dlg.set_current_name(default_name) + + extensions = {} + for desc, ext in types: + ff = gtk.FileFilter() + ff.set_name(desc) + ff.add_pattern("*.%s" % ext) + extensions[desc] = ext + dlg.add_filter(ff) + + res = dlg.run() + + fname = dlg.get_filename() + ext = extensions[dlg.get_filter().get_name()] + if fname and not fname.endswith(".%s" % ext): + fname = "%s.%s" % (fname, ext) + + dlg.destroy() + + if res == gtk.RESPONSE_OK: + self._last_dir = os.path.dirname(fname) + return fname + else: + return None + + def gui_select_dir(self, start_dir=None): + """Prompt the user to pick a directory""" + import gtk + + if not start_dir: + start_dir = self._last_dir + + dlg = gtk.FileChooserDialog("Choose folder", + None, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + if start_dir and os.path.isdir(start_dir): + dlg.set_current_folder(start_dir) + + res = dlg.run() + fname = dlg.get_filename() + dlg.destroy() + + if res == gtk.RESPONSE_OK and os.path.isdir(fname): + self._last_dir = fname + return fname + else: + return None + + def os_version_string(self): + """Return a string that describes the OS/platform version""" + return "Unknown Operating System" + + def executable_path(self): + """Return a full path to the program executable""" + def we_are_frozen(): + return hasattr(sys, "frozen") + + if we_are_frozen(): + # Win32, find the directory of the executable + return os.path.dirname(six.text_type(sys.executable, + sys.getfilesystemencoding())) + else: + # UNIX: Find the parent directory of this module + return os.path.dirname(os.path.abspath(os.path.join(_find_me(), + ".."))) + + def find_resource(self, filename): + """Searches for files installed to a share/ prefix.""" + execpath = self.executable_path() + share_candidates = [ + os.path.join(execpath, "share"), + os.path.join(sys.prefix, "share"), + "/usr/local/share", + "/usr/share", + ] + pkgshare_candidates = [os.path.join(i, "chirp") + for i in share_candidates] + search_paths = [execpath] + pkgshare_candidates + share_candidates + for path in search_paths: + candidate = os.path.join(path, filename) + if os.path.exists(candidate): + return candidate + return "" + + +def _unix_editor(): + macos_textedit = "/Applications/TextEdit.app/Contents/MacOS/TextEdit" + + if os.path.exists(macos_textedit): + return macos_textedit + else: + return "gedit" + + +class UnixPlatform(Platform): + """A platform module suitable for UNIX systems""" + def __init__(self, basepath): + if not basepath: + basepath = os.path.abspath(os.path.join(self.default_dir(), + ".chirp")) + + if not os.path.isdir(basepath): + os.mkdir(basepath) + + Platform.__init__(self, basepath) + + # This is a hack that needs to be properly fixed by importing the + # latest changes to this module from d-rats. In the interest of + # time, however, I'll throw it here + if sys.platform == "darwin": + if "DISPLAY" not in os.environ: + LOG.info("Forcing DISPLAY for MacOS") + os.environ["DISPLAY"] = ":0" + + os.environ["PANGO_RC_FILE"] = "../Resources/etc/pango/pangorc" + + def default_dir(self): + return os.path.abspath(os.getenv("HOME")) + + def filter_filename(self, filename): + return filename.replace("/", "") + + def open_text_file(self, path): + pid1 = os.fork() + if pid1 == 0: + pid2 = os.fork() + if pid2 == 0: + editor = _unix_editor() + LOG.debug("calling `%s %s'" % (editor, path)) + os.execlp(editor, editor, path) + else: + sys.exit(0) + else: + os.waitpid(pid1, 0) + LOG.debug("Exec child exited") + + def open_html_file(self, path): + os.system("firefox '%s'" % path) + + def list_serial_ports(self): + ports = ["/dev/ttyS*", + "/dev/ttyUSB*", + "/dev/ttyAMA*", + "/dev/ttyACM*", + "/dev/cu.*", + "/dev/cuaU*", + "/dev/cua0*", + "/dev/term/*", + "/dev/tty.KeySerial*"] + return natural_sorted(sum([glob.glob(x) for x in ports], [])) + + def os_version_string(self): + try: + issue = file("/etc/issue.net", "r") + ver = issue.read().strip().replace("\r", "").replace("\n", "")[:64] + issue.close() + ver = "%s - %s" % (os.uname()[0], ver) + except Exception: + ver = " ".join(os.uname()) + + return ver + + +class Win32Platform(Platform): + """A platform module suitable for Windows systems""" + def __init__(self, basepath=None): + if not basepath: + appdata = os.getenv("APPDATA") + if not appdata: + appdata = "C:\\" + basepath = os.path.abspath(os.path.join(appdata, "CHIRP")) + + if not os.path.isdir(basepath): + os.mkdir(basepath) + + Platform.__init__(self, basepath) + + def default_dir(self): + return os.path.abspath(os.path.join(os.getenv("USERPROFILE"), + "Desktop")) + + def filter_filename(self, filename): + for char in "/\\:*?\"<>|": + filename = filename.replace(char, "") + + return filename + + def open_text_file(self, path): + Popen(["notepad", path]) + return + + def open_html_file(self, path): + os.system("explorer %s" % path) + + def list_serial_ports(self): + try: + ports = list(comports()) + except Exception as e: + if comports != win32_comports_bruteforce: + LOG.error("Failed to detect win32 serial ports: %s" % e) + ports = win32_comports_bruteforce() + return natural_sorted([port for port, name, url in ports]) + + def gui_open_file(self, start_dir=None, types=[]): + import win32gui + + typestrs = "" + for desc, spec in types: + typestrs += "%s\0%s\0" % (desc, spec) + if not typestrs: + typestrs = None + + try: + fname, _, _ = win32gui.GetOpenFileNameW(Filter=typestrs) + except Exception as e: + LOG.error("Failed to get filename: %s" % e) + return None + + return str(fname) + + def gui_save_file(self, start_dir=None, default_name=None, types=[]): + import win32gui + import win32api + + (pform, _, _, _, _) = win32api.GetVersionEx() + + typestrs = "" + custom = "%s\0*.%s\0" % (types[0][0], types[0][1]) + for desc, ext in types[1:]: + typestrs += "%s\0%s\0" % (desc, "*.%s" % ext) + + if pform > 5: + typestrs = "%s\0%s\0" % (types[0][0], "*.%s" % types[0][1]) + \ + typestrs + + if not typestrs: + typestrs = custom + custom = None + + def_ext = "*.%s" % types[0][1] + try: + fname, _, _ = win32gui.GetSaveFileNameW(File=default_name, + CustomFilter=custom, + DefExt=def_ext, + Filter=typestrs) + except Exception as e: + LOG.error("Failed to get filename: %s" % e) + return None + + return str(fname) + + def gui_select_dir(self, start_dir=None): + from win32com.shell import shell + + try: + pidl, _, _ = shell.SHBrowseForFolder() + fname = shell.SHGetPathFromIDList(pidl) + except Exception as e: + LOG.error("Failed to get directory: %s" % e) + return None + + return str(fname) + + def os_version_string(self): + import win32api + + vers = {4: "Win2k", + 5: "WinXP", + 6: "WinVista/7", + } + + (pform, sub, build, _, _) = win32api.GetVersionEx() + + return vers.get(pform, + "Win32 (Unknown %i.%i:%i)" % (pform, sub, build)) + + +def _get_platform(basepath): + if os.name == "nt": + return Win32Platform(basepath) + else: + return UnixPlatform(basepath) + +PLATFORM = None + + +def get_platform(basepath=None): + """Return the platform singleton""" + global PLATFORM + + if not PLATFORM: + PLATFORM = _get_platform(basepath) + + return PLATFORM + + +def _do_test(): + __pform = get_platform() + + print("Config dir: %s" % __pform.config_dir()) + print("Default dir: %s" % __pform.default_dir()) + print("Log file (foo): %s" % __pform.log_file("foo")) + print("Serial ports: %s" % __pform.list_serial_ports()) + print("OS Version: %s" % __pform.os_version_string()) + # __pform.open_text_file("d-rats.py") + + # print "Open file: %s" % __pform.gui_open_file() + # print "Save file: %s" % __pform.gui_save_file(default_name="Foo.txt") + print("Open folder: %s" % __pform.gui_select_dir("/tmp")) + +if __name__ == "__main__": + _do_test() diff --git a/chirp/pyPEG.py b/chirp/pyPEG.py new file mode 100644 index 0000000..ce04502 --- /dev/null +++ b/chirp/pyPEG.py @@ -0,0 +1,401 @@ +# YPL parser 1.5 + +# written by VB. + +import re +import sys +import codecs +import collections + +import six + + +class keyword(str): + pass + + +class code(str): + pass + + +class ignore(object): + def __init__(self, regex_text, *args): + self.regex = re.compile(regex_text, *args) + + +class _and(object): + def __init__(self, something): + self.obj = something + + +class _not(_and): + pass + + +class Name(str): + def __init__(self, *args): + self.line = 0 + self.file = "" + + +class Symbol(list): + def __init__(self, name, what): + self.__name__ = name + self.append(name) + self.what = what + self.append(what) + + def __call__(self): + return self.what + + def __unicode__(self): + return 'Symbol(' + repr(self.__name__) + ', ' + repr(self.what) + ')' + + def __repr__(self): + return str(self) + +word_regex = re.compile(r"\w+") +rest_regex = re.compile(r".*") + +print_trace = False + + +def u(text): + return six.text_type(text) + if isinstance(text, exceptions.BaseException): + text = text.args[0] + if type(text) is str: + return text + if isinstance(text, str): + if sys.stdin.encoding: + return codecs.decode(text, sys.stdin.encoding) + else: + return codecs.decode(text, "utf-8") + return str(text) + + +def skip(skipper, text, skipWS, skipComments): + if skipWS: + t = text.lstrip() + else: + t = text + if skipComments: + try: + while True: + skip, t = skipper.parseLine(t, skipComments, [], skipWS, None) + if skipWS: + t = t.lstrip() + except: + pass + return t + + +class parser(object): + def __init__(self, another=False, p=False): + self.restlen = -1 + if not(another): + self.skipper = parser(True, p) + self.skipper.packrat = p + else: + self.skipper = self + self.lines = None + self.textlen = 0 + self.memory = {} + self.packrat = p + + # parseLine(): + # textline: text to parse + # pattern: pyPEG language description + # resultSoFar: parsing result so far (default: blank list []) + # skipWS: Flag if whitespace should be skipped (default: True) + # skipComments: Python functions returning pyPEG for matching comments + # + # returns: pyAST, textrest + # + # raises: SyntaxError(reason) if textline is detected not + # being in language described by pattern + # + # SyntaxError(reason) if pattern is an illegal + # language description + + def parseLine(self, textline, pattern, resultSoFar=[], + skipWS=True, skipComments=None): + name = None + _textline = textline + _pattern = pattern + + def R(result, text): + if __debug__: + if print_trace: + try: + if _pattern.__name__ != "comment": + sys.stderr.write("match: " + + _pattern.__name__ + "\n") + except: + pass + + if self.restlen == -1: + self.restlen = len(text) + else: + self.restlen = min(self.restlen, len(text)) + res = resultSoFar + if name and result: + name.line = self.lineNo() + res.append(Symbol(name, result)) + elif name: + name.line = self.lineNo() + res.append(Symbol(name, [])) + elif result: + if isinstance(result, list): + res.extend(result) + else: + res.extend([result]) + if self.packrat: + self.memory[(len(_textline), id(_pattern))] = (res, text) + return res, text + + def syntaxError(): + if self.packrat: + self.memory[(len(_textline), id(_pattern))] = False + raise SyntaxError() + + if self.packrat: + try: + result = self.memory[(len(textline), id(pattern))] + if result: + return result + else: + raise SyntaxError() + except: + pass + + if isinstance(pattern, collections.Callable): + if __debug__: + if print_trace: + try: + if pattern.__name__ != "comment": + sys.stderr.write("testing with " + + pattern.__name__ + ": " + + textline[:40] + "\n") + except: + pass + + if pattern.__name__[0] != "_": + name = Name(pattern.__name__) + + pattern = pattern() + if isinstance(pattern, collections.Callable): + pattern = (pattern,) + + text = skip(self.skipper, textline, skipWS, skipComments) + + pattern_type = type(pattern) + + if pattern_type is str or pattern_type is str: + if text[:len(pattern)] == pattern: + text = skip(self.skipper, text[len(pattern):], + skipWS, skipComments) + return R(None, text) + else: + syntaxError() + + elif pattern_type is keyword: + m = word_regex.match(text) + if m: + if m.group(0) == pattern: + text = skip(self.skipper, text[len(pattern):], + skipWS, skipComments) + return R(None, text) + else: + syntaxError() + else: + syntaxError() + + elif pattern_type is _not: + try: + r, t = self.parseLine(text, pattern.obj, [], + skipWS, skipComments) + except: + return resultSoFar, textline + syntaxError() + + elif pattern_type is _and: + r, t = self.parseLine(text, pattern.obj, [], skipWS, skipComments) + return resultSoFar, textline + + elif pattern_type is type(word_regex) or pattern_type is ignore: + if pattern_type is ignore: + pattern = pattern.regex + m = pattern.match(text) + if m: + text = skip(self.skipper, text[len(m.group(0)):], + skipWS, skipComments) + if pattern_type is ignore: + return R(None, text) + else: + return R(m.group(0), text) + else: + syntaxError() + + elif pattern_type is tuple: + result = [] + n = 1 + for p in pattern: + if isinstance(p, int): + n = p + else: + if n > 0: + for i in range(n): + result, text = self.parseLine( + text, p, result, skipWS, skipComments) + elif n == 0: + if text == "": + pass + else: + try: + newResult, newText = self.parseLine( + text, p, result, skipWS, skipComments) + result, text = newResult, newText + except SyntaxError: + pass + elif n < 0: + found = False + while True: + try: + newResult, newText = self.parseLine( + text, p, result, skipWS, skipComments) + result, text, found = newResult, newText, True + except SyntaxError: + break + if n == -2 and not(found): + syntaxError() + n = 1 + return R(result, text) + + elif pattern_type is list: + result = [] + found = False + for p in pattern: + try: + result, text = self.parseLine(text, p, result, + skipWS, skipComments) + found = True + except SyntaxError: + pass + if found: + break + if found: + return R(result, text) + else: + syntaxError() + + else: + raise SyntaxError("illegal type in grammar: " + u(pattern_type)) + + def lineNo(self): + if not(self.lines): + return "" + if self.restlen == -1: + return "" + parsed = self.textlen - self.restlen + + left, right = 0, len(self.lines) + + while True: + mid = int((right + left) / 2) + if self.lines[mid][0] <= parsed: + try: + if self.lines[mid + 1][0] >= parsed: + try: + return u(self.lines[mid + 1][1]) + \ + ":" + u(self.lines[mid + 1][2]) + except: + return "" + else: + left = mid + 1 + except: + try: + return u(self.lines[mid + 1][1]) + \ + ":" + u(self.lines[mid + 1][2]) + except: + return "" + else: + right = mid - 1 + if left > right: + return "" + + +# plain module APIs + + +def parseLine(textline, pattern, resultSoFar=[], skipWS=True, + skipComments=None, packrat=False): + p = parser(p=packrat) + text = skip(p.skipper, textline, skipWS, skipComments) + ast, text = p.parseLine(text, pattern, resultSoFar, skipWS, skipComments) + return ast, text + +# parse(): +# language: pyPEG language description +# lineSource: a fileinput.FileInput object +# skipWS: Flag if whitespace should be skipped (default: True) +# skipComments: Python function which returns pyPEG for matching comments +# packrat: use memoization +# lineCount: add line number information to AST +# +# returns: pyAST +# +# raises: SyntaxError(reason), if a parsed line is not in language +# SyntaxError(reason), if the language description is illegal + + +def parse(language, lineSource, skipWS=True, skipComments=None, + packrat=False, lineCount=True): + lines, lineNo = [], 0 + + while isinstance(language, collections.Callable): + language = language() + + orig, ld = "", 0 + for line in lineSource: + if lineSource.isfirstline(): + ld = 1 + else: + ld += 1 + lines.append((len(orig), lineSource.filename(), + lineSource.lineno() - 1)) + orig += u(line) + + textlen = len(orig) + + try: + p = parser(p=packrat) + p.textlen = len(orig) + if lineCount: + p.lines = lines + else: + p.line = None + text = skip(p.skipper, orig, skipWS, skipComments) + result, text = p.parseLine(text, language, [], skipWS, skipComments) + if text: + raise SyntaxError() + + except SyntaxError as msg: + parsed = textlen - p.restlen + textlen = 0 + nn, lineNo, file = 0, 0, "" + for n, ld, l in lines: + if n >= parsed: + break + else: + lineNo = l + nn += 1 + file = ld + + lineNo += 1 + nn -= 1 + lineCont = orig.splitlines()[nn] + raise SyntaxError("syntax error in " + u(file) + ":" + + u(lineNo) + ": " + lineCont) + + return result diff --git a/chirp/radioreference.py b/chirp/radioreference.py new file mode 100644 index 0000000..262e592 --- /dev/null +++ b/chirp/radioreference.py @@ -0,0 +1,192 @@ +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +from chirp import chirp_common, errors + +LOG = logging.getLogger(__name__) + +try: + from suds.client import Client + from suds import WebFault + HAVE_SUDS = True +except ImportError: + HAVE_SUDS = False + +MODES = { + "FM": "FM", + "AM": "AM", + "FMN": "NFM", + "D-STAR": "DV", + "USB": "USB", + "LSB": "LSB", + "P25": "P25", +} + + +class RadioReferenceRadio(chirp_common.NetworkSourceRadio): + """RadioReference.com data source""" + VENDOR = "Radio Reference LLC" + MODEL = "RadioReference.com" + + URL = "http://api.radioreference.com/soap2/?wsdl" + APPKEY = "46785108" + + def __init__(self, *args, **kwargs): + chirp_common.NetworkSourceRadio.__init__(self, *args, **kwargs) + + if not HAVE_SUDS: + raise errors.RadioError( + "Suds library required for RadioReference.com import.\n" + + "Try installing your distribution's python-suds package.") + + self._auth = {"appKey": self.APPKEY, "username": "", "password": ""} + self._client = Client(self.URL) + self._freqs = None + self._modes = None + self._zip = None + + def set_params(self, zipcode, username, password): + """Set the parameters to be used for a query""" + self._zip = zipcode + self._auth["username"] = username + self._auth["password"] = password + + def do_fetch(self): + """Fetches frequencies for all subcategories in a county.""" + self._freqs = [] + + try: + service = self._client.service + zipcode = service.getZipcodeInfo(self._zip, self._auth) + county = service.getCountyInfo(zipcode.ctid, self._auth) + except WebFault as err: + raise errors.RadioError(err) + + status = chirp_common.Status() + status.max = 0 + for cat in county.cats: + status.max += len(cat.subcats) + status.max += len(county.agencyList) + + for cat in county.cats: + LOG.debug("Fetching category:", cat.cName) + for subcat in cat.subcats: + LOG.debug("\t", subcat.scName) + result = self._client.service.getSubcatFreqs(subcat.scid, + self._auth) + self._freqs += result + status.cur += 1 + self.status_fn(status) + status.max -= len(county.agencyList) + for agency in county.agencyList: + agency = self._client.service.getAgencyInfo(agency.aid, self._auth) + for cat in agency.cats: + status.max += len(cat.subcats) + for cat in agency.cats: + LOG.debug("Fetching category:", cat.cName) + for subcat in cat.subcats: + try: + LOG.debug("\t", subcat.scName) + except AttributeError: + pass + result = self._client.service.getSubcatFreqs(subcat.scid, + self._auth) + self._freqs += result + status.cur += 1 + self.status_fn(status) + + def get_features(self): + if not self._freqs: + self.do_fetch() + + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, len(self._freqs)-1) + rf.has_bank = False + rf.has_ctone = False + rf.valid_tmodes = ["", "TSQL", "DTCS"] + return rf + + def get_raw_memory(self, number): + return repr(self._freqs[number]) + + def get_memory(self, number): + if not self._freqs: + self.do_fetch() + + freq = self._freqs[number] + + mem = chirp_common.Memory() + mem.number = number + + mem.name = freq.alpha or freq.descr or "" + mem.freq = chirp_common.parse_freq(str(freq.out)) + if freq["in"] == 0.0: + mem.duplex = "" + else: + mem.duplex = "split" + mem.offset = chirp_common.parse_freq(str(freq["in"])) + if freq.tone is not None: + if str(freq.tone) == "CSQ": # Carrier Squelch + mem.tmode = "" + else: + try: + tone, tmode = freq.tone.split(" ") + except Exception: + tone, tmode = None, None + if tmode == "PL": + mem.tmode = "TSQL" + mem.rtone = mem.ctone = float(tone) + elif tmode == "DPL": + mem.tmode = "DTCS" + mem.dtcs = int(tone) + else: + LOG.error("Error: unsupported tone: %s" % freq) + try: + mem.mode = self._get_mode(freq.mode) + except KeyError: + # skip memory if mode is unsupported + mem.empty = True + return mem + mem.comment = freq.descr.strip() + + return mem + + def _get_mode(self, modeid): + if not self._modes: + self._modes = {} + for mode in self._client.service.getMode("0", self._auth): + # sax.text.Text cannot be coerced directly to int + self._modes[int(str(mode.mode))] = str(mode.modeName) + return MODES[self._modes[int(str(modeid))]] + + +def main(): + """ + Usage: + cd ~/src/chirp.hg + python ./chirp/radioreference.py [ZIPCODE] [USERNAME] [PASSWORD] + """ + import sys + rrr = RadioReferenceRadio(None) + rrr.set_params(zipcode=sys.argv[1], + username=sys.argv[2], + password=sys.argv[3]) + rrr.do_fetch() + print(rrr.get_raw_memory(0)) + print(rrr.get_memory(0)) + +if __name__ == "__main__": + main() diff --git a/chirp/settings.py b/chirp/settings.py new file mode 100644 index 0000000..7438425 --- /dev/null +++ b/chirp/settings.py @@ -0,0 +1,477 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import chirp_common + + +class InvalidValueError(Exception): + + """An invalid value was specified for a given setting""" + pass + + +class InternalError(Exception): + + """A driver provided an invalid settings object structure""" + pass + + +class RadioSettingValue: + + """Base class for a single radio setting""" + + def __init__(self): + self._current = None + self._has_changed = False + self._validate_callback = lambda x: x + self._mutable = True + + def set_mutable(self, mutable): + self._mutable = mutable + + def get_mutable(self): + return self._mutable + + def changed(self): + """Returns True if the setting has been changed since init""" + return self._has_changed + + def set_validate_callback(self, callback): + self._validate_callback = callback + + def set_value(self, value): + """Sets the current value, triggers changed""" + if not self.get_mutable(): + raise InvalidValueError("This value is not mutable") + + if self._current is not None and value != self._current: + self._has_changed = True + self._current = self._validate_callback(value) + + def get_value(self): + """Gets the current value""" + return self._current + + def __trunc__(self): + return int(self.get_value()) + + def __str__(self): + return str(self.get_value()) + + +class RadioSettingValueInteger(RadioSettingValue): + + """An integer setting""" + + def __init__(self, minval, maxval, current, step=1): + RadioSettingValue.__init__(self) + self._min = minval + self._max = maxval + self._step = step + self.set_value(current) + + def set_value(self, value): + try: + value = int(value) + except: + raise InvalidValueError("An integer is required") + if value > self._max or value < self._min: + raise InvalidValueError("Value %i not in range %i-%i" % + (value, self._min, self._max)) + RadioSettingValue.set_value(self, value) + + def get_min(self): + """Returns the minimum allowed value""" + return self._min + + def get_max(self): + """Returns the maximum allowed value""" + return self._max + + def get_step(self): + """Returns the step increment""" + return self._step + + +class RadioSettingValueFloat(RadioSettingValue): + + """A floating-point setting""" + + def __init__(self, minval, maxval, current, resolution=0.001, precision=4): + RadioSettingValue.__init__(self) + self._min = minval + self._max = maxval + self._res = resolution + self._pre = precision + self.set_value(current) + + def format(self, value=None): + """Formats the value into a string""" + if value is None: + value = self._current + fmt_string = "%%.%if" % self._pre + return fmt_string % value + + def set_value(self, value): + try: + value = float(value) + except: + raise InvalidValueError("A floating point value is required") + if value > self._max or value < self._min: + raise InvalidValueError("Value %s not in range %s-%s" % ( + self.format(value), + self.format(self._min), self.format(self._max))) + + # FIXME: honor resolution + + RadioSettingValue.set_value(self, value) + + def get_min(self): + """Returns the minimum allowed value""" + return self._min + + def get_max(self): + """Returns the maximum allowed value""" + + +class RadioSettingValueBoolean(RadioSettingValue): + + """A boolean setting""" + + def __init__(self, current): + RadioSettingValue.__init__(self) + self.set_value(current) + + def set_value(self, value): + RadioSettingValue.set_value(self, bool(value)) + + def __bool__(self): + return bool(self.get_value()) + __nonzero__ = __bool__ + + def __str__(self): + return str(bool(self.get_value())) + + +class RadioSettingValueList(RadioSettingValue): + + """A list-of-strings setting""" + + def __init__(self, options, current): + RadioSettingValue.__init__(self) + self._options = options + self.set_value(current) + + def set_value(self, value): + if value not in self._options: + raise InvalidValueError("%s is not valid for this setting" % value) + RadioSettingValue.set_value(self, value) + + def get_options(self): + """Returns the list of valid option values""" + return self._options + + def __trunc__(self): + return self._options.index(self._current) + + +class RadioSettingValueString(RadioSettingValue): + + """A string setting""" + + def __init__(self, minlength, maxlength, current, + autopad=True, charset=chirp_common.CHARSET_ASCII): + RadioSettingValue.__init__(self) + self._minlength = minlength + self._maxlength = maxlength + self._charset = charset + self._autopad = autopad + self.set_value(current) + + def set_charset(self, charset): + """Sets the set of allowed characters""" + self._charset = charset + + def set_value(self, value): + if len(value) < self._minlength or len(value) > self._maxlength: + raise InvalidValueError("Value must be between %i and %i chars" % + (self._minlength, self._maxlength)) + if self._autopad: + value = value.ljust(self._maxlength) + for char in value: + if char not in self._charset: + raise InvalidValueError("Value contains invalid " + + "character `%s'" % char) + RadioSettingValue.set_value(self, value) + + def __str__(self): + return self._current + + def __len__(self): + return len(self._current) + + def __getitem__(self, i): + return self._current[i] + + +class RadioSettingValueMap(RadioSettingValueList): + + """Map User Options to Radio Memory Values + + Provides User Option list for GUI, maintains state, verifies new values, + and allows {setting,getting} by User Option OR Memory Value. External + conversions not needed. + + """ + + def __init__(self, map_entries, mem_val=None, user_option=None): + """Create new map + + Pass in list of 2 member tuples, typically of type (str, int), + for each Radio Setting. First member of each tuple is the + User Option Name, second is the Memory Value that corresponds. + An example is APO: ("Off", 0), ("0.5", 5), ("1.0", 10). + + """ + # Catch bugs early by testing tuple geometry + for map_entry in map_entries: + if not len(map_entry) == 2: + raise InvalidValueError("map_entries must be 2 el tuples " + "instead of: %s" % str(map_entry)) + user_options = [e[0] for e in map_entries] + self._mem_vals = [e[1] for e in map_entries] + RadioSettingValueList.__init__(self, user_options, user_options[0]) + if mem_val is not None: + self.set_mem_val(mem_val) + elif user_option is not None: + self.set_value(user_option) + self._has_changed = False + + def set_mem_val(self, mem_val): + """Change setting to User Option that corresponds to 'mem_val'""" + if mem_val in self._mem_vals: + index = self._mem_vals.index(mem_val) + self.set_value(self._options[index]) + else: + raise InvalidValueError( + "%s is not valid for this setting" % mem_val) + + def get_mem_val(self): + """Get the mem val corresponding to the currently selected user + option""" + return self._mem_vals[self._options.index(self.get_value())] + + def __trunc__(self): + """Return memory value that matches current user option""" + index = self._options.index(self._current) + value = self._mem_vals[index] + return value + + +def zero_indexed_seq_map(user_options): + """RadioSettingValueMap factory method + + Radio Setting Maps commonly use a list of strings that map to a sequence + that starts with 0. Pass in a list of User Options and this function + returns a list of tuples of form (str, int). + + """ + mem_vals = range(0, len(user_options)) + return zip(user_options, mem_vals) + + +class RadioSettings(list): + + def __init__(self, *groups): + list.__init__(self, groups) + + def __str__(self): + items = [str(self[i]) for i in range(0, len(self))] + return "\n".join(items) + + +class RadioSettingGroup(object): + + """A group of settings""" + + def _validate(self, element): + # RadioSettingGroup can only contain RadioSettingGroup objects + if not isinstance(element, RadioSettingGroup): + raise InternalError("Incorrect type %s" % type(element)) + + def __init__(self, name, shortname, *elements): + self._name = name # Setting identifier + self._shortname = shortname # Short human-readable name/description + self.__doc__ = name # Longer explanation/documentation + self._elements = {} + self._element_order = [] + + for element in elements: + self._validate(element) + self.append(element) + + def get_name(self): + """Returns the group name""" + return self._name + + def get_shortname(self): + """Returns the short group identifier""" + return self._shortname + + def set_doc(self, doc): + """Sets the docstring for the group""" + self.__doc__ = doc + + def __str__(self): + string = "group '%s': {\n" % self._name + for element in sorted(self._elements.values()): + string += "\t" + str(element) + "\n" + string += "}" + return string + + # Kinda list interface + + def append(self, element): + """Adds an element to the group""" + self[element.get_name()] = element + + def __iter__(self): + class RSGIterator: + + """Iterator for a RadioSettingGroup""" + + def __init__(self, rsg): + self.__rsg = rsg + self.__i = 0 + + def __iter__(self): + return self + + def next(self): + return self.__next__() + + def __next__(self): + """Next Iterator Interface""" + if self.__i >= len(self.__rsg.keys()): + raise StopIteration() + e = self.__rsg[self.__rsg.keys()[self.__i]] + self.__i += 1 + return e + return RSGIterator(self) + + # Dictionary interface + + def __len__(self): + return len(self._elements) + + def __getitem__(self, name): + return self._elements[name] + + def __setitem__(self, name, value): + if name in self._element_order: + raise KeyError("Duplicate item %s" % name) + self._elements[name] = value + self._element_order.append(name) + + def items(self): + """Returns a key=>value set of elements, like a dict""" + return [(name, self._elements[name]) for name in self._element_order] + + def keys(self): + """Returns a list of string element names""" + return self._element_order + + def values(self): + """Returns the list of elements""" + return [self._elements[name] for name in self._element_order] + + def __lt__(self, other): + return self._name < other._name + + +class RadioSetting(RadioSettingGroup): + + """A single setting, which could be an array of items like a group""" + + def __init__(self, *args): + super(RadioSetting, self).__init__(*args) + self._apply_callback = None + + def set_apply_callback(self, callback, *args): + self._apply_callback = lambda: callback(self, *args) + + def has_apply_callback(self): + return self._apply_callback is not None + + def run_apply_callback(self): + return self._apply_callback() + + def _validate(self, value): + # RadioSetting can only contain RadioSettingValue objects + if not isinstance(value, RadioSettingValue): + raise InternalError("Incorrect type") + + def changed(self): + """Returns True if any of the elements + in the group have been changed""" + for element in self._elements.values(): + if element.changed(): + return True + return False + + def __str__(self): + return "%s:%s" % (self._name, self.value) + + def __repr__(self): + return "[RadioSetting %s:%s]" % (self._name, self._value) + + # Magic foo.value attribute + def __getattr__(self, name): + if name == "value": + if len(self) == 1: + return self._elements[self._element_order[0]] + else: + return self._elements.values() + else: + return self.__dict__[name] + + def __setattr__(self, name, value): + if name == "value": + if len(self) == 1: + self._elements[self._element_order[0]].set_value(value) + else: + raise InternalError("Setting %s is not a scalar" % self._name) + else: + self.__dict__[name] = value + + # List interface + + def append(self, value): + index = len(self._element_order) + self._elements[index] = value + self._element_order.append(index) + + def __getitem__(self, name): + if not isinstance(name, int): + raise IndexError("Index `%s' is not an integer" % name) + return self._elements[name] + + def __setitem__(self, name, value): + if not isinstance(name, int): + raise IndexError("Index `%s' is not an integer" % name) + if name in self._elements: + self._elements[name].set_value(value) + else: + self._elements[name] = value diff --git a/chirp/ui/__init__.py b/chirp/ui/__init__.py new file mode 100644 index 0000000..a2a6f35 --- /dev/null +++ b/chirp/ui/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/chirp/ui/__pycache__/__init__.cpython-37.pyc b/chirp/ui/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..6ded4ba Binary files /dev/null and b/chirp/ui/__pycache__/__init__.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/__init__.cpython-38.pyc b/chirp/ui/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..55f8834 Binary files /dev/null and b/chirp/ui/__pycache__/__init__.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/bandplans.cpython-37.pyc b/chirp/ui/__pycache__/bandplans.cpython-37.pyc new file mode 100644 index 0000000..c73c021 Binary files /dev/null and b/chirp/ui/__pycache__/bandplans.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/bandplans.cpython-38.pyc b/chirp/ui/__pycache__/bandplans.cpython-38.pyc new file mode 100644 index 0000000..b5032a9 Binary files /dev/null and b/chirp/ui/__pycache__/bandplans.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/bankedit.cpython-37.pyc b/chirp/ui/__pycache__/bankedit.cpython-37.pyc new file mode 100644 index 0000000..e68d8cc Binary files /dev/null and b/chirp/ui/__pycache__/bankedit.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/bankedit.cpython-38.pyc b/chirp/ui/__pycache__/bankedit.cpython-38.pyc new file mode 100644 index 0000000..21bdcbf Binary files /dev/null and b/chirp/ui/__pycache__/bankedit.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/clone.cpython-37.pyc b/chirp/ui/__pycache__/clone.cpython-37.pyc new file mode 100644 index 0000000..75cc5fb Binary files /dev/null and b/chirp/ui/__pycache__/clone.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/clone.cpython-38.pyc b/chirp/ui/__pycache__/clone.cpython-38.pyc new file mode 100644 index 0000000..79fe18b Binary files /dev/null and b/chirp/ui/__pycache__/clone.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/cloneprog.cpython-37.pyc b/chirp/ui/__pycache__/cloneprog.cpython-37.pyc new file mode 100644 index 0000000..ed97a6f Binary files /dev/null and b/chirp/ui/__pycache__/cloneprog.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/cloneprog.cpython-38.pyc b/chirp/ui/__pycache__/cloneprog.cpython-38.pyc new file mode 100644 index 0000000..72cc576 Binary files /dev/null and b/chirp/ui/__pycache__/cloneprog.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/common.cpython-37.pyc b/chirp/ui/__pycache__/common.cpython-37.pyc new file mode 100644 index 0000000..6de32f4 Binary files /dev/null and b/chirp/ui/__pycache__/common.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/common.cpython-38.pyc b/chirp/ui/__pycache__/common.cpython-38.pyc new file mode 100644 index 0000000..ba2c579 Binary files /dev/null and b/chirp/ui/__pycache__/common.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/compat.cpython-37.pyc b/chirp/ui/__pycache__/compat.cpython-37.pyc new file mode 100644 index 0000000..3083f5d Binary files /dev/null and b/chirp/ui/__pycache__/compat.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/compat.cpython-38.pyc b/chirp/ui/__pycache__/compat.cpython-38.pyc new file mode 100644 index 0000000..5106bc3 Binary files /dev/null and b/chirp/ui/__pycache__/compat.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/config.cpython-37.pyc b/chirp/ui/__pycache__/config.cpython-37.pyc new file mode 100644 index 0000000..6513a0f Binary files /dev/null and b/chirp/ui/__pycache__/config.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/config.cpython-38.pyc b/chirp/ui/__pycache__/config.cpython-38.pyc new file mode 100644 index 0000000..c3068e3 Binary files /dev/null and b/chirp/ui/__pycache__/config.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/dstaredit.cpython-37.pyc b/chirp/ui/__pycache__/dstaredit.cpython-37.pyc new file mode 100644 index 0000000..0849ab4 Binary files /dev/null and b/chirp/ui/__pycache__/dstaredit.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/dstaredit.cpython-38.pyc b/chirp/ui/__pycache__/dstaredit.cpython-38.pyc new file mode 100644 index 0000000..39cadf2 Binary files /dev/null and b/chirp/ui/__pycache__/dstaredit.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/editorset.cpython-37.pyc b/chirp/ui/__pycache__/editorset.cpython-37.pyc new file mode 100644 index 0000000..705653d Binary files /dev/null and b/chirp/ui/__pycache__/editorset.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/editorset.cpython-38.pyc b/chirp/ui/__pycache__/editorset.cpython-38.pyc new file mode 100644 index 0000000..6bbcb58 Binary files /dev/null and b/chirp/ui/__pycache__/editorset.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/fips.cpython-37.pyc b/chirp/ui/__pycache__/fips.cpython-37.pyc new file mode 100644 index 0000000..53ecc8e Binary files /dev/null and b/chirp/ui/__pycache__/fips.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/fips.cpython-38.pyc b/chirp/ui/__pycache__/fips.cpython-38.pyc new file mode 100644 index 0000000..8e578a5 Binary files /dev/null and b/chirp/ui/__pycache__/fips.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/importdialog.cpython-37.pyc b/chirp/ui/__pycache__/importdialog.cpython-37.pyc new file mode 100644 index 0000000..be47060 Binary files /dev/null and b/chirp/ui/__pycache__/importdialog.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/importdialog.cpython-38.pyc b/chirp/ui/__pycache__/importdialog.cpython-38.pyc new file mode 100644 index 0000000..b1d0ce5 Binary files /dev/null and b/chirp/ui/__pycache__/importdialog.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/inputdialog.cpython-37.pyc b/chirp/ui/__pycache__/inputdialog.cpython-37.pyc new file mode 100644 index 0000000..c070775 Binary files /dev/null and b/chirp/ui/__pycache__/inputdialog.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/inputdialog.cpython-38.pyc b/chirp/ui/__pycache__/inputdialog.cpython-38.pyc new file mode 100644 index 0000000..abb11ce Binary files /dev/null and b/chirp/ui/__pycache__/inputdialog.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/mainapp.cpython-37.pyc b/chirp/ui/__pycache__/mainapp.cpython-37.pyc new file mode 100644 index 0000000..539d659 Binary files /dev/null and b/chirp/ui/__pycache__/mainapp.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/mainapp.cpython-38.pyc b/chirp/ui/__pycache__/mainapp.cpython-38.pyc new file mode 100644 index 0000000..e73a6a6 Binary files /dev/null and b/chirp/ui/__pycache__/mainapp.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/memdetail.cpython-37.pyc b/chirp/ui/__pycache__/memdetail.cpython-37.pyc new file mode 100644 index 0000000..8f2afc4 Binary files /dev/null and b/chirp/ui/__pycache__/memdetail.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/memdetail.cpython-38.pyc b/chirp/ui/__pycache__/memdetail.cpython-38.pyc new file mode 100644 index 0000000..7fdfed6 Binary files /dev/null and b/chirp/ui/__pycache__/memdetail.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/memedit.cpython-37.pyc b/chirp/ui/__pycache__/memedit.cpython-37.pyc new file mode 100644 index 0000000..992b398 Binary files /dev/null and b/chirp/ui/__pycache__/memedit.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/memedit.cpython-38.pyc b/chirp/ui/__pycache__/memedit.cpython-38.pyc new file mode 100644 index 0000000..e8ca6f0 Binary files /dev/null and b/chirp/ui/__pycache__/memedit.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/miscwidgets.cpython-37.pyc b/chirp/ui/__pycache__/miscwidgets.cpython-37.pyc new file mode 100644 index 0000000..24e7e5f Binary files /dev/null and b/chirp/ui/__pycache__/miscwidgets.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/miscwidgets.cpython-38.pyc b/chirp/ui/__pycache__/miscwidgets.cpython-38.pyc new file mode 100644 index 0000000..2630e34 Binary files /dev/null and b/chirp/ui/__pycache__/miscwidgets.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/radiobrowser.cpython-37.pyc b/chirp/ui/__pycache__/radiobrowser.cpython-37.pyc new file mode 100644 index 0000000..1c30de1 Binary files /dev/null and b/chirp/ui/__pycache__/radiobrowser.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/radiobrowser.cpython-38.pyc b/chirp/ui/__pycache__/radiobrowser.cpython-38.pyc new file mode 100644 index 0000000..d5cd44d Binary files /dev/null and b/chirp/ui/__pycache__/radiobrowser.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/reporting.cpython-37.pyc b/chirp/ui/__pycache__/reporting.cpython-37.pyc new file mode 100644 index 0000000..3af8c90 Binary files /dev/null and b/chirp/ui/__pycache__/reporting.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/reporting.cpython-38.pyc b/chirp/ui/__pycache__/reporting.cpython-38.pyc new file mode 100644 index 0000000..c41a673 Binary files /dev/null and b/chirp/ui/__pycache__/reporting.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/settingsedit.cpython-37.pyc b/chirp/ui/__pycache__/settingsedit.cpython-37.pyc new file mode 100644 index 0000000..d7011db Binary files /dev/null and b/chirp/ui/__pycache__/settingsedit.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/settingsedit.cpython-38.pyc b/chirp/ui/__pycache__/settingsedit.cpython-38.pyc new file mode 100644 index 0000000..a5bab7d Binary files /dev/null and b/chirp/ui/__pycache__/settingsedit.cpython-38.pyc differ diff --git a/chirp/ui/__pycache__/shiftdialog.cpython-37.pyc b/chirp/ui/__pycache__/shiftdialog.cpython-37.pyc new file mode 100644 index 0000000..ca67283 Binary files /dev/null and b/chirp/ui/__pycache__/shiftdialog.cpython-37.pyc differ diff --git a/chirp/ui/__pycache__/shiftdialog.cpython-38.pyc b/chirp/ui/__pycache__/shiftdialog.cpython-38.pyc new file mode 100644 index 0000000..dee7139 Binary files /dev/null and b/chirp/ui/__pycache__/shiftdialog.cpython-38.pyc differ diff --git a/chirp/ui/bandplans.py b/chirp/ui/bandplans.py new file mode 100644 index 0000000..483261c --- /dev/null +++ b/chirp/ui/bandplans.py @@ -0,0 +1,112 @@ +# Copyright 2013 Sean Burford +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import logging +from chirp import bandplan, bandplan_na, bandplan_au +from chirp import bandplan_iaru_r1, bandplan_iaru_r2, bandplan_iaru_r3 +from chirp.ui import inputdialog + +LOG = logging.getLogger(__name__) + + +class BandPlans(object): + def __init__(self, config): + self._config = config + self.plans = {} + + # Migrate old "automatic repeater offset" setting to + # "North American Amateur Band Plan" + ro = self._config.get("autorpt", "memedit") + if ro is not None: + self._config.set_bool("north_america", ro, "bandplan") + self._config.remove_option("autorpt", "memedit") + # And default new users to North America. + if not self._config.is_defined("north_america", "bandplan"): + self._config.set_bool("north_america", True, "bandplan") + + for plan in (bandplan_na, bandplan_au, bandplan_iaru_r1, + bandplan_iaru_r2, bandplan_iaru_r3): + name = plan.DESC.get("name", plan.SHORTNAME) + self.plans[plan.SHORTNAME] = (name, plan) + + rpt_inputs = [] + for band in plan.BANDS: + # Check for duplicates. + duplicates = [x for x in plan.BANDS if x == band] + if len(duplicates) > 1: + LOG.warn("Bandplan %s has duplicates %s" % + (name, duplicates)) + # Add repeater inputs. + rpt_input = band.inverse() + if rpt_input not in plan.BANDS: + rpt_inputs.append(band.inverse()) + plan.bands = list(plan.BANDS) + plan.bands.extend(rpt_inputs) + + def get_defaults_for_frequency(self, freq): + freq = int(freq) + result = bandplan.Band((freq, freq), repr(freq)) + + for shortname, details in self.plans.items(): + if self._config.get_bool(shortname, "bandplan"): + matches = [x for x in details[1].bands if x.contains(result)] + # Add matches to defaults, favoring more specific matches. + matches = sorted(matches, key=lambda x: x.width(), + reverse=True) + for match in matches: + result.mode = match.mode or result.mode + result.step_khz = match.step_khz or result.step_khz + result.offset = match.offset or result.offset + result.duplex = match.duplex or result.duplex + result.tones = match.tones or result.tones + if match.name: + result.name = '/'.join((result.name or '', match.name)) + # Limit ourselves to one band plan match for simplicity. + # Note that if the user selects multiple band plans by editing + # the config file it will work as expected (except where plans + # conflict). + if matches: + break + + return result + + def select_bandplan(self, parent_window): + plans = ["None"] + for shortname, details in self.plans.iteritems(): + if self._config.get_bool(shortname, "bandplan"): + plans.insert(0, details[0]) + else: + plans.append(details[0]) + + d = inputdialog.ChoiceDialog(plans, parent=parent_window, + title="Choose Defaults") + d.label.set_text(_("Band plans define default channel settings for " + "frequencies in a region. Choose a band plan " + "or None for completely manual channel " + "settings.")) + d.label.set_line_wrap(True) + r = d.run() + + if r == gtk.RESPONSE_OK: + selection = d.choice.get_active_text() + for shortname, details in self.plans.iteritems(): + self._config.set_bool(shortname, selection == details[0], + "bandplan") + if selection == details[0]: + LOG.info("Selected band plan %s: %s" % + (shortname, selection)) + + d.destroy() diff --git a/chirp/ui/bankedit.py b/chirp/ui/bankedit.py new file mode 100644 index 0000000..339a42c --- /dev/null +++ b/chirp/ui/bankedit.py @@ -0,0 +1,419 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +import time +import logging + +from gobject import TYPE_INT, TYPE_STRING, TYPE_BOOLEAN + +from chirp import chirp_common +from chirp.ui import common, miscwidgets + +LOG = logging.getLogger(__name__) + + +class MappingNamesJob(common.RadioJob): + def __init__(self, model, editor, cb): + common.RadioJob.__init__(self, cb, None) + self.__model = model + self.__editor = editor + + def execute(self, radio): + self.__editor.mappings = [] + + mappings = self.__model.get_mappings() + for mapping in mappings: + self.__editor.mappings.append((mapping, mapping.get_name())) + + gobject.idle_add(self.cb, *self.cb_args) + + +class MappingNameEditor(common.Editor): + def refresh(self): + def got_mappings(): + self._keys = [] + for mapping, name in self.mappings: + self._keys.append(mapping.get_index()) + self.listw.set_item(mapping.get_index(), + mapping.get_index(), + name) + + self.listw.connect("item-set", self.mapping_changed) + + job = MappingNamesJob(self._model, self, got_mappings) + job.set_desc(_("Retrieving %s information") % self._type) + self.rthread.submit(job) + + def get_mapping_list(self): + mappings = [] + keys = self.listw.get_keys() + for key in keys: + mappings.append(self.listw.get_item(key)[2]) + + return mappings + + def mapping_changed(self, listw, key): + def cb(*args): + self.emit("changed") + + name = self.listw.get_item(key)[2] + mapping, oldname = self.mappings[self._keys.index(key)] + + def trigger_changed(*args): + self.emit("changed") + + job = common.RadioJob(trigger_changed, "set_name", name) + job.set_target(mapping) + job.set_desc(_("Setting name on %s") % self._type.lower()) + self.rthread.submit(job) + + return True + + def __init__(self, rthread, model): + super(MappingNameEditor, self).__init__(rthread) + self._model = model + self._type = common.unpluralize(model.get_name()) + + types = [(gobject.TYPE_STRING, "key"), + (gobject.TYPE_STRING, self._type), + (gobject.TYPE_STRING, _("Name"))] + + self.listw = miscwidgets.KeyedListWidget(types) + self.listw.set_editable(1, True) + self.listw.set_sort_column(0, 1) + self.listw.set_sort_column(1, -1) + self.listw.show() + + self.mappings = [] + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add_with_viewport(self.listw) + + self.root = sw + self._loaded = False + + def focus(self): + if self._loaded: + return + + self.refresh() + self._loaded = True + + def other_editor_changed(self, target_editor): + self._loaded = False + if self.is_focused(): + self.refresh_all_memories() + + def mappings_changed(self): + pass + + +class MemoryMappingsJob(common.RadioJob): + def __init__(self, model, cb, number): + common.RadioJob.__init__(self, cb, None) + self.__model = model + self.__number = number + + def execute(self, radio): + mem = radio.get_memory(self.__number) + if mem.empty: + mappings = [] + indexes = [] + else: + mappings = self.__model.get_memory_mappings(mem) + indexes = [] + if isinstance(self.__model, + chirp_common.MappingModelIndexInterface): + for mapping in mappings: + indexes.append(self.__model.get_memory_index(mem, mapping)) + self.cb(mem, mappings, indexes, *self.cb_args) + + +class MappingMembershipEditor(common.Editor): + def _number_to_path(self, number): + return (number - self._rf.memory_bounds[0],) + + def _get_next_mapping_index(self, mapping): + # NB: Only works for one-to-one models right now! + iter = self._store.get_iter_first() + indexes = [] + ncols = len(self._cols) + len(self.mappings) + while iter: + vals = self._store.get(iter, *tuple([n for n in range(0, ncols)])) + loc = vals[self.C_LOC] + index = vals[self.C_INDEX] + mappings = vals[self.C_MAPPINGS:] + if True in mappings and mappings.index(True) == mapping: + indexes.append(index) + iter = self._store.iter_next(iter) + + index_bounds = self._model.get_index_bounds() + num_indexes = index_bounds[1] - index_bounds[0] + indexes.sort() + for i in range(0, num_indexes): + if i not in indexes: + return i + index_bounds[0] # In case not zero-origin index + + return 0 # If the mapping is full, just wrap around! + + def _toggled_cb(self, rend, path, colnum): + try: + if not rend.get_sensitive(): + return + except AttributeError: + # support PyGTK < 2.22 + iter = self._store.get_iter(path) + if not self._store.get(iter, self.C_FILLED)[0]: + return + + # The mapping index is the column number, minus the 3 label columns + mapping, name = self.mappings[colnum - len(self._cols)] + loc, = self._store.get(self._store.get_iter(path), self.C_LOC) + + is_indexed = isinstance(self._model, + chirp_common.MappingModelIndexInterface) + + if rend.get_active(): + # Changing from True to False + fn = "remove_memory_from_mapping" + index = None + else: + # Changing from False to True + fn = "add_memory_to_mapping" + if is_indexed: + index = self._get_next_mapping_index(colnum - len(self._cols)) + else: + index = None + + def do_refresh_memory(*args): + # Step 2: Update our notion of the memory's mapping information + self.refresh_memory(loc) + + def do_mapping_index(result, memory): + if isinstance(result, Exception): + common.show_error("Failed to add {mem} to mapping: {err}" + .format(mem=memory.number, + err=str(result)), + parent=self.editorset.parent_window) + return + self.emit("changed") + # Step 3: Set the memory's mapping index (maybe) + if not is_indexed or index is None: + return do_refresh_memory() + + job = common.RadioJob(do_refresh_memory, + "set_memory_index", memory, mapping, index) + job.set_target(self._model) + job.set_desc(_("Updating {type} index " + "for memory {num}").format(type=self._type, + num=memory.number)) + self.rthread.submit(job) + + def do_mapping_adjustment(memory): + # Step 1: Do the mapping add/remove + job = common.RadioJob(do_mapping_index, fn, memory, mapping) + job.set_target(self._model) + job.set_cb_args(memory) + job.set_desc(_("Updating mapping information " + "for memory {num}").format(num=memory.number)) + self.rthread.submit(job) + + # Step 0: Fetch the memory + job = common.RadioJob(do_mapping_adjustment, "get_memory", loc) + job.set_desc(_("Getting memory {num}").format(num=loc)) + self.rthread.submit(job) + + def _index_edited_cb(self, rend, path, new): + loc, = self._store.get(self._store.get_iter(path), self.C_LOC) + + def refresh_memory(*args): + self.refresh_memory(loc) + + def set_index(mappings, memory): + self.emit("changed") + # Step 2: Set the index + job = common.RadioJob(refresh_memory, "set_memory_index", + memory, mappings[0], int(new)) + job.set_target(self._model) + job.set_desc(_("Setting index " + "for memory {num}").format(num=memory.number)) + self.rthread.submit(job) + + def get_mapping(memory): + # Step 1: Get the first/only mapping + job = common.RadioJob(set_index, "get_memory_mappings", memory) + job.set_cb_args(memory) + job.set_target(self._model) + job.set_desc(_("Getting {type} for " + "memory {num}").format(type=self._type, + num=memory.number)) + self.rthread.submit(job) + + # Step 0: Get the memory + job = common.RadioJob(get_mapping, "get_memory", loc) + job.set_desc(_("Getting memory {num}").format(num=loc)) + self.rthread.submit(job) + + def __init__(self, rthread, editorset, model): + super(MappingMembershipEditor, self).__init__(rthread) + + self.editorset = editorset + self._rf = rthread.radio.get_features() + self._model = model + self._type = common.unpluralize(model.get_name()) + + self._view_cols = [ + (_("Loc"), TYPE_INT, gtk.CellRendererText, ), + (_("Frequency"), TYPE_STRING, gtk.CellRendererText, ), + (_("Name"), TYPE_STRING, gtk.CellRendererText, ), + (_("Index"), TYPE_INT, gtk.CellRendererText, ), + ] + + self._cols = [ + ("_filled", TYPE_BOOLEAN, None, ), + ] + self._view_cols + + self.C_FILLED = 0 + self.C_LOC = 1 + self.C_FREQ = 2 + self.C_NAME = 3 + self.C_INDEX = 4 + self.C_MAPPINGS = 5 # and beyond + + cols = list(self._cols) + + self._index_cache = [] + + for i in range(0, self._model.get_num_mappings()): + label = "%s %i" % (self._type, (i+1)) + cols.append((label, TYPE_BOOLEAN, gtk.CellRendererToggle)) + + self._store = gtk.ListStore(*tuple([y for x, y, z in cols])) + self._view = gtk.TreeView(self._store) + + is_indexed = isinstance(self._model, + chirp_common.MappingModelIndexInterface) + + colnum = 0 + for label, dtype, rtype in cols: + if not rtype: + colnum += 1 + continue + rend = rtype() + if dtype == TYPE_BOOLEAN: + rend.set_property("activatable", True) + rend.connect("toggled", self._toggled_cb, colnum) + col = gtk.TreeViewColumn(label, rend, active=colnum, + sensitive=self.C_FILLED) + else: + col = gtk.TreeViewColumn(label, rend, text=colnum, + sensitive=self.C_FILLED) + + self._view.append_column(col) + col.set_resizable(True) + if colnum == self.C_NAME: + col.set_visible(self._rf.has_name) + elif colnum == self.C_INDEX: + rend.set_property("editable", True) + rend.connect("edited", self._index_edited_cb) + col.set_visible(is_indexed) + colnum += 1 + + # A non-rendered column to absorb extra space in the row + self._view.append_column(gtk.TreeViewColumn()) + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add(self._view) + self._view.show() + + (min, max) = self._rf.memory_bounds + for i in range(min, max+1): + iter = self._store.append() + self._store.set(iter, + self.C_FILLED, False, + self.C_LOC, i, + self.C_FREQ, "", + self.C_NAME, "", + self.C_INDEX, 0) + + self.root = sw + self._loaded = False + + def refresh_memory(self, number): + def got_mem(memory, mappings, indexes): + iter = self._store.get_iter(self._number_to_path(memory.number)) + row = [self.C_FILLED, not memory.empty, + self.C_LOC, memory.number, + self.C_FREQ, chirp_common.format_freq(memory.freq), + self.C_NAME, memory.name, + # Hack for only one index right now + self.C_INDEX, indexes and indexes[0] or 0, + ] + for i in range(0, len(self.mappings)): + row.append(i + len(self._cols)) + row.append(self.mappings[i][0] in mappings) + + self._store.set(iter, *tuple(row)) + + job = MemoryMappingsJob(self._model, got_mem, number) + job.set_desc(_("Getting {type} information " + "for memory {num}").format(type=self._type, num=number)) + self.rthread.submit(job) + + def refresh_all_memories(self): + start = time.time() + (min, max) = self._rf.memory_bounds + for i in range(min, max+1): + self.refresh_memory(i) + LOG.debug("Got all %s info in %s" % + (self._type, (time.time() - start))) + + def refresh_mappings(self, and_memories=False): + def got_mappings(): + for i in range(len(self._cols) - len(self._view_cols) - 1, + len(self.mappings)): + col = self._view.get_column(i + len(self._view_cols)) + mapping, name = self.mappings[i] + if name: + col.set_title(name) + else: + col.set_title("(%s)" % i) + if and_memories: + self.refresh_all_memories() + + job = MappingNamesJob(self._model, self, got_mappings) + job.set_desc(_("Getting %s information") % self._type) + self.rthread.submit(job) + + def focus(self): + common.Editor.focus(self) + if self._loaded: + return + + self.refresh_mappings(True) + + self._loaded = True + + def other_editor_changed(self, target_editor): + self._loaded = False + if self.is_focused(): + self.refresh_all_memories() + + def mappings_changed(self): + self.refresh_mappings() diff --git a/chirp/ui/clone.py b/chirp/ui/clone.py new file mode 100644 index 0000000..e659a8a --- /dev/null +++ b/chirp/ui/clone.py @@ -0,0 +1,278 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import collections +import threading +import logging +import os + +import gtk +import gobject + +from chirp import platform, directory, detect, chirp_common +from chirp.ui import miscwidgets, cloneprog, inputdialog, common, config + +LOG = logging.getLogger(__name__) + +AUTO_DETECT_STRING = "Auto Detect (Icom Only)" + + +class CloneSettings: + def __init__(self): + self.port = None + self.radio_class = None + + def __str__(self): + s = "" + if self.radio_class: + return _("{vendor} {model} on {port}").format( + vendor=self.radio_class.VENDOR, + model=self.radio_class.MODEL, + port=self.port) + + +class CloneSettingsDialog(gtk.Dialog): + def __make_field(self, label, widget): + l = gtk.Label(label) + self.__table.attach(l, 0, 1, self.__row, self.__row+1) + self.__table.attach(widget, 1, 2, self.__row, self.__row+1) + self.__row += 1 + + l.show() + widget.show() + + def __make_port(self, port): + conf = config.get("state") + + ports = platform.get_platform().list_serial_ports() + if not port: + if conf.get("last_port"): + port = conf.get("last_port") + elif ports: + port = ports[0] + else: + port = "" + if port not in ports: + ports.insert(0, port) + + return miscwidgets.make_choice(ports, True, port) + + def __make_model(self): + return miscwidgets.make_choice([], False) + + def __make_vendor(self, modelbox): + vendors = collections.defaultdict(list) + for name, rclass in sorted(directory.DRV_TO_RADIO.items()): + if not issubclass(rclass, chirp_common.CloneModeRadio) and \ + not issubclass(rclass, chirp_common.LiveRadio): + continue + + vendors[rclass.VENDOR].append(rclass) + for alias in rclass.ALIASES: + vendors[alias.VENDOR].append(alias) + + self.__vendors = vendors + + conf = config.get("state") + if not conf.get("last_vendor"): + conf.set("last_vendor", sorted(vendors.keys())[0]) + + last_vendor = conf.get("last_vendor") + if last_vendor not in list(vendors.keys()): + last_vendor = list(vendors.keys())[0] + + v = miscwidgets.make_choice(sorted(vendors.keys()), False, last_vendor) + + def _changed(box, vendors, boxes): + (vendorbox, modelbox) = boxes + models = vendors[vendorbox.value] + + added_models = [] + + model_store = modelbox.get_model() + model_store.clear() + for rclass in sorted(models, key=lambda c: c.__name__): + if rclass.MODEL not in added_models: + model_store.append([rclass.MODEL]) + added_models.append(rclass.MODEL) + + if vendorbox.value in detect.DETECT_FUNCTIONS: + model_store.append([_("Detect")]) + added_models.insert(0, _("Detect")) + + model_names = [x.MODEL for x in models] + if conf.get("last_model") in model_names: + modelbox.value = conf.get("last_model") + elif added_models: + modelbox.value = added_models[0] + + v.widget.connect("changed", _changed, vendors, (v, modelbox)) + _changed(v, vendors, (v, modelbox)) + + return v + + def __make_ui(self, settings): + self.__table = gtk.Table(3, 2) + self.__table.set_row_spacings(3) + self.__table.set_col_spacings(10) + self.__row = 0 + + self.__port = self.__make_port(settings and settings.port or None) + self.__modl = self.__make_model() + self.__vend = self.__make_vendor(self.__modl) + + self.__make_field(_("Port"), self.__port.widget) + self.__make_field(_("Vendor"), self.__vend.widget) + self.__make_field(_("Model"), self.__modl.widget) + + if settings and settings.radio_class: + self.__vend.value = settings.radio_class.VENDOR + self.__modl.value = settings.radio_class.MODEL + self.__vend.widget.set_sensitive(False) + self.__modl.widget.set_sensitive(False) + + self.__table.show() + self.vbox.pack_start(self.__table, 1, 1, 1) + + def __init__(self, settings=None, parent=None, title=_("Radio")): + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + gtk.Dialog.__init__(self, title, + parent=parent, + flags=gtk.DIALOG_MODAL) + self.__make_ui(settings) + self.__cancel_button = self.add_button(gtk.STOCK_CANCEL, + gtk.RESPONSE_CANCEL) + self.__okay_button = self.add_button(gtk.STOCK_OK, + gtk.RESPONSE_OK) + self.__okay_button.grab_default() + self.__okay_button.grab_focus() + + def run(self): + r = gtk.Dialog.run(self) + if r != gtk.RESPONSE_OK: + return None + + vendor = self.__vend.value + model = self.__modl.value + + cs = CloneSettings() + cs.port = self.__port.value + if model == _("Detect"): + try: + cs.radio_class = detect.DETECT_FUNCTIONS[vendor](cs.port) + if not cs.radio_class: + raise Exception( + _("Unable to detect radio on {port}").format( + port=cs.port)) + except Exception as e: + d = inputdialog.ExceptionDialog(e) + d.run() + d.destroy() + return None + else: + for rclass in list(directory.DRV_TO_RADIO.values()): + if rclass.MODEL == model: + cs.radio_class = rclass + break + alias_match = None + for alias in rclass.ALIASES: + if alias.MODEL == model: + alias_match = rclass + alias_class = alias + break + if alias_match: + + class DynamicRadioAlias(rclass): + VENDOR = alias.VENDOR + MODEL = alias.MODEL + VARIANT = alias.VARIANT + + cs.radio_class = DynamicRadioAlias + LOG.debug( + 'Chose %s alias for %s because model %s selected' % ( + alias_match, cs.radio_class, model)) + break + if not cs.radio_class: + common.show_error( + _("Internal error: Unable to upload to {model}").format( + model=model)) + LOG.info(self.__vendors) + return None + + conf = config.get("state") + conf.set("last_port", cs.port) + conf.set("last_vendor", cs.radio_class.VENDOR) + conf.set("last_model", model) + + return cs + + +class CloneCancelledException(Exception): + pass + + +class CloneThread(threading.Thread): + def __status(self, status): + gobject.idle_add(self.__progw.status, status) + + def __init__(self, radio, direction, cb=None, parent=None): + threading.Thread.__init__(self) + + self.__radio = radio + self.__out = direction == "out" + self.__cback = cb + self.__cancelled = False + + self.__progw = cloneprog.CloneProg(parent=parent, cancel=self.cancel) + + def cancel(self): + self.__radio.pipe.close() + self.__cancelled = True + + def run(self): + LOG.debug("Clone thread started") + + gobject.idle_add(self.__progw.show) + + self.__radio.status_fn = self.__status + + try: + if self.__out: + self.__radio.sync_out() + else: + self.__radio.sync_in() + + emsg = None + except Exception as e: + common.log_exception() + LOG.error(_("Clone failed: {error}").format(error=e)) + emsg = e + + gobject.idle_add(self.__progw.hide) + + # NB: Compulsory close of the radio's serial connection + self.__radio.pipe.close() + + LOG.debug("Clone thread ended") + + if self.__cback and not self.__cancelled: + gobject.idle_add(self.__cback, self.__radio, emsg) + + +if __name__ == "__main__": + d = CloneSettingsDialog("/dev/ttyUSB0") + r = d.run() + print(r) diff --git a/chirp/ui/cloneprog.py b/chirp/ui/cloneprog.py new file mode 100644 index 0000000..99beea9 --- /dev/null +++ b/chirp/ui/cloneprog.py @@ -0,0 +1,66 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk + + +class CloneProg(gtk.Window): + def __init__(self, **args): + if "parent" in args: + parent = args["parent"] + del args["parent"] + else: + parent = None + + if "cancel" in args: + cancel = args["cancel"] + del args["cancel"] + else: + cancel = None + + gtk.Window.__init__(self, **args) + + self.set_transient_for(parent) + self.set_modal(True) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.set_position(gtk.WIN_POS_CENTER_ON_PARENT) + + vbox = gtk.VBox(False, 2) + vbox.show() + self.add(vbox) + + self.set_title(_("Clone Progress")) + self.set_resizable(False) + + self.infolabel = gtk.Label(_("Cloning")) + self.infolabel.show() + vbox.pack_start(self.infolabel, 1, 1, 1) + + self.progbar = gtk.ProgressBar() + self.progbar.set_fraction(0.0) + self.progbar.show() + vbox.pack_start(self.progbar, 0, 0, 0) + + cancel_b = gtk.Button(_("Cancel")) + cancel_b.connect("clicked", lambda b: cancel()) + cancel_b.show() + vbox.pack_start(cancel_b, 0, 0, 0) + + def status(self, _status): + self.infolabel.set_text(_status.msg) + + if _status.cur > _status.max: + _status.cur = _status.max + self.progbar.set_fraction(_status.cur / float(_status.max)) diff --git a/chirp/ui/common.py b/chirp/ui/common.py new file mode 100644 index 0000000..a6b90ee --- /dev/null +++ b/chirp/ui/common.py @@ -0,0 +1,451 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +import pango + +import threading +import time +import os +import traceback +import logging + +from chirp import errors +from chirp.ui import reporting, config + +LOG = logging.getLogger(__name__) + +CONF = config.get() + + +class Editor(gobject.GObject): + __gsignals__ = { + 'changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'usermsg': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + } + + root = None + + def __init__(self, rthread): + gobject.GObject.__init__(self) + self.read_only = False + self._focused = False + self.rthread = rthread + + def is_focused(self): + return self._focused + + def focus(self): + self._focused = True + + def unfocus(self): + self._focused = False + + def copy_selection(self, cut=False): + pass + + def paste_selection(self): + pass + + def hotkey(self, action): + pass + + def set_read_only(self, read_only): + self.read_only = read_only + + def get_read_only(self): + return self.read_only + + def prepare_close(self): + pass + + def other_editor_changed(self, editor): + pass + +gobject.type_register(Editor) + + +def DBG(*args): + if False: + LOG.debug(" ".join(args)) + + +class RadioJob: + def __init__(self, cb, func, *args, **kwargs): + self.cb = cb + self.cb_args = () + self.func = func + self.args = args + self.kwargs = kwargs + self.desc = "Working" + self.target = None + self.tb = traceback.format_stack() + + def __str__(self): + return "RadioJob(%s,%s,%s)" % (self.func, self.args, self.kwargs) + + def set_desc(self, desc): + self.desc = desc + + def set_cb_args(self, *args): + self.cb_args = args + + def set_target(self, target): + self.target = target + + def _execute(self, target, func): + try: + DBG("Running %s (%s %s)" % (self.func, + str(self.args), + str(self.kwargs))) + DBG(self.desc) + result = func(*self.args, **self.kwargs) + except errors.InvalidMemoryLocation as e: + result = e + except Exception as e: + LOG.error("Exception running RadioJob: %s" % e) + log_exception() + LOG.error("Job Args: %s" % str(self.args)) + LOG.error("Job KWArgs: %s" % str(self.kwargs)) + LOG.error("Job Called from:%s%s" % + (os.linesep, "".join(self.tb[:-1]))) + result = e + + if self.cb: + gobject.idle_add(self.cb, result, *self.cb_args) + + def execute(self, radio): + if not self.target: + self.target = radio + + try: + func = getattr(self.target, self.func) + except AttributeError as e: + LOG.error("No such radio function `%s' in %s" % + (self.func, self.target)) + return + + self._execute(self.target, func) + + +class RadioThread(threading.Thread, gobject.GObject): + __gsignals__ = { + "status": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + } + + def __init__(self, radio, parent=None): + threading.Thread.__init__(self) + gobject.GObject.__init__(self) + self.__queue = {} + if parent: + self.__runlock = parent._get_run_lock() + self.status = lambda msg: parent.status(msg) + else: + self.__runlock = threading.Lock() + self.status = self._status + + self.__counter = threading.Semaphore(0) + self.__lock = threading.Lock() + + self.__enabled = True + self.radio = radio + + def _get_run_lock(self): + return self.__runlock + + def _qlock(self): + self.__lock.acquire() + + def _qunlock(self): + self.__lock.release() + + def _qsubmit(self, job, priority): + if priority not in self.__queue: + self.__queue[priority] = [] + + self.__queue[priority].append(job) + self.__counter.release() + + def _queue_clear_below(self, priority): + for i in range(0, priority): + if i in self.__queue and len(self.__queue[i]) != 0: + return False + + return True + + def _qlock_when_idle(self, priority=10): + while True: + DBG("Attempting queue lock (%i)" % len(self.__queue)) + self._qlock() + if self._queue_clear_below(priority): + return + self._qunlock() + time.sleep(0.1) + + # This is the external lock, which stops any threads from running + # so that the radio can be operated synchronously + def lock(self): + self.__runlock.acquire() + + def unlock(self): + self.__runlock.release() + + def submit(self, job, priority=0): + self._qlock() + self._qsubmit(job, priority) + self._qunlock() + + def flush(self, priority=None): + self._qlock() + + if priority is None: + for i in self.__queue.keys(): + self.__queue[i] = [] + else: + self.__queue[priority] = [] + + self._qunlock() + + def stop(self): + self.flush() + self.__counter.release() + self.__enabled = False + + def _status(self, msg): + jobs = 0 + for i in dict(self.__queue): + jobs += len(self.__queue[i]) + gobject.idle_add(self.emit, "status", "[%i] %s" % (jobs, msg)) + + def _queue_pop(self, priority): + try: + return self.__queue[priority].pop(0) + except IndexError: + return None + + def run(self): + last_job_desc = "idle" + while self.__enabled: + DBG("Waiting for a job") + if last_job_desc: + self.status(_("Completed") + " " + last_job_desc + + " (" + _("idle") + ")") + self.__counter.acquire() + + self._qlock() + for i in sorted(self.__queue.keys()): + job = self._queue_pop(i) + if job: + DBG("Running job at priority %i" % i) + break + self._qunlock() + + if job: + self.lock() + self.status(job.desc) + job.execute(self.radio) + last_job_desc = job.desc + self.unlock() + + LOG.debug("RadioThread exiting") + + +def log_exception(): + import traceback + import sys + + reporting.report_exception(traceback.format_exc(limit=30)) + + LOG.error("-- Exception: --") + LOG.error(traceback.format_exc(limit=30)) + LOG.error("----------------") + + +def show_error(msg, parent=None): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=parent, + type=gtk.MESSAGE_ERROR) + d.set_property("text", msg) + + if not parent: + d.set_position(gtk.WIN_POS_CENTER_ALWAYS) + + d.run() + d.destroy() + + +def ask_yesno_question(msg, parent=None): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=parent, + type=gtk.MESSAGE_QUESTION) + d.set_property("text", msg) + + if not parent: + d.set_position(gtk.WIN_POS_CENTER_ALWAYS) + + r = d.run() + d.destroy() + + return r == gtk.RESPONSE_YES + + +def combo_select(box, value): + store = box.get_model() + iter = store.get_iter_first() + while iter: + if store.get(iter, 0)[0] == value: + box.set_active_iter(iter) + return True + iter = store.iter_next(iter) + + return False + + +def _add_text(d, text): + v = gtk.TextView() + v.get_buffer().set_text(text) + v.set_editable(False) + v.set_cursor_visible(False) + v.show() + sw = gtk.ScrolledWindow() + sw.add(v) + sw.show() + d.vbox.pack_start(sw, 1, 1, 1) + return v + + +def show_error_text(msg, text, parent=None): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=parent, + type=gtk.MESSAGE_ERROR) + d.set_property("text", msg) + + _add_text(d, text) + if not parent: + d.set_position(gtk.WIN_POS_CENTER_ALWAYS) + + d.set_size_request(600, 400) + d.run() + d.destroy() + + +def show_warning(msg, text, + parent=None, buttons=None, title="Warning", + can_squelch=False): + if buttons is None: + buttons = gtk.BUTTONS_OK + d = gtk.MessageDialog(buttons=buttons, + parent=parent, + type=gtk.MESSAGE_WARNING) + d.set_title(title) + d.set_property("text", msg) + l = gtk.Label(_("Details") + ":") + l.show() + d.vbox.pack_start(l, 0, 0, 0) + l = gtk.Label(_("Proceed?")) + l.show() + d.get_action_area().pack_start(l, 0, 0, 0) + d.get_action_area().reorder_child(l, 0) + textview = _add_text(d, text) + textview.set_wrap_mode(gtk.WRAP_WORD) + if not parent: + d.set_position(gtk.WIN_POS_CENTER_ALWAYS) + if can_squelch: + cb = gtk.CheckButton(_("Do not show this next time")) + cb.show() + d.vbox.pack_start(cb, 0, 0, 0) + + d.set_size_request(600, 400) + r = d.run() + d.destroy() + if can_squelch: + return r, cb.get_active() + return r + + +def simple_diff(a, b, diffsonly=False): + lines_a = a.split(os.linesep) + lines_b = b.split(os.linesep) + blankprinted = True + + diff = "" + for i in range(0, len(lines_a)): + if lines_a[i] != lines_b[i]: + diff += "-%s%s" % (lines_a[i], os.linesep) + diff += "+%s%s" % (lines_b[i], os.linesep) + blankprinted = False + elif diffsonly is True: + if blankprinted: + continue + diff += os.linesep + blankprinted = True + else: + diff += " %s%s" % (lines_a[i], os.linesep) + return diff + + +# A quick hacked up tool to show a blob of text in a dialog window +# using fixed-width fonts. It also highlights lines that start with +# a '-' in red bold font and '+' with blue bold font. +def show_diff_blob(title, result): + d = gtk.Dialog(title=title, + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK)) + b = gtk.TextBuffer() + + tags = b.get_tag_table() + for color in ["red", "blue", "green", "grey"]: + tag = gtk.TextTag(color) + tag.set_property("foreground", color) + tags.add(tag) + tag = gtk.TextTag("bold") + tag.set_property("weight", pango.WEIGHT_BOLD) + tags.add(tag) + + try: + fontsize = CONF.get_int("diff_fontsize", "developer") + except Exception: + fontsize = 11 + if fontsize < 4 or fontsize > 144: + LOG.info("Unsupported diff_fontsize %i. Using 11." % fontsize) + fontsize = 11 + + lines = result.split(os.linesep) + for line in lines: + if line.startswith("-"): + tags = ("red", "bold") + elif line.startswith("+"): + tags = ("blue", "bold") + else: + tags = () + b.insert_with_tags_by_name(b.get_end_iter(), line + os.linesep, *tags) + v = gtk.TextView(b) + fontdesc = pango.FontDescription("Courier %i" % fontsize) + v.modify_font(fontdesc) + v.set_editable(False) + v.show() + s = gtk.ScrolledWindow() + s.add(v) + s.show() + d.vbox.pack_start(s, 1, 1, 1) + d.set_size_request(600, 400) + d.run() + d.destroy() + + +def unpluralize(string): + if string.endswith("s"): + return string[:-1] + else: + return string diff --git a/chirp/ui/compat.py b/chirp/ui/compat.py new file mode 100644 index 0000000..b4ac7d2 --- /dev/null +++ b/chirp/ui/compat.py @@ -0,0 +1,82 @@ +import contextlib +import logging +import serial as base_serial +import six + +try: + import gtk +except ImportError: + # FIXME + # For chirpc + gtk = None + +from chirp import bitwise + +LOG = logging.getLogger('uicompat') + + +@contextlib.contextmanager +def py3safe(quiet=False): + try: + yield + except Exception as e: + if not quiet: + LOG.exception('FIXMEPY3: %s' % e) + + +def SpinButton(adj): + try: + return gtk.SpinButton(adj) + except TypeError: + sb = gtk.SpinButton() + sb.configure(adj, 1.0, 0) + return sb + +def Frame(label): + try: + return gtk.Frame(label) + except TypeError: + f = gtk.Frame() + f.set_label(label) + return f + + +class CompatSerial(base_serial.Serial): + """A PY2-compatible Serial class + + This wraps serial.Serial to provide translation between + hex-char-having unicode strings in radios and the bytes-wanting + serial channel. See bitwise.string_straigth_encode() and + bitwise.string_straight_decode() for more details. + + This should only be used as a bridge for older drivers until + they can be rewritten. + """ + def write(self, data): + data = bitwise.string_straight_encode(data) + return super(CompatSerial, self).write(data) + + def read(self, count): + data = super(CompatSerial, self).read(count) + return bitwise.string_straight_decode(data) + + @classmethod + def get(cls, needs_compat, *args, **kwargs): + if six.PY3 and needs_compat: + return cls(*args, **kwargs) + else: + return base_serial.Serial(*args, **kwargs) + + +class CompatTooltips(object): + def __init__(self): + try: + self.tips = gtk.Tooltips() + except AttributeError: + self.tips = None + + def set_tip(self, widget, tip): + if self.tips: + self.tips.set_tip(widget, tip) + else: + widget.set_tooltip_text(tip) diff --git a/chirp/ui/config.py b/chirp/ui/config.py new file mode 100644 index 0000000..3867d4a --- /dev/null +++ b/chirp/ui/config.py @@ -0,0 +1,131 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from chirp import platform +try: + from ConfigParser import ConfigParser +except ImportError: + from configparser import ConfigParser +import os + + +class ChirpConfig: + def __init__(self, basepath, name="chirp.config"): + self.__basepath = basepath + self.__name = name + + self._default_section = "global" + + self.__config = ConfigParser() + + cfg = os.path.join(basepath, name) + if os.path.exists(cfg): + self.__config.read(cfg) + + def save(self): + cfg = os.path.join(self.__basepath, self.__name) + with open(cfg, "w") as cfg_file: + self.__config.write(cfg_file) + + def get(self, key, section, raw=False): + if not self.__config.has_section(section): + return None + + if not self.__config.has_option(section, key): + return None + + return self.__config.get(section, key, raw=raw) + + def set(self, key, value, section): + if not self.__config.has_section(section): + self.__config.add_section(section) + + self.__config.set(section, key, value) + + def is_defined(self, key, section): + return self.__config.has_option(section, key) + + def remove_option(self, section, key): + self.__config.remove_option(section, key) + + if not self.__config.items(section): + self.__config.remove_section(section) + + +class ChirpConfigProxy: + def __init__(self, config, section="global"): + self._config = config + self._section = section + + def get(self, key, section=None, raw=False): + return self._config.get(key, section or self._section, + raw=raw) + + def set(self, key, value, section=None): + return self._config.set(key, value, section or self._section) + + def get_int(self, key, section=None): + try: + return int(self.get(key, section)) + except ValueError: + return 0 + + def set_int(self, key, value, section=None): + if not isinstance(value, int): + raise ValueError("Value is not an integer") + + self.set(key, "%i" % value, section) + + def get_float(self, key, section=None): + try: + return float(self.get(key, section)) + except ValueError: + return 0 + + def set_float(self, key, value, section=None): + if not isinstance(value, float): + raise ValueError("Value is not an integer") + + self.set(key, "%i" % value, section) + + def get_bool(self, key, section=None, default=False): + val = self.get(key, section) + if val is None: + return default + else: + return val == "True" + + def set_bool(self, key, value, section=None): + self.set(key, str(bool(value)), section) + + def is_defined(self, key, section=None): + return self._config.is_defined(key, section or self._section) + + def remove_option(self, key, section): + self._config.remove_option(section, key) + + +_CONFIG = None + + +def get(section="global"): + global _CONFIG + + p = platform.get_platform() + + if not _CONFIG: + _CONFIG = ChirpConfig(p.config_dir()) + + return ChirpConfigProxy(_CONFIG, section) diff --git a/chirp/ui/dstaredit.py b/chirp/ui/dstaredit.py new file mode 100644 index 0000000..4d122f2 --- /dev/null +++ b/chirp/ui/dstaredit.py @@ -0,0 +1,202 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +import logging + +from chirp.ui import common, miscwidgets +from chirp.ui import compat + +LOG = logging.getLogger(__name__) + +WIDGETW = 80 +WIDGETH = 30 + + +class CallsignEditor(gtk.HBox): + __gsignals__ = { + "changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def _cs_changed(self, listw, callid): + if callid == 0 and self.first_fixed: + return False + + self.emit("changed") + + return True + + def make_list(self, width): + cols = [(gobject.TYPE_INT, ""), + (gobject.TYPE_INT, ""), + (gobject.TYPE_STRING, _("Callsign")), + ] + + self.listw = miscwidgets.KeyedListWidget(cols) + self.listw.show() + + self.listw.set_editable(1, True) + self.listw.connect("item-set", self._cs_changed) + + with compat.py3safe(): + rend = self.listw.get_renderer(1) + rend.set_property("family", "Monospace") + rend.set_property("width-chars", width) + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add_with_viewport(self.listw) + sw.show() + + return sw + + def __init__(self, first_fixed=False, width=8): + gtk.HBox.__init__(self, False, 2) + + self.first_fixed = first_fixed + + self.listw = None + + self.pack_start(self.make_list(width), 1, 1, 1) + + def set_callsigns(self, calls): + if self.first_fixed: + st = 1 + else: + st = 0 + + values = [] + i = 1 + for call in calls[st:]: + self.listw.set_item(i, i, call) + i += 1 + + def get_callsigns(self): + calls = [] + keys = self.listw.get_keys() + for key in keys: + id, idx, call = self.listw.get_item(key) + calls.append(call) + + if self.first_fixed: + calls.insert(0, "") + + return calls + + +class DStarEditor(common.Editor): + def __cs_changed(self, cse): + job = None + + LOG.debug("Callsigns: %s" % cse.get_callsigns()) + if cse == self.editor_ucall: + job = common.RadioJob(None, + "set_urcall_list", + cse.get_callsigns()) + LOG.debug("Set urcall") + elif cse == self.editor_rcall: + job = common.RadioJob(None, + "set_repeater_call_list", + cse.get_callsigns()) + LOG.debug("Set rcall") + elif cse == self.editor_mcall: + job = common.RadioJob(None, + "set_mycall_list", + cse.get_callsigns()) + + if job: + LOG.debug("Submitting job to update call lists") + self.rthread.submit(job) + + self.emit("changed") + + def make_callsigns(self): + box = gtk.HBox(True, 2) + + fixed = self.rthread.radio.get_features().has_implicit_calls + + frame = compat.Frame(_("Your callsign")) + self.editor_ucall = CallsignEditor(first_fixed=fixed) + self.editor_ucall.set_size_request(-1, 200) + self.editor_ucall.show() + frame.add(self.editor_ucall) + frame.show() + box.pack_start(frame, 1, 1, 0) + + frame = compat.Frame(_("Repeater callsign")) + self.editor_rcall = CallsignEditor(first_fixed=fixed) + self.editor_rcall.set_size_request(-1, 200) + self.editor_rcall.show() + frame.add(self.editor_rcall) + frame.show() + box.pack_start(frame, 1, 1, 0) + + frame = compat.Frame(_("My callsign")) + self.editor_mcall = CallsignEditor() + self.editor_mcall.set_size_request(-1, 200) + self.editor_mcall.show() + frame.add(self.editor_mcall) + frame.show() + box.pack_start(frame, 1, 1, 0) + + box.show() + return box + + def focus(self): + if self.loaded: + return + self.loaded = True + LOG.debug("Loading callsigns...") + + def set_ucall(calls): + self.editor_ucall.set_callsigns(calls) + self.editor_ucall.connect("changed", self.__cs_changed) + + def set_rcall(calls): + self.editor_rcall.set_callsigns(calls) + self.editor_rcall.connect("changed", self.__cs_changed) + + def set_mcall(calls): + self.editor_mcall.set_callsigns(calls) + self.editor_mcall.connect("changed", self.__cs_changed) + + job = common.RadioJob(set_ucall, "get_urcall_list") + job.set_desc(_("Downloading URCALL list")) + self.rthread.submit(job) + + job = common.RadioJob(set_rcall, "get_repeater_call_list") + job.set_desc(_("Downloading RPTCALL list")) + self.rthread.submit(job) + + job = common.RadioJob(set_mcall, "get_mycall_list") + job.set_desc(_("Downloading MYCALL list")) + self.rthread.submit(job) + + def __init__(self, rthread): + super(DStarEditor, self).__init__(rthread) + + self.loaded = False + + self.editor_ucall = self.editor_rcall = None + + vbox = gtk.VBox(False, 2) + vbox.pack_start(self.make_callsigns(), 0, 0, 0) + + tmp = gtk.Label("") + tmp.show() + vbox.pack_start(tmp, 1, 1, 1) + + self.root = vbox diff --git a/chirp/ui/editorset.py b/chirp/ui/editorset.py new file mode 100644 index 0000000..697f5ef --- /dev/null +++ b/chirp/ui/editorset.py @@ -0,0 +1,428 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import gtk +import gobject +import logging + +from chirp import chirp_common, directory +from chirp.drivers import generic_csv +from chirp.ui import memedit, dstaredit, bankedit, common, importdialog +from chirp.ui import inputdialog, reporting, settingsedit, radiobrowser, config + +LOG = logging.getLogger(__name__) + + +class EditorSet(gtk.VBox): + __gsignals__ = { + "want-close": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + "status": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + "usermsg": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + "editor-selected": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + } + + def _make_device_mapping_editors(self, device, devrthread, index): + sub_index = 0 + memory_editor = self.editors["memedit%i" % index] + mappings = device.get_mapping_models() + for mapping_model in mappings: + members = bankedit.MappingMembershipEditor(devrthread, self, + mapping_model) + label = mapping_model.get_name() + if self.rf.has_sub_devices: + label += "(%s)" % device.VARIANT + lab = gtk.Label(label) + self.tabs.append_page(members.root, lab) + self.editors["mapping_members%i%i" % (index, sub_index)] = members + + basename = common.unpluralize(mapping_model.get_name()) + names = bankedit.MappingNameEditor(devrthread, mapping_model) + label = "%s Names" % basename + if self.rf.has_sub_devices: + label += " (%s)" % device.VARIANT + lab = gtk.Label(label) + self.tabs.append_page(names.root, lab) + self.editors["mapping_names%i%i" % (index, sub_index)] = names + + members.root.show() + members.connect("changed", self.editor_changed) + if hasattr(mapping_model.get_mappings()[0], "set_name"): + names.root.show() + members.connect("changed", lambda x: names.mappings_changed()) + names.connect("changed", lambda x: members.mappings_changed()) + names.connect("changed", self.editor_changed) + sub_index += 1 + + def _make_device_editors(self, device, devrthread, index): + if isinstance(device, chirp_common.IcomDstarSupport): + memories = memedit.DstarMemoryEditor(devrthread) + else: + memories = memedit.MemoryEditor(devrthread) + + memories.connect("usermsg", lambda e, m: self.emit("usermsg", m)) + memories.connect("changed", self.editor_changed) + + if self.rf.has_sub_devices: + label = (_("Memories (%(variant)s)") % + dict(variant=device.VARIANT)) + rf = device.get_features() + else: + label = _("Memories") + rf = self.rf + lab = gtk.Label(label) + self.tabs.append_page(memories.root, lab) + memories.root.show() + self.editors["memedit%i" % index] = memories + + self._make_device_mapping_editors(device, devrthread, index) + + if isinstance(device, chirp_common.IcomDstarSupport): + editor = dstaredit.DStarEditor(devrthread) + self.tabs.append_page(editor.root, gtk.Label(_("D-STAR"))) + editor.root.show() + editor.connect("changed", self.dstar_changed, memories) + editor.connect("changed", self.editor_changed) + self.editors["dstar"] = editor + + def __init__(self, source, parent_window=None, + filename=None, tempname=None): + gtk.VBox.__init__(self, True, 0) + + self.parent_window = parent_window + + if isinstance(source, str): + self.filename = source + self.radio = directory.get_radio_by_image(self.filename) + elif isinstance(source, chirp_common.Radio): + self.radio = source + self.filename = filename or tempname or source.VARIANT + else: + raise Exception("Unknown source type") + + rthread = common.RadioThread(self.radio) + rthread.setDaemon(True) + rthread.start() + + rthread.connect("status", lambda e, m: self.emit("status", m)) + + self.tabs = gtk.Notebook() + self.tabs.connect("switch-page", self.tab_selected) + self.tabs.set_tab_pos(gtk.POS_LEFT) + + self.editors = {} + + self.rf = self.radio.get_features() + if self.rf.has_sub_devices: + devices = self.radio.get_sub_devices() + else: + devices = [self.radio] + + index = 0 + for device in devices: + devrthread = common.RadioThread(device, rthread) + devrthread.setDaemon(True) + devrthread.start() + self._make_device_editors(device, devrthread, index) + index += 1 + + if self.rf.has_settings: + editor = settingsedit.SettingsEditor(rthread) + self.tabs.append_page(editor.root, gtk.Label(_("Settings"))) + editor.root.show() + editor.connect("changed", self.editor_changed) + self.editors["settings"] = editor + + conf = config.get() + if (hasattr(self.rthread.radio, '_memobj') and + conf.get_bool("developer", "state")): + editor = radiobrowser.RadioBrowser(self.rthread) + lab = gtk.Label(_("Browser")) + self.tabs.append_page(editor.root, lab) + editor.connect("changed", self.editor_changed) + self.editors["browser"] = editor + + self.pack_start(self.tabs) + self.tabs.show() + + self.label = self.text_label = None + self.make_label() + self.modified = (tempname is not None) + if tempname: + self.filename = tempname + self.update_tab() + + def make_label(self): + self.label = gtk.HBox(False, 0) + + self.text_label = gtk.Label("") + self.text_label.show() + self.label.pack_start(self.text_label, 1, 1, 1) + + button = gtk.Button() + button.set_relief(gtk.RELIEF_NONE) + button.set_focus_on_click(False) + + icon = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + icon.show() + button.add(icon) + + button.connect("clicked", lambda x: self.emit("want-close")) + button.show() + self.label.pack_start(button, 0, 0, 0) + + self.label.show() + + def update_tab(self): + fn = os.path.basename(self.filename) + if self.modified: + text = "%s*" % fn + else: + text = fn + + self.text_label.set_text(self.radio.get_name() + ": " + text) + + def save(self, fname=None): + if not fname: + fname = self.filename + if not os.path.exists(self.filename): + return # Probably before the first "Save as" + else: + self.filename = fname + + self.rthread.lock() + try: + self.radio.save(fname) + except: + self.rthread.unlock() + raise + self.rthread.unlock() + + self.modified = False + self.update_tab() + + def dstar_changed(self, dstared, memedit): + memedit.set_urcall_list(dstared.editor_ucall.get_callsigns()) + memedit.set_repeater_list(dstared.editor_rcall.get_callsigns()) + memedit.prefill() + + def editor_changed(self, target_editor=None): + LOG.debug("%s changed" % target_editor) + if not isinstance(self.radio, chirp_common.LiveRadio): + self.modified = True + self.update_tab() + for editor in list(self.editors.values()): + if editor != target_editor: + editor.other_editor_changed(target_editor) + + def get_tab_label(self): + return self.label + + def is_modified(self): + return self.modified + + def _do_import_locked(self, dlgclass, src_radio, dst_rthread): + + # An import/export action needs to be done in the absence of any + # other queued changes. So, we make sure that nothing else is + # staged for the thread and lock it up. Then we use the hidden + # interface to queue our own changes before opening it up to the + # rest of the world. + + dst_rthread._qlock_when_idle(5) # Suspend job submission when idle + + dialog = dlgclass(src_radio, dst_rthread.radio, self.parent_window) + r = dialog.run() + dialog.hide() + if r != gtk.RESPONSE_OK: + dst_rthread._qunlock() + return + + count = dialog.do_import(dst_rthread) + LOG.debug("Imported %i" % count) + dst_rthread._qunlock() + + if count > 0: + self.editor_changed() + current_editor = self.get_current_editor() + gobject.idle_add(current_editor.prefill) + + return count + + def choose_sub_device(self, radio): + devices = radio.get_sub_devices() + choices = [x.VARIANT for x in devices] + + d = inputdialog.ChoiceDialog(choices) + text = _("The {vendor} {model} has multiple independent sub-devices") + d.label.set_text(text.format(vendor=radio.VENDOR, model=radio.MODEL) + + os.linesep + _("Choose one to import from:")) + r = d.run() + chosen = d.choice.get_active_text() + d.destroy() + if r == gtk.RESPONSE_CANCEL: + raise Exception(_("Cancelled")) + for d in devices: + if d.VARIANT == chosen: + return d + + raise Exception(_("Internal Error")) + + def do_import(self, filen): + current_editor = self.get_current_editor() + if not isinstance(current_editor, memedit.MemoryEditor): + # FIXME: We need a nice message to let the user know that they + # need to select the appropriate memory editor tab before doing + # and import so that we know which thread and editor to import + # into and refresh. This will do for the moment. + common.show_error("Memory editor must be selected before import") + try: + src_radio = directory.get_radio_by_image(filen) + except Exception as e: + common.show_error(e) + return + + if isinstance(src_radio, chirp_common.NetworkSourceRadio): + ww = importdialog.WaitWindow("Querying...", self.parent_window) + ww.show() + + def status(status): + ww.set(float(status.cur) / float(status.max)) + + try: + src_radio.status_fn = status + src_radio.do_fetch() + except Exception as e: + common.show_error(e) + ww.hide() + return + ww.hide() + + try: + if src_radio.get_features().has_sub_devices: + src_radio = self.choose_sub_device(src_radio) + except Exception as e: + common.show_error(e) + return + + if len(src_radio.errors) > 0: + _filen = os.path.basename(filen) + common.show_error_text(_("There were errors while opening {file}. " + "The affected memories will not " + "be importable!").format(file=_filen), + "\r\n".join(src_radio.errors)) + + try: + count = self._do_import_locked(importdialog.ImportDialog, + src_radio, + self.rthread) + reporting.report_model_usage(src_radio, "importsrc", True) + except Exception as e: + common.log_exception() + common.show_error(_("There was an error during " + "import: {error}").format(error=e)) + + def do_export(self, filen): + try: + if filen.lower().endswith(".csv"): + dst_radio = generic_csv.CSVRadio(filen) + else: + raise Exception(_("Unsupported file type")) + except Exception as e: + common.log_exception() + common.show_error(e) + return + + dst_rthread = common.RadioThread(dst_radio) + dst_rthread.setDaemon(True) + dst_rthread.start() + + try: + count = self._do_import_locked(importdialog.ExportDialog, + self.rthread.radio, + dst_rthread) + except Exception as e: + common.log_exception() + common.show_error(_("There was an error during " + "export: {error}").format(error=e), + self.parent_window) + return + + if count <= 0: + return + + # Wait for thread queue to complete + dst_rthread._qlock_when_idle() + + try: + dst_radio.save(filename=filen) + except Exception as e: + common.log_exception() + common.show_error(_("There was an error during " + "export: {error}").format(error=e), + self) + + def prime(self): + # NOTE: this is only called to prime new CSV files, so assume + # only one memory editor for now + mem = chirp_common.Memory() + mem.freq = 146010000 + + def cb(*args): + gobject.idle_add(self.editors["memedit0"].prefill) + + job = common.RadioJob(cb, "set_memory", mem) + job.set_desc(_("Priming memory")) + self.rthread.submit(job) + + def tab_selected(self, notebook, foo, pagenum): + widget = notebook.get_nth_page(pagenum) + for k, v in list(self.editors.items()): + if v and v.root == widget: + v.focus() + self.emit("editor-selected", k) + elif v: + v.unfocus() + + def set_read_only(self, read_only=True): + for editor in list(self.editors.values()): + editor and editor.set_read_only(read_only) + + def get_read_only(self): + return self.editors["memedit0"].get_read_only() + + def prepare_close(self): + for editor in list(self.editors.values()): + editor and editor.prepare_close() + + def get_current_editor(self): + tabs = self.tabs + for lab, e in list(self.editors.items()): + if e and tabs.page_num(e.root) == tabs.get_current_page(): + return e + raise Exception("No editor selected?") + + @property + def rthread(self): + """Magic rthread property to return the rthread of the currently- + selected editor""" + e = self.get_current_editor() + return e.rthread diff --git a/chirp/ui/fips.py b/chirp/ui/fips.py new file mode 100644 index 0000000..a7089e7 --- /dev/null +++ b/chirp/ui/fips.py @@ -0,0 +1,6605 @@ +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +FIPS_STATES = { + "Alaska": 2, + "Alabama": 1, + "Arkansas": 5, + "Arizona": 4, + "California": 6, + "Colorado": 8, + "Connecticut": 9, + "District of Columbia": 11, + "Delaware": 10, + "Florida": 12, + "Georgia": 13, + "Guam": 66, + "Hawaii": 15, + "Iowa": 19, + "Idaho": 16, + "Illinois": 17, + "Indiana": 18, + "Kansas": 20, + "Kentucky": 21, + "Louisiana": 22, + "Massachusetts": 25, + "Maryland": 24, + "Maine": 23, + "Michigan": 26, + "Minnesota": 27, + "Missouri": 29, + "Mississippi": 28, + "Montana": 30, + "North Carolina": 37, + "North Dakota": 38, + "Nebraska": 31, + "New Hampshire": 33, + "New Jersey": 34, + "New Mexico": 35, + "Nevada": 32, + "New York": 36, + "Ohio": 39, + "Oklahoma": 40, + "Oregon": 41, + "Pennsylvania": 42, + "Puerto Rico": 72, + "Rhode Island": 44, + "South Carolina": 45, + "South Dakota": 46, + "Tennessee": 47, + "Texas": 48, + "Utah": 49, + "Virginia": 51, + "Virgin Islands": 78, + "Vermont": 50, + "Washington": 53, + "Wisconsin": 55, + "West Virginia": 54, + "Wyoming": 56, + "Alberta": "CA01", + "British Columbia": "CA02", + "Manitoba": "CA03", + "New Brunswick": "CA04", + "Newfoundland and Labrador": "CA05", + "Northwest Territories": "CA13", + "Nova Scotia": "CA07", + "Nunavut": "CA14", + "Ontario": "CA08", + "Prince Edward Island": "CA09", + "Quebec": "CA10", + "Saskatchewan": "CA11", + "Yukon": "CA12", +} + +FIPS_COUNTIES = { + 1: {'--All--': '%', + 'Autauga County, AL': '001', + 'Baldwin County, AL': '003', + 'Barbour County, AL': '005', + 'Bibb County, AL': '007', + 'Blount County, AL': '009', + 'Bullock County, AL': '011', + 'Butler County, AL': '013', + 'Calhoun County, AL': '015', + 'Chambers County, AL': '017', + 'Cherokee County, AL': '019', + 'Chilton County, AL': '021', + 'Choctaw County, AL': '023', + 'Clarke County, AL': '025', + 'Clay County, AL': '027', + 'Cleburne County, AL': '029', + 'Coffee County, AL': '031', + 'Colbert County, AL': '033', + 'Conecuh County, AL': '035', + 'Coosa County, AL': '037', + 'Covington County, AL': '039', + 'Crenshaw County, AL': '041', + 'Cullman County, AL': '043', + 'Dale County, AL': '045', + 'Dallas County, AL': '047', + 'DeKalb County, AL': '049', + 'Elmore County, AL': '051', + 'Escambia County, AL': '053', + 'Etowah County, AL': '055', + 'Fayette County, AL': '057', + 'Franklin County, AL': '059', + 'Geneva County, AL': '061', + 'Greene County, AL': '063', + 'Hale County, AL': '065', + 'Henry County, AL': '067', + 'Houston County, AL': '069', + 'Jackson County, AL': '071', + 'Jefferson County, AL': '073', + 'Lamar County, AL': '075', + 'Lauderdale County, AL': '077', + 'Lawrence County, AL': '079', + 'Lee County, AL': '081', + 'Limestone County, AL': '083', + 'Lowndes County, AL': '085', + 'Macon County, AL': '087', + 'Madison County, AL': '089', + 'Marengo County, AL': '091', + 'Marion County, AL': '093', + 'Marshall County, AL': '095', + 'Mobile County, AL': '097', + 'Monroe County, AL': '099', + 'Montgomery County, AL': '101', + 'Morgan County, AL': '103', + 'Perry County, AL': '105', + 'Pickens County, AL': '107', + 'Pike County, AL': '109', + 'Randolph County, AL': '111', + 'Russell County, AL': '113', + 'Shelby County, AL': '117', + 'St. Clair County, AL': '115', + 'Sumter County, AL': '119', + 'Talladega County, AL': '121', + 'Tallapoosa County, AL': '123', + 'Tuscaloosa County, AL': '125', + 'Walker County, AL': '127', + 'Washington County, AL': '129', + 'Wilcox County, AL': '131', + 'Winston County, AL': '133'}, + 2: {'--All--': '%', + 'Aleutians East Borough, AK': '013', + 'Aleutians West Census Area, AK': '016', + 'Anchorage Borough/municipality, AK': '020', + 'Bethel Census Area, AK': '050', + 'Bristol Bay Borough, AK': '060', + 'Denali Borough, AK': '068', + 'Dillingham Census Area, AK': '070', + 'Fairbanks North Star Borough, AK': '090', + 'Haines Borough, AK': '100', + 'Juneau Borough/city, AK': '110', + 'Kenai Peninsula Borough, AK': '122', + 'Ketchikan Gateway Borough, AK': '130', + 'Kodiak Island Borough, AK': '150', + 'Lake and Peninsula Borough, AK': '164', + 'Matanuska-Susitna Borough, AK': '170', + 'Nome Census Area, AK': '180', + 'North Slope Borough, AK': '185', + 'Northwest Arctic Borough, AK': '188', + 'Prince of Wales-Outer Ketchikan Census Area, AK': '201', + 'Sitka Borough/city, AK': '220', + 'Skagway-Hoonah-Angoon Census Area, AK': '232', + 'Southeast Fairbanks Census Area, AK': '240', + 'Valdez-Cordova Census Area, AK': '261', + 'Wade Hampton Census Area, AK': '270', + 'Wrangell-Petersburg Census Area, AK': '280', + 'Yakutat Borough, AK': '282', + 'Yukon-Koyukuk Census Area, AK': '290'}, + 4: {'--All--': '%', + 'Apache County, AZ': '001', + 'Cochise County, AZ': '003', + 'Coconino County, AZ': '005', + 'Gila County, AZ': '007', + 'Graham County, AZ': '009', + 'Greenlee County, AZ': '011', + 'La Paz County, AZ': '012', + 'Maricopa County, AZ': '013', + 'Mohave County, AZ': '015', + 'Navajo County, AZ': '017', + 'Pima County, AZ': '019', + 'Pinal County, AZ': '021', + 'Santa Cruz County, AZ': '023', + 'Yavapai County, AZ': '025', + 'Yuma County, AZ': '027'}, + 5: {'--All--': '%', + 'Arkansas County, AR': '001', + 'Ashley County, AR': '003', + 'Baxter County, AR': '005', + 'Benton County, AR': '007', + 'Boone County, AR': '009', + 'Bradley County, AR': '011', + 'Calhoun County, AR': '013', + 'Carroll County, AR': '015', + 'Chicot County, AR': '017', + 'Clark County, AR': '019', + 'Clay County, AR': '021', + 'Cleburne County, AR': '023', + 'Cleveland County, AR': '025', + 'Columbia County, AR': '027', + 'Conway County, AR': '029', + 'Craighead County, AR': '031', + 'Crawford County, AR': '033', + 'Crittenden County, AR': '035', + 'Cross County, AR': '037', + 'Dallas County, AR': '039', + 'Desha County, AR': '041', + 'Drew County, AR': '043', + 'Faulkner County, AR': '045', + 'Franklin County, AR': '047', + 'Fulton County, AR': '049', + 'Garland County, AR': '051', + 'Grant County, AR': '053', + 'Greene County, AR': '055', + 'Hempstead County, AR': '057', + 'Hot Spring County, AR': '059', + 'Howard County, AR': '061', + 'Independence County, AR': '063', + 'Izard County, AR': '065', + 'Jackson County, AR': '067', + 'Jefferson County, AR': '069', + 'Johnson County, AR': '071', + 'Lafayette County, AR': '073', + 'Lawrence County, AR': '075', + 'Lee County, AR': '077', + 'Lincoln County, AR': '079', + 'Little River County, AR': '081', + 'Logan County, AR': '083', + 'Lonoke County, AR': '085', + 'Madison County, AR': '087', + 'Marion County, AR': '089', + 'Miller County, AR': '091', + 'Mississippi County, AR': '093', + 'Monroe County, AR': '095', + 'Montgomery County, AR': '097', + 'Nevada County, AR': '099', + 'Newton County, AR': '101', + 'Ouachita County, AR': '103', + 'Perry County, AR': '105', + 'Phillips County, AR': '107', + 'Pike County, AR': '109', + 'Poinsett County, AR': '111', + 'Polk County, AR': '113', + 'Pope County, AR': '115', + 'Prairie County, AR': '117', + 'Pulaski County, AR': '119', + 'Randolph County, AR': '121', + 'Saline County, AR': '125', + 'Scott County, AR': '127', + 'Searcy County, AR': '129', + 'Sebastian County, AR': '131', + 'Sevier County, AR': '133', + 'Sharp County, AR': '135', + 'St. Francis County, AR': '123', + 'Stone County, AR': '137', + 'Union County, AR': '139', + 'Van Buren County, AR': '141', + 'Washington County, AR': '143', + 'White County, AR': '145', + 'Woodruff County, AR': '147', + 'Yell County, AR': '149'}, + 6: {'--All--': '%', + 'Alameda County, CA': '001', + 'Alpine County, CA': '003', + 'Amador County, CA': '005', + 'Butte County, CA': '007', + 'Calaveras County, CA': '009', + 'Colusa County, CA': '011', + 'Contra Costa County, CA': '013', + 'Del Norte County, CA': '015', + 'El Dorado County, CA': '017', + 'Fresno County, CA': '019', + 'Glenn County, CA': '021', + 'Humboldt County, CA': '023', + 'Imperial County, CA': '025', + 'Inyo County, CA': '027', + 'Kern County, CA': '029', + 'Kings County, CA': '031', + 'Lake County, CA': '033', + 'Lassen County, CA': '035', + 'Los Angeles County, CA': '037', + 'Madera County, CA': '039', + 'Marin County, CA': '041', + 'Mariposa County, CA': '043', + 'Mendocino County, CA': '045', + 'Merced County, CA': '047', + 'Modoc County, CA': '049', + 'Mono County, CA': '051', + 'Monterey County, CA': '053', + 'Napa County, CA': '055', + 'Nevada County, CA': '057', + 'Orange County, CA': '059', + 'Placer County, CA': '061', + 'Plumas County, CA': '063', + 'Riverside County, CA': '065', + 'Sacramento County, CA': '067', + 'San Benito County, CA': '069', + 'San Bernardino County, CA': '071', + 'San Diego County, CA': '073', + 'San Francisco County/city, CA': '075', + 'San Joaquin County, CA': '077', + 'San Luis Obispo County, CA': '079', + 'San Mateo County, CA': '081', + 'Santa Barbara County, CA': '083', + 'Santa Clara County, CA': '085', + 'Santa Cruz County, CA': '087', + 'Shasta County, CA': '089', + 'Sierra County, CA': '091', + 'Siskiyou County, CA': '093', + 'Solano County, CA': '095', + 'Sonoma County, CA': '097', + 'Stanislaus County, CA': '099', + 'Sutter County, CA': '101', + 'Tehama County, CA': '103', + 'Trinity County, CA': '105', + 'Tulare County, CA': '107', + 'Tuolumne County, CA': '109', + 'Ventura County, CA': '111', + 'Yolo County, CA': '113', + 'Yuba County, CA': '115'}, + 8: {'--All--': '%', + 'Adams County, CO': '001', + 'Alamosa County, CO': '003', + 'Arapahoe County, CO': '005', + 'Archuleta County, CO': '007', + 'Baca County, CO': '009', + 'Bent County, CO': '011', + 'Boulder County, CO': '013', + 'Broomfield County/city, CO': '014', + 'Chaffee County, CO': '015', + 'Cheyenne County, CO': '017', + 'Clear Creek County, CO': '019', + 'Conejos County, CO': '021', + 'Costilla County, CO': '023', + 'Crowley County, CO': '025', + 'Custer County, CO': '027', + 'Delta County, CO': '029', + 'Denver County/city, CO': '031', + 'Dolores County, CO': '033', + 'Douglas County, CO': '035', + 'Eagle County, CO': '037', + 'El Paso County, CO': '041', + 'Elbert County, CO': '039', + 'Fremont County, CO': '043', + 'Garfield County, CO': '045', + 'Gilpin County, CO': '047', + 'Grand County, CO': '049', + 'Gunnison County, CO': '051', + 'Hinsdale County, CO': '053', + 'Huerfano County, CO': '055', + 'Jackson County, CO': '057', + 'Jefferson County, CO': '059', + 'Kiowa County, CO': '061', + 'Kit Carson County, CO': '063', + 'La Plata County, CO': '067', + 'Lake County, CO': '065', + 'Larimer County, CO': '069', + 'Las Animas County, CO': '071', + 'Lincoln County, CO': '073', + 'Logan County, CO': '075', + 'Mesa County, CO': '077', + 'Mineral County, CO': '079', + 'Moffat County, CO': '081', + 'Montezuma County, CO': '083', + 'Montrose County, CO': '085', + 'Morgan County, CO': '087', + 'Otero County, CO': '089', + 'Ouray County, CO': '091', + 'Park County, CO': '093', + 'Phillips County, CO': '095', + 'Pitkin County, CO': '097', + 'Prowers County, CO': '099', + 'Pueblo County, CO': '101', + 'Rio Blanco County, CO': '103', + 'Rio Grande County, CO': '105', + 'Routt County, CO': '107', + 'Saguache County, CO': '109', + 'San Juan County, CO': '111', + 'San Miguel County, CO': '113', + 'Sedgwick County, CO': '115', + 'Summit County, CO': '117', + 'Teller County, CO': '119', + 'Washington County, CO': '121', + 'Weld County, CO': '123', + 'Yuma County, CO': '125'}, + 9: {'--All--': '%', + 'Fairfield County, CT': '001', + 'Hartford County, CT': '003', + 'Litchfield County, CT': '005', + 'Middlesex County, CT': '007', + 'New Haven County, CT': '009', + 'New London County, CT': '011', + 'Tolland County, CT': '013', + 'Windham County, CT': '015'}, + 10: {'--All--': '%', + 'Kent County, DE': '001', + 'New Castle County, DE': '003', + 'Sussex County, DE': '005'}, + 11: {'--All--': '%', 'District of Columbia': '001'}, + 12: {'--All--': '%', + 'Alachua County, FL': '001', + 'Baker County, FL': '003', + 'Bay County, FL': '005', + 'Bradford County, FL': '007', + 'Brevard County, FL': '009', + 'Broward County, FL': '011', + 'Calhoun County, FL': '013', + 'Charlotte County, FL': '015', + 'Citrus County, FL': '017', + 'Clay County, FL': '019', + 'Collier County, FL': '021', + 'Columbia County, FL': '023', + 'DeSoto County, FL': '027', + 'Dixie County, FL': '029', + 'Duval County, FL': '031', + 'Escambia County, FL': '033', + 'Flagler County, FL': '035', + 'Franklin County, FL': '037', + 'Gadsden County, FL': '039', + 'Gilchrist County, FL': '041', + 'Glades County, FL': '043', + 'Gulf County, FL': '045', + 'Hamilton County, FL': '047', + 'Hardee County, FL': '049', + 'Hendry County, FL': '051', + 'Hernando County, FL': '053', + 'Highlands County, FL': '055', + 'Hillsborough County, FL': '057', + 'Holmes County, FL': '059', + 'Indian River County, FL': '061', + 'Jackson County, FL': '063', + 'Jefferson County, FL': '065', + 'Lafayette County, FL': '067', + 'Lake County, FL': '069', + 'Lee County, FL': '071', + 'Leon County, FL': '073', + 'Levy County, FL': '075', + 'Liberty County, FL': '077', + 'Madison County, FL': '079', + 'Manatee County, FL': '081', + 'Marion County, FL': '083', + 'Martin County, FL': '085', + 'Miami-Dade County, FL': '086', + 'Monroe County, FL': '087', + 'Nassau County, FL': '089', + 'Okaloosa County, FL': '091', + 'Okeechobee County, FL': '093', + 'Orange County, FL': '095', + 'Osceola County, FL': '097', + 'Palm Beach County, FL': '099', + 'Pasco County, FL': '101', + 'Pinellas County, FL': '103', + 'Polk County, FL': '105', + 'Putnam County, FL': '107', + 'Santa Rosa County, FL': '113', + 'Sarasota County, FL': '115', + 'Seminole County, FL': '117', + 'St. Johns County, FL': '109', + 'St. Lucie County, FL': '111', + 'Sumter County, FL': '119', + 'Suwannee County, FL': '121', + 'Taylor County, FL': '123', + 'Union County, FL': '125', + 'Volusia County, FL': '127', + 'Wakulla County, FL': '129', + 'Walton County, FL': '131', + 'Washington County, FL': '133'}, + 13: {'--All--': '%', + 'Appling County, GA': '001', + 'Atkinson County, GA': '003', + 'Bacon County, GA': '005', + 'Baker County, GA': '007', + 'Baldwin County, GA': '009', + 'Banks County, GA': '011', + 'Barrow County, GA': '013', + 'Bartow County, GA': '015', + 'Ben Hill County, GA': '017', + 'Berrien County, GA': '019', + 'Bibb County, GA': '021', + 'Bleckley County, GA': '023', + 'Brantley County, GA': '025', + 'Brooks County, GA': '027', + 'Bryan County, GA': '029', + 'Bulloch County, GA': '031', + 'Burke County, GA': '033', + 'Butts County, GA': '035', + 'Calhoun County, GA': '037', + 'Camden County, GA': '039', + 'Candler County, GA': '043', + 'Carroll County, GA': '045', + 'Catoosa County, GA': '047', + 'Charlton County, GA': '049', + 'Chatham County, GA': '051', + 'Chattahoochee County, GA': '053', + 'Chattooga County, GA': '055', + 'Cherokee County, GA': '057', + 'Clarke County, GA': '059', + 'Clay County, GA': '061', + 'Clayton County, GA': '063', + 'Clinch County, GA': '065', + 'Cobb County, GA': '067', + 'Coffee County, GA': '069', + 'Colquitt County, GA': '071', + 'Columbia County, GA': '073', + 'Cook County, GA': '075', + 'Coweta County, GA': '077', + 'Crawford County, GA': '079', + 'Crisp County, GA': '081', + 'Dade County, GA': '083', + 'Dawson County, GA': '085', + 'DeKalb County, GA': '089', + 'Decatur County, GA': '087', + 'Dodge County, GA': '091', + 'Dooly County, GA': '093', + 'Dougherty County, GA': '095', + 'Douglas County, GA': '097', + 'Early County, GA': '099', + 'Echols County, GA': '101', + 'Effingham County, GA': '103', + 'Elbert County, GA': '105', + 'Emanuel County, GA': '107', + 'Evans County, GA': '109', + 'Fannin County, GA': '111', + 'Fayette County, GA': '113', + 'Floyd County, GA': '115', + 'Forsyth County, GA': '117', + 'Franklin County, GA': '119', + 'Fulton County, GA': '121', + 'Gilmer County, GA': '123', + 'Glascock County, GA': '125', + 'Glynn County, GA': '127', + 'Gordon County, GA': '129', + 'Grady County, GA': '131', + 'Greene County, GA': '133', + 'Gwinnett County, GA': '135', + 'Habersham County, GA': '137', + 'Hall County, GA': '139', + 'Hancock County, GA': '141', + 'Haralson County, GA': '143', + 'Harris County, GA': '145', + 'Hart County, GA': '147', + 'Heard County, GA': '149', + 'Henry County, GA': '151', + 'Houston County, GA': '153', + 'Irwin County, GA': '155', + 'Jackson County, GA': '157', + 'Jasper County, GA': '159', + 'Jeff Davis County, GA': '161', + 'Jefferson County, GA': '163', + 'Jenkins County, GA': '165', + 'Johnson County, GA': '167', + 'Jones County, GA': '169', + 'Lamar County, GA': '171', + 'Lanier County, GA': '173', + 'Laurens County, GA': '175', + 'Lee County, GA': '177', + 'Liberty County, GA': '179', + 'Lincoln County, GA': '181', + 'Long County, GA': '183', + 'Lowndes County, GA': '185', + 'Lumpkin County, GA': '187', + 'Macon County, GA': '193', + 'Madison County, GA': '195', + 'Marion County, GA': '197', + 'McDuffie County, GA': '189', + 'McIntosh County, GA': '191', + 'Meriwether County, GA': '199', + 'Miller County, GA': '201', + 'Mitchell County, GA': '205', + 'Monroe County, GA': '207', + 'Montgomery County, GA': '209', + 'Morgan County, GA': '211', + 'Murray County, GA': '213', + 'Muscogee County, GA': '215', + 'Newton County, GA': '217', + 'Oconee County, GA': '219', + 'Oglethorpe County, GA': '221', + 'Paulding County, GA': '223', + 'Peach County, GA': '225', + 'Pickens County, GA': '227', + 'Pierce County, GA': '229', + 'Pike County, GA': '231', + 'Polk County, GA': '233', + 'Pulaski County, GA': '235', + 'Putnam County, GA': '237', + 'Quitman County, GA': '239', + 'Rabun County, GA': '241', + 'Randolph County, GA': '243', + 'Richmond County, GA': '245', + 'Rockdale County, GA': '247', + 'Schley County, GA': '249', + 'Screven County, GA': '251', + 'Seminole County, GA': '253', + 'Spalding County, GA': '255', + 'Stephens County, GA': '257', + 'Stewart County, GA': '259', + 'Sumter County, GA': '261', + 'Talbot County, GA': '263', + 'Taliaferro County, GA': '265', + 'Tattnall County, GA': '267', + 'Taylor County, GA': '269', + 'Telfair County, GA': '271', + 'Terrell County, GA': '273', + 'Thomas County, GA': '275', + 'Tift County, GA': '277', + 'Toombs County, GA': '279', + 'Towns County, GA': '281', + 'Treutlen County, GA': '283', + 'Troup County, GA': '285', + 'Turner County, GA': '287', + 'Twiggs County, GA': '289', + 'Union County, GA': '291', + 'Upson County, GA': '293', + 'Walker County, GA': '295', + 'Walton County, GA': '297', + 'Ware County, GA': '299', + 'Warren County, GA': '301', + 'Washington County, GA': '303', + 'Wayne County, GA': '305', + 'Webster County, GA': '307', + 'Wheeler County, GA': '309', + 'White County, GA': '311', + 'Whitfield County, GA': '313', + 'Wilcox County, GA': '315', + 'Wilkes County, GA': '317', + 'Wilkinson County, GA': '319', + 'Worth County, GA': '321'}, + 15: {'--All--': '%', + 'Hawaii County, HI': '001', + 'Honolulu County/city, HI': '003', + 'Kauai County, HI': '007', + 'Maui County, HI': '009'}, + 16: {'--All--': '%', + 'Ada County, ID': '001', + 'Adams County, ID': '003', + 'Bannock County, ID': '005', + 'Bear Lake County, ID': '007', + 'Benewah County, ID': '009', + 'Bingham County, ID': '011', + 'Blaine County, ID': '013', + 'Boise County, ID': '015', + 'Bonner County, ID': '017', + 'Bonneville County, ID': '019', + 'Boundary County, ID': '021', + 'Butte County, ID': '023', + 'Camas County, ID': '025', + 'Canyon County, ID': '027', + 'Caribou County, ID': '029', + 'Cassia County, ID': '031', + 'Clark County, ID': '033', + 'Clearwater County, ID': '035', + 'Custer County, ID': '037', + 'Elmore County, ID': '039', + 'Franklin County, ID': '041', + 'Fremont County, ID': '043', + 'Gem County, ID': '045', + 'Gooding County, ID': '047', + 'Idaho County, ID': '049', + 'Jefferson County, ID': '051', + 'Jerome County, ID': '053', + 'Kootenai County, ID': '055', + 'Latah County, ID': '057', + 'Lemhi County, ID': '059', + 'Lewis County, ID': '061', + 'Lincoln County, ID': '063', + 'Madison County, ID': '065', + 'Minidoka County, ID': '067', + 'Nez Perce County, ID': '069', + 'Oneida County, ID': '071', + 'Owyhee County, ID': '073', + 'Payette County, ID': '075', + 'Power County, ID': '077', + 'Shoshone County, ID': '079', + 'Teton County, ID': '081', + 'Twin Falls County, ID': '083', + 'Valley County, ID': '085', + 'Washington County, ID': '087'}, + 17: {'--All--': '%', + 'Adams County, IL': '001', + 'Alexander County, IL': '003', + 'Bond County, IL': '005', + 'Boone County, IL': '007', + 'Brown County, IL': '009', + 'Bureau County, IL': '011', + 'Calhoun County, IL': '013', + 'Carroll County, IL': '015', + 'Cass County, IL': '017', + 'Champaign County, IL': '019', + 'Christian County, IL': '021', + 'Clark County, IL': '023', + 'Clay County, IL': '025', + 'Clinton County, IL': '027', + 'Coles County, IL': '029', + 'Cook County, IL': '031', + 'Crawford County, IL': '033', + 'Cumberland County, IL': '035', + 'De Witt County, IL': '039', + 'DeKalb County, IL': '037', + 'Douglas County, IL': '041', + 'DuPage County, IL': '043', + 'Edgar County, IL': '045', + 'Edwards County, IL': '047', + 'Effingham County, IL': '049', + 'Fayette County, IL': '051', + 'Ford County, IL': '053', + 'Franklin County, IL': '055', + 'Fulton County, IL': '057', + 'Gallatin County, IL': '059', + 'Greene County, IL': '061', + 'Grundy County, IL': '063', + 'Hamilton County, IL': '065', + 'Hancock County, IL': '067', + 'Hardin County, IL': '069', + 'Henderson County, IL': '071', + 'Henry County, IL': '073', + 'Iroquois County, IL': '075', + 'Jackson County, IL': '077', + 'Jasper County, IL': '079', + 'Jefferson County, IL': '081', + 'Jersey County, IL': '083', + 'Jo Daviess County, IL': '085', + 'Johnson County, IL': '087', + 'Kane County, IL': '089', + 'Kankakee County, IL': '091', + 'Kendall County, IL': '093', + 'Knox County, IL': '095', + 'La Salle County, IL': '099', + 'Lake County, IL': '097', + 'Lawrence County, IL': '101', + 'Lee County, IL': '103', + 'Livingston County, IL': '105', + 'Logan County, IL': '107', + 'Macon County, IL': '115', + 'Macoupin County, IL': '117', + 'Madison County, IL': '119', + 'Marion County, IL': '121', + 'Marshall County, IL': '123', + 'Mason County, IL': '125', + 'Massac County, IL': '127', + 'McDonough County, IL': '109', + 'McHenry County, IL': '111', + 'McLean County, IL': '113', + 'Menard County, IL': '129', + 'Mercer County, IL': '131', + 'Monroe County, IL': '133', + 'Montgomery County, IL': '135', + 'Morgan County, IL': '137', + 'Moultrie County, IL': '139', + 'Ogle County, IL': '141', + 'Peoria County, IL': '143', + 'Perry County, IL': '145', + 'Piatt County, IL': '147', + 'Pike County, IL': '149', + 'Pope County, IL': '151', + 'Pulaski County, IL': '153', + 'Putnam County, IL': '155', + 'Randolph County, IL': '157', + 'Richland County, IL': '159', + 'Rock Island County, IL': '161', + 'Saline County, IL': '165', + 'Sangamon County, IL': '167', + 'Schuyler County, IL': '169', + 'Scott County, IL': '171', + 'Shelby County, IL': '173', + 'St. Clair County, IL': '163', + 'Stark County, IL': '175', + 'Stephenson County, IL': '177', + 'Tazewell County, IL': '179', + 'Union County, IL': '181', + 'Vermilion County, IL': '183', + 'Wabash County, IL': '185', + 'Warren County, IL': '187', + 'Washington County, IL': '189', + 'Wayne County, IL': '191', + 'White County, IL': '193', + 'Whiteside County, IL': '195', + 'Will County, IL': '197', + 'Williamson County, IL': '199', + 'Winnebago County, IL': '201', + 'Woodford County, IL': '203'}, + 18: {'--All--': '%', + 'Adams County, IN': '001', + 'Allen County, IN': '003', + 'Bartholomew County, IN': '005', + 'Benton County, IN': '007', + 'Blackford County, IN': '009', + 'Boone County, IN': '011', + 'Brown County, IN': '013', + 'Carroll County, IN': '015', + 'Cass County, IN': '017', + 'Clark County, IN': '019', + 'Clay County, IN': '021', + 'Clinton County, IN': '023', + 'Crawford County, IN': '025', + 'Daviess County, IN': '027', + 'DeKalb County, IN': '033', + 'Dearborn County, IN': '029', + 'Decatur County, IN': '031', + 'Delaware County, IN': '035', + 'Dubois County, IN': '037', + 'Elkhart County, IN': '039', + 'Fayette County, IN': '041', + 'Floyd County, IN': '043', + 'Fountain County, IN': '045', + 'Franklin County, IN': '047', + 'Fulton County, IN': '049', + 'Gibson County, IN': '051', + 'Grant County, IN': '053', + 'Greene County, IN': '055', + 'Hamilton County, IN': '057', + 'Hancock County, IN': '059', + 'Harrison County, IN': '061', + 'Hendricks County, IN': '063', + 'Henry County, IN': '065', + 'Howard County, IN': '067', + 'Huntington County, IN': '069', + 'Jackson County, IN': '071', + 'Jasper County, IN': '073', + 'Jay County, IN': '075', + 'Jefferson County, IN': '077', + 'Jennings County, IN': '079', + 'Johnson County, IN': '081', + 'Knox County, IN': '083', + 'Kosciusko County, IN': '085', + 'LaGrange County, IN': '087', + 'LaPorte County, IN': '091', + 'Lake County, IN': '089', + 'Lawrence County, IN': '093', + 'Madison County, IN': '095', + 'Marion County, IN': '097', + 'Marshall County, IN': '099', + 'Martin County, IN': '101', + 'Miami County, IN': '103', + 'Monroe County, IN': '105', + 'Montgomery County, IN': '107', + 'Morgan County, IN': '109', + 'Newton County, IN': '111', + 'Noble County, IN': '113', + 'Ohio County, IN': '115', + 'Orange County, IN': '117', + 'Owen County, IN': '119', + 'Parke County, IN': '121', + 'Perry County, IN': '123', + 'Pike County, IN': '125', + 'Porter County, IN': '127', + 'Posey County, IN': '129', + 'Pulaski County, IN': '131', + 'Putnam County, IN': '133', + 'Randolph County, IN': '135', + 'Ripley County, IN': '137', + 'Rush County, IN': '139', + 'Scott County, IN': '143', + 'Shelby County, IN': '145', + 'Spencer County, IN': '147', + 'St. Joseph County, IN': '141', + 'Starke County, IN': '149', + 'Steuben County, IN': '151', + 'Sullivan County, IN': '153', + 'Switzerland County, IN': '155', + 'Tippecanoe County, IN': '157', + 'Tipton County, IN': '159', + 'Union County, IN': '161', + 'Vanderburgh County, IN': '163', + 'Vermillion County, IN': '165', + 'Vigo County, IN': '167', + 'Wabash County, IN': '169', + 'Warren County, IN': '171', + 'Warrick County, IN': '173', + 'Washington County, IN': '175', + 'Wayne County, IN': '177', + 'Wells County, IN': '179', + 'White County, IN': '181', + 'Whitley County, IN': '183'}, + 19: {'--All--': '%', + 'Adair County, IA': '001', + 'Adams County, IA': '003', + 'Allamakee County, IA': '005', + 'Appanoose County, IA': '007', + 'Audubon County, IA': '009', + 'Benton County, IA': '011', + 'Black Hawk County, IA': '013', + 'Boone County, IA': '015', + 'Bremer County, IA': '017', + 'Buchanan County, IA': '019', + 'Buena Vista County, IA': '021', + 'Butler County, IA': '023', + 'Calhoun County, IA': '025', + 'Carroll County, IA': '027', + 'Cass County, IA': '029', + 'Cedar County, IA': '031', + 'Cerro Gordo County, IA': '033', + 'Cherokee County, IA': '035', + 'Chickasaw County, IA': '037', + 'Clarke County, IA': '039', + 'Clay County, IA': '041', + 'Clayton County, IA': '043', + 'Clinton County, IA': '045', + 'Crawford County, IA': '047', + 'Dallas County, IA': '049', + 'Davis County, IA': '051', + 'Decatur County, IA': '053', + 'Delaware County, IA': '055', + 'Des Moines County, IA': '057', + 'Dickinson County, IA': '059', + 'Dubuque County, IA': '061', + 'Emmet County, IA': '063', + 'Fayette County, IA': '065', + 'Floyd County, IA': '067', + 'Franklin County, IA': '069', + 'Fremont County, IA': '071', + 'Greene County, IA': '073', + 'Grundy County, IA': '075', + 'Guthrie County, IA': '077', + 'Hamilton County, IA': '079', + 'Hancock County, IA': '081', + 'Hardin County, IA': '083', + 'Harrison County, IA': '085', + 'Henry County, IA': '087', + 'Howard County, IA': '089', + 'Humboldt County, IA': '091', + 'Ida County, IA': '093', + 'Iowa County, IA': '095', + 'Jackson County, IA': '097', + 'Jasper County, IA': '099', + 'Jefferson County, IA': '101', + 'Johnson County, IA': '103', + 'Jones County, IA': '105', + 'Keokuk County, IA': '107', + 'Kossuth County, IA': '109', + 'Lee County, IA': '111', + 'Linn County, IA': '113', + 'Louisa County, IA': '115', + 'Lucas County, IA': '117', + 'Lyon County, IA': '119', + 'Madison County, IA': '121', + 'Mahaska County, IA': '123', + 'Marion County, IA': '125', + 'Marshall County, IA': '127', + 'Mills County, IA': '129', + 'Mitchell County, IA': '131', + 'Monona County, IA': '133', + 'Monroe County, IA': '135', + 'Montgomery County, IA': '137', + 'Muscatine County, IA': '139', + "O'Brien County, IA": '141', + 'Osceola County, IA': '143', + 'Page County, IA': '145', + 'Palo Alto County, IA': '147', + 'Plymouth County, IA': '149', + 'Pocahontas County, IA': '151', + 'Polk County, IA': '153', + 'Pottawattamie County, IA': '155', + 'Poweshiek County, IA': '157', + 'Ringgold County, IA': '159', + 'Sac County, IA': '161', + 'Scott County, IA': '163', + 'Shelby County, IA': '165', + 'Sioux County, IA': '167', + 'Story County, IA': '169', + 'Tama County, IA': '171', + 'Taylor County, IA': '173', + 'Union County, IA': '175', + 'Van Buren County, IA': '177', + 'Wapello County, IA': '179', + 'Warren County, IA': '181', + 'Washington County, IA': '183', + 'Wayne County, IA': '185', + 'Webster County, IA': '187', + 'Winnebago County, IA': '189', + 'Winneshiek County, IA': '191', + 'Woodbury County, IA': '193', + 'Worth County, IA': '195', + 'Wright County, IA': '197'}, + 20: {'--All--': '%', + 'Allen County, KS': '001', + 'Anderson County, KS': '003', + 'Atchison County, KS': '005', + 'Barber County, KS': '007', + 'Barton County, KS': '009', + 'Bourbon County, KS': '011', + 'Brown County, KS': '013', + 'Butler County, KS': '015', + 'Chase County, KS': '017', + 'Chautauqua County, KS': '019', + 'Cherokee County, KS': '021', + 'Cheyenne County, KS': '023', + 'Clark County, KS': '025', + 'Clay County, KS': '027', + 'Cloud County, KS': '029', + 'Coffey County, KS': '031', + 'Comanche County, KS': '033', + 'Cowley County, KS': '035', + 'Crawford County, KS': '037', + 'Decatur County, KS': '039', + 'Dickinson County, KS': '041', + 'Doniphan County, KS': '043', + 'Douglas County, KS': '045', + 'Edwards County, KS': '047', + 'Elk County, KS': '049', + 'Ellis County, KS': '051', + 'Ellsworth County, KS': '053', + 'Finney County, KS': '055', + 'Ford County, KS': '057', + 'Franklin County, KS': '059', + 'Geary County, KS': '061', + 'Gove County, KS': '063', + 'Graham County, KS': '065', + 'Grant County, KS': '067', + 'Gray County, KS': '069', + 'Greeley County, KS': '071', + 'Greenwood County, KS': '073', + 'Hamilton County, KS': '075', + 'Harper County, KS': '077', + 'Harvey County, KS': '079', + 'Haskell County, KS': '081', + 'Hodgeman County, KS': '083', + 'Jackson County, KS': '085', + 'Jefferson County, KS': '087', + 'Jewell County, KS': '089', + 'Johnson County, KS': '091', + 'Kearny County, KS': '093', + 'Kingman County, KS': '095', + 'Kiowa County, KS': '097', + 'Labette County, KS': '099', + 'Lane County, KS': '101', + 'Leavenworth County, KS': '103', + 'Lincoln County, KS': '105', + 'Linn County, KS': '107', + 'Logan County, KS': '109', + 'Lyon County, KS': '111', + 'Marion County, KS': '115', + 'Marshall County, KS': '117', + 'McPherson County, KS': '113', + 'Meade County, KS': '119', + 'Miami County, KS': '121', + 'Mitchell County, KS': '123', + 'Montgomery County, KS': '125', + 'Morris County, KS': '127', + 'Morton County, KS': '129', + 'Nemaha County, KS': '131', + 'Neosho County, KS': '133', + 'Ness County, KS': '135', + 'Norton County, KS': '137', + 'Osage County, KS': '139', + 'Osborne County, KS': '141', + 'Ottawa County, KS': '143', + 'Pawnee County, KS': '145', + 'Phillips County, KS': '147', + 'Pottawatomie County, KS': '149', + 'Pratt County, KS': '151', + 'Rawlins County, KS': '153', + 'Reno County, KS': '155', + 'Republic County, KS': '157', + 'Rice County, KS': '159', + 'Riley County, KS': '161', + 'Rooks County, KS': '163', + 'Rush County, KS': '165', + 'Russell County, KS': '167', + 'Saline County, KS': '169', + 'Scott County, KS': '171', + 'Sedgwick County, KS': '173', + 'Seward County, KS': '175', + 'Shawnee County, KS': '177', + 'Sheridan County, KS': '179', + 'Sherman County, KS': '181', + 'Smith County, KS': '183', + 'Stafford County, KS': '185', + 'Stanton County, KS': '187', + 'Stevens County, KS': '189', + 'Sumner County, KS': '191', + 'Thomas County, KS': '193', + 'Trego County, KS': '195', + 'Wabaunsee County, KS': '197', + 'Wallace County, KS': '199', + 'Washington County, KS': '201', + 'Wichita County, KS': '203', + 'Wilson County, KS': '205', + 'Woodson County, KS': '207', + 'Wyandotte County, KS': '209'}, + 21: {'--All--': '%', + 'Adair County, KY': '001', + 'Allen County, KY': '003', + 'Anderson County, KY': '005', + 'Ballard County, KY': '007', + 'Barren County, KY': '009', + 'Bath County, KY': '011', + 'Bell County, KY': '013', + 'Boone County, KY': '015', + 'Bourbon County, KY': '017', + 'Boyd County, KY': '019', + 'Boyle County, KY': '021', + 'Bracken County, KY': '023', + 'Breathitt County, KY': '025', + 'Breckinridge County, KY': '027', + 'Bullitt County, KY': '029', + 'Butler County, KY': '031', + 'Caldwell County, KY': '033', + 'Calloway County, KY': '035', + 'Campbell County, KY': '037', + 'Carlisle County, KY': '039', + 'Carroll County, KY': '041', + 'Carter County, KY': '043', + 'Casey County, KY': '045', + 'Christian County, KY': '047', + 'Clark County, KY': '049', + 'Clay County, KY': '051', + 'Clinton County, KY': '053', + 'Crittenden County, KY': '055', + 'Cumberland County, KY': '057', + 'Daviess County, KY': '059', + 'Edmonson County, KY': '061', + 'Elliott County, KY': '063', + 'Estill County, KY': '065', + 'Fayette County, KY': '067', + 'Fleming County, KY': '069', + 'Floyd County, KY': '071', + 'Franklin County, KY': '073', + 'Fulton County, KY': '075', + 'Gallatin County, KY': '077', + 'Garrard County, KY': '079', + 'Grant County, KY': '081', + 'Graves County, KY': '083', + 'Grayson County, KY': '085', + 'Green County, KY': '087', + 'Greenup County, KY': '089', + 'Hancock County, KY': '091', + 'Hardin County, KY': '093', + 'Harlan County, KY': '095', + 'Harrison County, KY': '097', + 'Hart County, KY': '099', + 'Henderson County, KY': '101', + 'Henry County, KY': '103', + 'Hickman County, KY': '105', + 'Hopkins County, KY': '107', + 'Jackson County, KY': '109', + 'Jefferson County, KY': '111', + 'Jessamine County, KY': '113', + 'Johnson County, KY': '115', + 'Kenton County, KY': '117', + 'Knott County, KY': '119', + 'Knox County, KY': '121', + 'Larue County, KY': '123', + 'Laurel County, KY': '125', + 'Lawrence County, KY': '127', + 'Lee County, KY': '129', + 'Leslie County, KY': '131', + 'Letcher County, KY': '133', + 'Lewis County, KY': '135', + 'Lincoln County, KY': '137', + 'Livingston County, KY': '139', + 'Logan County, KY': '141', + 'Lyon County, KY': '143', + 'Madison County, KY': '151', + 'Magoffin County, KY': '153', + 'Marion County, KY': '155', + 'Marshall County, KY': '157', + 'Martin County, KY': '159', + 'Mason County, KY': '161', + 'McCracken County, KY': '145', + 'McCreary County, KY': '147', + 'McLean County, KY': '149', + 'Meade County, KY': '163', + 'Menifee County, KY': '165', + 'Mercer County, KY': '167', + 'Metcalfe County, KY': '169', + 'Monroe County, KY': '171', + 'Montgomery County, KY': '173', + 'Morgan County, KY': '175', + 'Muhlenberg County, KY': '177', + 'Nelson County, KY': '179', + 'Nicholas County, KY': '181', + 'Ohio County, KY': '183', + 'Oldham County, KY': '185', + 'Owen County, KY': '187', + 'Owsley County, KY': '189', + 'Pendleton County, KY': '191', + 'Perry County, KY': '193', + 'Pike County, KY': '195', + 'Powell County, KY': '197', + 'Pulaski County, KY': '199', + 'Robertson County, KY': '201', + 'Rockcastle County, KY': '203', + 'Rowan County, KY': '205', + 'Russell County, KY': '207', + 'Scott County, KY': '209', + 'Shelby County, KY': '211', + 'Simpson County, KY': '213', + 'Spencer County, KY': '215', + 'Taylor County, KY': '217', + 'Todd County, KY': '219', + 'Trigg County, KY': '221', + 'Trimble County, KY': '223', + 'Union County, KY': '225', + 'Warren County, KY': '227', + 'Washington County, KY': '229', + 'Wayne County, KY': '231', + 'Webster County, KY': '233', + 'Whitley County, KY': '235', + 'Wolfe County, KY': '237', + 'Woodford County, KY': '239'}, + 22: {'--All--': '%', + 'Acadia Parish, LA': '001', + 'Allen Parish, LA': '003', + 'Ascension Parish, LA': '005', + 'Assumption Parish, LA': '007', + 'Avoyelles Parish, LA': '009', + 'Beauregard Parish, LA': '011', + 'Bienville Parish, LA': '013', + 'Bossier Parish, LA': '015', + 'Caddo Parish, LA': '017', + 'Calcasieu Parish, LA': '019', + 'Caldwell Parish, LA': '021', + 'Cameron Parish, LA': '023', + 'Catahoula Parish, LA': '025', + 'Claiborne Parish, LA': '027', + 'Concordia Parish, LA': '029', + 'De Soto Parish, LA': '031', + 'East Baton Rouge Parish, LA': '033', + 'East Carroll Parish, LA': '035', + 'East Feliciana Parish, LA': '037', + 'Evangeline Parish, LA': '039', + 'Franklin Parish, LA': '041', + 'Grant Parish, LA': '043', + 'Iberia Parish, LA': '045', + 'Iberville Parish, LA': '047', + 'Jackson Parish, LA': '049', + 'Jefferson Davis Parish, LA': '053', + 'Jefferson Parish, LA': '051', + 'La Salle Parish, LA': '059', + 'Lafayette Parish, LA': '055', + 'Lafourche Parish, LA': '057', + 'Lincoln Parish, LA': '061', + 'Livingston Parish, LA': '063', + 'Madison Parish, LA': '065', + 'Morehouse Parish, LA': '067', + 'Natchitoches Parish, LA': '069', + 'Orleans Parish, LA': '071', + 'Ouachita Parish, LA': '073', + 'Plaquemines Parish, LA': '075', + 'Pointe Coupee Parish, LA': '077', + 'Rapides Parish, LA': '079', + 'Red River Parish, LA': '081', + 'Richland Parish, LA': '083', + 'Sabine Parish, LA': '085', + 'St. Bernard Parish, LA': '087', + 'St. Charles Parish, LA': '089', + 'St. Helena Parish, LA': '091', + 'St. James Parish, LA': '093', + 'St. John the Baptist Parish, LA': '095', + 'St. Landry Parish, LA': '097', + 'St. Martin Parish, LA': '099', + 'St. Mary Parish, LA': '101', + 'St. Tammany Parish, LA': '103', + 'Tangipahoa Parish, LA': '105', + 'Tensas Parish, LA': '107', + 'Terrebonne Parish, LA': '109', + 'Union Parish, LA': '111', + 'Vermilion Parish, LA': '113', + 'Vernon Parish, LA': '115', + 'Washington Parish, LA': '117', + 'Webster Parish, LA': '119', + 'West Baton Rouge Parish, LA': '121', + 'West Carroll Parish, LA': '123', + 'West Feliciana Parish, LA': '125', + 'Winn Parish, LA': '127'}, + 23: {'--All--': '%', + 'Androscoggin County, ME': '001', + 'Aroostook County, ME': '003', + 'Cumberland County, ME': '005', + 'Franklin County, ME': '007', + 'Hancock County, ME': '009', + 'Kennebec County, ME': '011', + 'Knox County, ME': '013', + 'Lincoln County, ME': '015', + 'Oxford County, ME': '017', + 'Penobscot County, ME': '019', + 'Piscataquis County, ME': '021', + 'Sagadahoc County, ME': '023', + 'Somerset County, ME': '025', + 'Waldo County, ME': '027', + 'Washington County, ME': '029', + 'York County, ME': '031'}, + 24: {'--All--': '%', + 'Allegany County, MD': '001', + 'Anne Arundel County, MD': '003', + 'Baltimore County, MD': '005', + 'Baltimore city, MD': '510', + 'Calvert County, MD': '009', + 'Caroline County, MD': '011', + 'Carroll County, MD': '013', + 'Cecil County, MD': '015', + 'Charles County, MD': '017', + 'Dorchester County, MD': '019', + 'Frederick County, MD': '021', + 'Garrett County, MD': '023', + 'Harford County, MD': '025', + 'Howard County, MD': '027', + 'Kent County, MD': '029', + 'Montgomery County, MD': '031', + "Prince George's County, MD": '033', + "Queen Anne's County, MD": '035', + 'Somerset County, MD': '039', + "St. Mary's County, MD": '037', + 'Talbot County, MD': '041', + 'Washington County, MD': '043', + 'Wicomico County, MD': '045', + 'Worcester County, MD': '047'}, + 25: {'--All--': '%', + 'Barnstable County, MA': '001', + 'Berkshire County, MA': '003', + 'Bristol County, MA': '005', + 'Dukes County, MA': '007', + 'Essex County, MA': '009', + 'Franklin County, MA': '011', + 'Hampden County, MA': '013', + 'Hampshire County, MA': '015', + 'Middlesex County, MA': '017', + 'Nantucket County/town, MA': '019', + 'Norfolk County, MA': '021', + 'Plymouth County, MA': '023', + 'Suffolk County, MA': '025', + 'Worcester County, MA': '027'}, + 26: {'--All--': '%', + 'Alcona County, MI': '001', + 'Alger County, MI': '003', + 'Allegan County, MI': '005', + 'Alpena County, MI': '007', + 'Antrim County, MI': '009', + 'Arenac County, MI': '011', + 'Baraga County, MI': '013', + 'Barry County, MI': '015', + 'Bay County, MI': '017', + 'Benzie County, MI': '019', + 'Berrien County, MI': '021', + 'Branch County, MI': '023', + 'Calhoun County, MI': '025', + 'Cass County, MI': '027', + 'Charlevoix County, MI': '029', + 'Cheboygan County, MI': '031', + 'Chippewa County, MI': '033', + 'Clare County, MI': '035', + 'Clinton County, MI': '037', + 'Crawford County, MI': '039', + 'Delta County, MI': '041', + 'Dickinson County, MI': '043', + 'Eaton County, MI': '045', + 'Emmet County, MI': '047', + 'Genesee County, MI': '049', + 'Gladwin County, MI': '051', + 'Gogebic County, MI': '053', + 'Grand Traverse County, MI': '055', + 'Gratiot County, MI': '057', + 'Hillsdale County, MI': '059', + 'Houghton County, MI': '061', + 'Huron County, MI': '063', + 'Ingham County, MI': '065', + 'Ionia County, MI': '067', + 'Iosco County, MI': '069', + 'Iron County, MI': '071', + 'Isabella County, MI': '073', + 'Jackson County, MI': '075', + 'Kalamazoo County, MI': '077', + 'Kalkaska County, MI': '079', + 'Kent County, MI': '081', + 'Keweenaw County, MI': '083', + 'Lake County, MI': '085', + 'Lapeer County, MI': '087', + 'Leelanau County, MI': '089', + 'Lenawee County, MI': '091', + 'Livingston County, MI': '093', + 'Luce County, MI': '095', + 'Mackinac County, MI': '097', + 'Macomb County, MI': '099', + 'Manistee County, MI': '101', + 'Marquette County, MI': '103', + 'Mason County, MI': '105', + 'Mecosta County, MI': '107', + 'Menominee County, MI': '109', + 'Midland County, MI': '111', + 'Missaukee County, MI': '113', + 'Monroe County, MI': '115', + 'Montcalm County, MI': '117', + 'Montmorency County, MI': '119', + 'Muskegon County, MI': '121', + 'Newaygo County, MI': '123', + 'Oakland County, MI': '125', + 'Oceana County, MI': '127', + 'Ogemaw County, MI': '129', + 'Ontonagon County, MI': '131', + 'Osceola County, MI': '133', + 'Oscoda County, MI': '135', + 'Otsego County, MI': '137', + 'Ottawa County, MI': '139', + 'Presque Isle County, MI': '141', + 'Roscommon County, MI': '143', + 'Saginaw County, MI': '145', + 'Sanilac County, MI': '151', + 'Schoolcraft County, MI': '153', + 'Shiawassee County, MI': '155', + 'St. Clair County, MI': '147', + 'St. Joseph County, MI': '149', + 'Tuscola County, MI': '157', + 'Van Buren County, MI': '159', + 'Washtenaw County, MI': '161', + 'Wayne County, MI': '163', + 'Wexford County, MI': '165'}, + 27: {'--All--': '%', + 'Aitkin County, MN': '001', + 'Anoka County, MN': '003', + 'Becker County, MN': '005', + 'Beltrami County, MN': '007', + 'Benton County, MN': '009', + 'Big Stone County, MN': '011', + 'Blue Earth County, MN': '013', + 'Brown County, MN': '015', + 'Carlton County, MN': '017', + 'Carver County, MN': '019', + 'Cass County, MN': '021', + 'Chippewa County, MN': '023', + 'Chisago County, MN': '025', + 'Clay County, MN': '027', + 'Clearwater County, MN': '029', + 'Cook County, MN': '031', + 'Cottonwood County, MN': '033', + 'Crow Wing County, MN': '035', + 'Dakota County, MN': '037', + 'Dodge County, MN': '039', + 'Douglas County, MN': '041', + 'Faribault County, MN': '043', + 'Fillmore County, MN': '045', + 'Freeborn County, MN': '047', + 'Goodhue County, MN': '049', + 'Grant County, MN': '051', + 'Hennepin County, MN': '053', + 'Houston County, MN': '055', + 'Hubbard County, MN': '057', + 'Isanti County, MN': '059', + 'Itasca County, MN': '061', + 'Jackson County, MN': '063', + 'Kanabec County, MN': '065', + 'Kandiyohi County, MN': '067', + 'Kittson County, MN': '069', + 'Koochiching County, MN': '071', + 'Lac qui Parle County, MN': '073', + 'Lake County, MN': '075', + 'Lake of the Woods County, MN': '077', + 'Le Sueur County, MN': '079', + 'Lincoln County, MN': '081', + 'Lyon County, MN': '083', + 'Mahnomen County, MN': '087', + 'Marshall County, MN': '089', + 'Martin County, MN': '091', + 'McLeod County, MN': '085', + 'Meeker County, MN': '093', + 'Mille Lacs County, MN': '095', + 'Morrison County, MN': '097', + 'Mower County, MN': '099', + 'Murray County, MN': '101', + 'Nicollet County, MN': '103', + 'Nobles County, MN': '105', + 'Norman County, MN': '107', + 'Olmsted County, MN': '109', + 'Otter Tail County, MN': '111', + 'Pennington County, MN': '113', + 'Pine County, MN': '115', + 'Pipestone County, MN': '117', + 'Polk County, MN': '119', + 'Pope County, MN': '121', + 'Ramsey County, MN': '123', + 'Red Lake County, MN': '125', + 'Redwood County, MN': '127', + 'Renville County, MN': '129', + 'Rice County, MN': '131', + 'Rock County, MN': '133', + 'Roseau County, MN': '135', + 'Scott County, MN': '139', + 'Sherburne County, MN': '141', + 'Sibley County, MN': '143', + 'St. Louis County, MN': '137', + 'Stearns County, MN': '145', + 'Steele County, MN': '147', + 'Stevens County, MN': '149', + 'Swift County, MN': '151', + 'Todd County, MN': '153', + 'Traverse County, MN': '155', + 'Wabasha County, MN': '157', + 'Wadena County, MN': '159', + 'Waseca County, MN': '161', + 'Washington County, MN': '163', + 'Watonwan County, MN': '165', + 'Wilkin County, MN': '167', + 'Winona County, MN': '169', + 'Wright County, MN': '171', + 'Yellow Medicine County, MN': '173'}, + 28: {'--All--': '%', + 'Adams County, MS': '001', + 'Alcorn County, MS': '003', + 'Amite County, MS': '005', + 'Attala County, MS': '007', + 'Benton County, MS': '009', + 'Bolivar County, MS': '011', + 'Calhoun County, MS': '013', + 'Carroll County, MS': '015', + 'Chickasaw County, MS': '017', + 'Choctaw County, MS': '019', + 'Claiborne County, MS': '021', + 'Clarke County, MS': '023', + 'Clay County, MS': '025', + 'Coahoma County, MS': '027', + 'Copiah County, MS': '029', + 'Covington County, MS': '031', + 'DeSoto County, MS': '033', + 'Forrest County, MS': '035', + 'Franklin County, MS': '037', + 'George County, MS': '039', + 'Greene County, MS': '041', + 'Grenada County, MS': '043', + 'Hancock County, MS': '045', + 'Harrison County, MS': '047', + 'Hinds County, MS': '049', + 'Holmes County, MS': '051', + 'Humphreys County, MS': '053', + 'Issaquena County, MS': '055', + 'Itawamba County, MS': '057', + 'Jackson County, MS': '059', + 'Jasper County, MS': '061', + 'Jefferson County, MS': '063', + 'Jefferson Davis County, MS': '065', + 'Jones County, MS': '067', + 'Kemper County, MS': '069', + 'Lafayette County, MS': '071', + 'Lamar County, MS': '073', + 'Lauderdale County, MS': '075', + 'Lawrence County, MS': '077', + 'Leake County, MS': '079', + 'Lee County, MS': '081', + 'Leflore County, MS': '083', + 'Lincoln County, MS': '085', + 'Lowndes County, MS': '087', + 'Madison County, MS': '089', + 'Marion County, MS': '091', + 'Marshall County, MS': '093', + 'Monroe County, MS': '095', + 'Montgomery County, MS': '097', + 'Neshoba County, MS': '099', + 'Newton County, MS': '101', + 'Noxubee County, MS': '103', + 'Oktibbeha County, MS': '105', + 'Panola County, MS': '107', + 'Pearl River County, MS': '109', + 'Perry County, MS': '111', + 'Pike County, MS': '113', + 'Pontotoc County, MS': '115', + 'Prentiss County, MS': '117', + 'Quitman County, MS': '119', + 'Rankin County, MS': '121', + 'Scott County, MS': '123', + 'Sharkey County, MS': '125', + 'Simpson County, MS': '127', + 'Smith County, MS': '129', + 'Stone County, MS': '131', + 'Sunflower County, MS': '133', + 'Tallahatchie County, MS': '135', + 'Tate County, MS': '137', + 'Tippah County, MS': '139', + 'Tishomingo County, MS': '141', + 'Tunica County, MS': '143', + 'Union County, MS': '145', + 'Walthall County, MS': '147', + 'Warren County, MS': '149', + 'Washington County, MS': '151', + 'Wayne County, MS': '153', + 'Webster County, MS': '155', + 'Wilkinson County, MS': '157', + 'Winston County, MS': '159', + 'Yalobusha County, MS': '161', + 'Yazoo County, MS': '163'}, + 29: {'--All--': '%', + 'Adair County, MO': '001', + 'Andrew County, MO': '003', + 'Atchison County, MO': '005', + 'Audrain County, MO': '007', + 'Barry County, MO': '009', + 'Barton County, MO': '011', + 'Bates County, MO': '013', + 'Benton County, MO': '015', + 'Bollinger County, MO': '017', + 'Boone County, MO': '019', + 'Buchanan County, MO': '021', + 'Butler County, MO': '023', + 'Caldwell County, MO': '025', + 'Callaway County, MO': '027', + 'Camden County, MO': '029', + 'Cape Girardeau County, MO': '031', + 'Carroll County, MO': '033', + 'Carter County, MO': '035', + 'Cass County, MO': '037', + 'Cedar County, MO': '039', + 'Chariton County, MO': '041', + 'Christian County, MO': '043', + 'Clark County, MO': '045', + 'Clay County, MO': '047', + 'Clinton County, MO': '049', + 'Cole County, MO': '051', + 'Cooper County, MO': '053', + 'Crawford County, MO': '055', + 'Dade County, MO': '057', + 'Dallas County, MO': '059', + 'Daviess County, MO': '061', + 'DeKalb County, MO': '063', + 'Dent County, MO': '065', + 'Douglas County, MO': '067', + 'Dunklin County, MO': '069', + 'Franklin County, MO': '071', + 'Gasconade County, MO': '073', + 'Gentry County, MO': '075', + 'Greene County, MO': '077', + 'Grundy County, MO': '079', + 'Harrison County, MO': '081', + 'Henry County, MO': '083', + 'Hickory County, MO': '085', + 'Holt County, MO': '087', + 'Howard County, MO': '089', + 'Howell County, MO': '091', + 'Iron County, MO': '093', + 'Jackson County, MO': '095', + 'Jasper County, MO': '097', + 'Jefferson County, MO': '099', + 'Johnson County, MO': '101', + 'Knox County, MO': '103', + 'Laclede County, MO': '105', + 'Lafayette County, MO': '107', + 'Lawrence County, MO': '109', + 'Lewis County, MO': '111', + 'Lincoln County, MO': '113', + 'Linn County, MO': '115', + 'Livingston County, MO': '117', + 'Macon County, MO': '121', + 'Madison County, MO': '123', + 'Maries County, MO': '125', + 'Marion County, MO': '127', + 'McDonald County, MO': '119', + 'Mercer County, MO': '129', + 'Miller County, MO': '131', + 'Mississippi County, MO': '133', + 'Moniteau County, MO': '135', + 'Monroe County, MO': '137', + 'Montgomery County, MO': '139', + 'Morgan County, MO': '141', + 'New Madrid County, MO': '143', + 'Newton County, MO': '145', + 'Nodaway County, MO': '147', + 'Oregon County, MO': '149', + 'Osage County, MO': '151', + 'Ozark County, MO': '153', + 'Pemiscot County, MO': '155', + 'Perry County, MO': '157', + 'Pettis County, MO': '159', + 'Phelps County, MO': '161', + 'Pike County, MO': '163', + 'Platte County, MO': '165', + 'Polk County, MO': '167', + 'Pulaski County, MO': '169', + 'Putnam County, MO': '171', + 'Ralls County, MO': '173', + 'Randolph County, MO': '175', + 'Ray County, MO': '177', + 'Reynolds County, MO': '179', + 'Ripley County, MO': '181', + 'Saline County, MO': '195', + 'Schuyler County, MO': '197', + 'Scotland County, MO': '199', + 'Scott County, MO': '201', + 'Shannon County, MO': '203', + 'Shelby County, MO': '205', + 'St. Charles County, MO': '183', + 'St. Clair County, MO': '185', + 'St. Francois County, MO': '187', + 'St. Louis County, MO': '189', + 'St. Louis city, MO': '510', + 'Ste. Genevieve County, MO': '186', + 'Stoddard County, MO': '207', + 'Stone County, MO': '209', + 'Sullivan County, MO': '211', + 'Taney County, MO': '213', + 'Texas County, MO': '215', + 'Vernon County, MO': '217', + 'Warren County, MO': '219', + 'Washington County, MO': '221', + 'Wayne County, MO': '223', + 'Webster County, MO': '225', + 'Worth County, MO': '227', + 'Wright County, MO': '229'}, + 30: {'--All--': '%', + 'Beaverhead County, MT': '001', + 'Big Horn County, MT': '003', + 'Blaine County, MT': '005', + 'Broadwater County, MT': '007', + 'Carbon County, MT': '009', + 'Carter County, MT': '011', + 'Cascade County, MT': '013', + 'Chouteau County, MT': '015', + 'Custer County, MT': '017', + 'Daniels County, MT': '019', + 'Dawson County, MT': '021', + 'Deer Lodge County, MT': '023', + 'Fallon County, MT': '025', + 'Fergus County, MT': '027', + 'Flathead County, MT': '029', + 'Gallatin County, MT': '031', + 'Garfield County, MT': '033', + 'Glacier County, MT': '035', + 'Golden Valley County, MT': '037', + 'Granite County, MT': '039', + 'Hill County, MT': '041', + 'Jefferson County, MT': '043', + 'Judith Basin County, MT': '045', + 'Lake County, MT': '047', + 'Lewis and Clark County, MT': '049', + 'Liberty County, MT': '051', + 'Lincoln County, MT': '053', + 'Madison County, MT': '057', + 'McCone County, MT': '055', + 'Meagher County, MT': '059', + 'Mineral County, MT': '061', + 'Missoula County, MT': '063', + 'Musselshell County, MT': '065', + 'Park County, MT': '067', + 'Petroleum County, MT': '069', + 'Phillips County, MT': '071', + 'Pondera County, MT': '073', + 'Powder River County, MT': '075', + 'Powell County, MT': '077', + 'Prairie County, MT': '079', + 'Ravalli County, MT': '081', + 'Richland County, MT': '083', + 'Roosevelt County, MT': '085', + 'Rosebud County, MT': '087', + 'Sanders County, MT': '089', + 'Sheridan County, MT': '091', + 'Silver Bow County, MT': '093', + 'Stillwater County, MT': '095', + 'Sweet Grass County, MT': '097', + 'Teton County, MT': '099', + 'Toole County, MT': '101', + 'Treasure County, MT': '103', + 'Valley County, MT': '105', + 'Wheatland County, MT': '107', + 'Wibaux County, MT': '109', + 'Yellowstone County, MT': '111'}, + 31: {'--All--': '%', + 'Adams County, NE': '001', + 'Antelope County, NE': '003', + 'Arthur County, NE': '005', + 'Banner County, NE': '007', + 'Blaine County, NE': '009', + 'Boone County, NE': '011', + 'Box Butte County, NE': '013', + 'Boyd County, NE': '015', + 'Brown County, NE': '017', + 'Buffalo County, NE': '019', + 'Burt County, NE': '021', + 'Butler County, NE': '023', + 'Cass County, NE': '025', + 'Cedar County, NE': '027', + 'Chase County, NE': '029', + 'Cherry County, NE': '031', + 'Cheyenne County, NE': '033', + 'Clay County, NE': '035', + 'Colfax County, NE': '037', + 'Cuming County, NE': '039', + 'Custer County, NE': '041', + 'Dakota County, NE': '043', + 'Dawes County, NE': '045', + 'Dawson County, NE': '047', + 'Deuel County, NE': '049', + 'Dixon County, NE': '051', + 'Dodge County, NE': '053', + 'Douglas County, NE': '055', + 'Dundy County, NE': '057', + 'Fillmore County, NE': '059', + 'Franklin County, NE': '061', + 'Frontier County, NE': '063', + 'Furnas County, NE': '065', + 'Gage County, NE': '067', + 'Garden County, NE': '069', + 'Garfield County, NE': '071', + 'Gosper County, NE': '073', + 'Grant County, NE': '075', + 'Greeley County, NE': '077', + 'Hall County, NE': '079', + 'Hamilton County, NE': '081', + 'Harlan County, NE': '083', + 'Hayes County, NE': '085', + 'Hitchcock County, NE': '087', + 'Holt County, NE': '089', + 'Hooker County, NE': '091', + 'Howard County, NE': '093', + 'Jefferson County, NE': '095', + 'Johnson County, NE': '097', + 'Kearney County, NE': '099', + 'Keith County, NE': '101', + 'Keya Paha County, NE': '103', + 'Kimball County, NE': '105', + 'Knox County, NE': '107', + 'Lancaster County, NE': '109', + 'Lincoln County, NE': '111', + 'Logan County, NE': '113', + 'Loup County, NE': '115', + 'Madison County, NE': '119', + 'McPherson County, NE': '117', + 'Merrick County, NE': '121', + 'Morrill County, NE': '123', + 'Nance County, NE': '125', + 'Nemaha County, NE': '127', + 'Nuckolls County, NE': '129', + 'Otoe County, NE': '131', + 'Pawnee County, NE': '133', + 'Perkins County, NE': '135', + 'Phelps County, NE': '137', + 'Pierce County, NE': '139', + 'Platte County, NE': '141', + 'Polk County, NE': '143', + 'Red Willow County, NE': '145', + 'Richardson County, NE': '147', + 'Rock County, NE': '149', + 'Saline County, NE': '151', + 'Sarpy County, NE': '153', + 'Saunders County, NE': '155', + 'Scotts Bluff County, NE': '157', + 'Seward County, NE': '159', + 'Sheridan County, NE': '161', + 'Sherman County, NE': '163', + 'Sioux County, NE': '165', + 'Stanton County, NE': '167', + 'Thayer County, NE': '169', + 'Thomas County, NE': '171', + 'Thurston County, NE': '173', + 'Valley County, NE': '175', + 'Washington County, NE': '177', + 'Wayne County, NE': '179', + 'Webster County, NE': '181', + 'Wheeler County, NE': '183', + 'York County, NE': '185'}, + 32: {'--All--': '%', + 'Carson City, NV': '510', + 'Churchill County, NV': '001', + 'Clark County, NV': '003', + 'Douglas County, NV': '005', + 'Elko County, NV': '007', + 'Esmeralda County, NV': '009', + 'Eureka County, NV': '011', + 'Humboldt County, NV': '013', + 'Lander County, NV': '015', + 'Lincoln County, NV': '017', + 'Lyon County, NV': '019', + 'Mineral County, NV': '021', + 'Nye County, NV': '023', + 'Pershing County, NV': '027', + 'Storey County, NV': '029', + 'Washoe County, NV': '031', + 'White Pine County, NV': '033'}, + 33: {'--All--': '%', + 'Belknap County, NH': '001', + 'Carroll County, NH': '003', + 'Cheshire County, NH': '005', + 'Coos County, NH': '007', + 'Grafton County, NH': '009', + 'Hillsborough County, NH': '011', + 'Merrimack County, NH': '013', + 'Rockingham County, NH': '015', + 'Strafford County, NH': '017', + 'Sullivan County, NH': '019'}, + 34: {'--All--': '%', + 'Atlantic County, NJ': '001', + 'Bergen County, NJ': '003', + 'Burlington County, NJ': '005', + 'Camden County, NJ': '007', + 'Cape May County, NJ': '009', + 'Cumberland County, NJ': '011', + 'Essex County, NJ': '013', + 'Gloucester County, NJ': '015', + 'Hudson County, NJ': '017', + 'Hunterdon County, NJ': '019', + 'Mercer County, NJ': '021', + 'Middlesex County, NJ': '023', + 'Monmouth County, NJ': '025', + 'Morris County, NJ': '027', + 'Ocean County, NJ': '029', + 'Passaic County, NJ': '031', + 'Salem County, NJ': '033', + 'Somerset County, NJ': '035', + 'Sussex County, NJ': '037', + 'Union County, NJ': '039', + 'Warren County, NJ': '041'}, + 35: {'--All--': '%', + 'Bernalillo County, NM': '001', + 'Catron County, NM': '003', + 'Chaves County, NM': '005', + 'Cibola County, NM': '006', + 'Colfax County, NM': '007', + 'Curry County, NM': '009', + 'DeBaca County, NM': '011', + 'Dona Ana County, NM': '013', + 'Eddy County, NM': '015', + 'Grant County, NM': '017', + 'Guadalupe County, NM': '019', + 'Harding County, NM': '021', + 'Hidalgo County, NM': '023', + 'Lea County, NM': '025', + 'Lincoln County, NM': '027', + 'Los Alamos County, NM': '028', + 'Luna County, NM': '029', + 'McKinley County, NM': '031', + 'Mora County, NM': '033', + 'Otero County, NM': '035', + 'Quay County, NM': '037', + 'Rio Arriba County, NM': '039', + 'Roosevelt County, NM': '041', + 'San Juan County, NM': '045', + 'San Miguel County, NM': '047', + 'Sandoval County, NM': '043', + 'Santa Fe County, NM': '049', + 'Sierra County, NM': '051', + 'Socorro County, NM': '053', + 'Taos County, NM': '055', + 'Torrance County, NM': '057', + 'Union County, NM': '059', + 'Valencia County, NM': '061'}, + 36: {'--All--': '%', + 'Albany County, NY': '001', + 'Allegany County, NY': '003', + 'Bronx County, NY': '005', + 'Broome County, NY': '007', + 'Cattaraugus County, NY': '009', + 'Cayuga County, NY': '011', + 'Chautauqua County, NY': '013', + 'Chemung County, NY': '015', + 'Chenango County, NY': '017', + 'Clinton County, NY': '019', + 'Columbia County, NY': '021', + 'Cortland County, NY': '023', + 'Delaware County, NY': '025', + 'Dutchess County, NY': '027', + 'Erie County, NY': '029', + 'Essex County, NY': '031', + 'Franklin County, NY': '033', + 'Fulton County, NY': '035', + 'Genesee County, NY': '037', + 'Greene County, NY': '039', + 'Hamilton County, NY': '041', + 'Herkimer County, NY': '043', + 'Jefferson County, NY': '045', + 'Kings County, NY': '047', + 'Lewis County, NY': '049', + 'Livingston County, NY': '051', + 'Madison County, NY': '053', + 'Monroe County, NY': '055', + 'Montgomery County, NY': '057', + 'Nassau County, NY': '059', + 'New York County, NY': '061', + 'Niagara County, NY': '063', + 'Oneida County, NY': '065', + 'Onondaga County, NY': '067', + 'Ontario County, NY': '069', + 'Orange County, NY': '071', + 'Orleans County, NY': '073', + 'Oswego County, NY': '075', + 'Otsego County, NY': '077', + 'Putnam County, NY': '079', + 'Queens County, NY': '081', + 'Rensselaer County, NY': '083', + 'Richmond County, NY': '085', + 'Rockland County, NY': '087', + 'Saratoga County, NY': '091', + 'Schenectady County, NY': '093', + 'Schoharie County, NY': '095', + 'Schuyler County, NY': '097', + 'Seneca County, NY': '099', + 'St. Lawrence County, NY': '089', + 'Steuben County, NY': '101', + 'Suffolk County, NY': '103', + 'Sullivan County, NY': '105', + 'Tioga County, NY': '107', + 'Tompkins County, NY': '109', + 'Ulster County, NY': '111', + 'Warren County, NY': '113', + 'Washington County, NY': '115', + 'Wayne County, NY': '117', + 'Westchester County, NY': '119', + 'Wyoming County, NY': '121', + 'Yates County, NY': '123'}, + 37: {'--All--': '%', + 'Alamance County, NC': '001', + 'Alexander County, NC': '003', + 'Alleghany County, NC': '005', + 'Anson County, NC': '007', + 'Ashe County, NC': '009', + 'Avery County, NC': '011', + 'Beaufort County, NC': '013', + 'Bertie County, NC': '015', + 'Bladen County, NC': '017', + 'Brunswick County, NC': '019', + 'Buncombe County, NC': '021', + 'Burke County, NC': '023', + 'Cabarrus County, NC': '025', + 'Caldwell County, NC': '027', + 'Camden County, NC': '029', + 'Carteret County, NC': '031', + 'Caswell County, NC': '033', + 'Catawba County, NC': '035', + 'Chatham County, NC': '037', + 'Cherokee County, NC': '039', + 'Chowan County, NC': '041', + 'Clay County, NC': '043', + 'Cleveland County, NC': '045', + 'Columbus County, NC': '047', + 'Craven County, NC': '049', + 'Cumberland County, NC': '051', + 'Currituck County, NC': '053', + 'Dare County, NC': '055', + 'Davidson County, NC': '057', + 'Davie County, NC': '059', + 'Duplin County, NC': '061', + 'Durham County, NC': '063', + 'Edgecombe County, NC': '065', + 'Forsyth County, NC': '067', + 'Franklin County, NC': '069', + 'Gaston County, NC': '071', + 'Gates County, NC': '073', + 'Graham County, NC': '075', + 'Granville County, NC': '077', + 'Greene County, NC': '079', + 'Guilford County, NC': '081', + 'Halifax County, NC': '083', + 'Harnett County, NC': '085', + 'Haywood County, NC': '087', + 'Henderson County, NC': '089', + 'Hertford County, NC': '091', + 'Hoke County, NC': '093', + 'Hyde County, NC': '095', + 'Iredell County, NC': '097', + 'Jackson County, NC': '099', + 'Johnston County, NC': '101', + 'Jones County, NC': '103', + 'Lee County, NC': '105', + 'Lenoir County, NC': '107', + 'Lincoln County, NC': '109', + 'Macon County, NC': '113', + 'Madison County, NC': '115', + 'Martin County, NC': '117', + 'McDowell County, NC': '111', + 'Mecklenburg County, NC': '119', + 'Mitchell County, NC': '121', + 'Montgomery County, NC': '123', + 'Moore County, NC': '125', + 'Nash County, NC': '127', + 'New Hanover County, NC': '129', + 'Northampton County, NC': '131', + 'Onslow County, NC': '133', + 'Orange County, NC': '135', + 'Pamlico County, NC': '137', + 'Pasquotank County, NC': '139', + 'Pender County, NC': '141', + 'Perquimans County, NC': '143', + 'Person County, NC': '145', + 'Pitt County, NC': '147', + 'Polk County, NC': '149', + 'Randolph County, NC': '151', + 'Richmond County, NC': '153', + 'Robeson County, NC': '155', + 'Rockingham County, NC': '157', + 'Rowan County, NC': '159', + 'Rutherford County, NC': '161', + 'Sampson County, NC': '163', + 'Scotland County, NC': '165', + 'Stanly County, NC': '167', + 'Stokes County, NC': '169', + 'Surry County, NC': '171', + 'Swain County, NC': '173', + 'Transylvania County, NC': '175', + 'Tyrrell County, NC': '177', + 'Union County, NC': '179', + 'Vance County, NC': '181', + 'Wake County, NC': '183', + 'Warren County, NC': '185', + 'Washington County, NC': '187', + 'Watauga County, NC': '189', + 'Wayne County, NC': '191', + 'Wilkes County, NC': '193', + 'Wilson County, NC': '195', + 'Yadkin County, NC': '197', + 'Yancey County, NC': '199'}, + 38: {'--All--': '%', + 'Adams County, ND': '001', + 'Barnes County, ND': '003', + 'Benson County, ND': '005', + 'Billings County, ND': '007', + 'Bottineau County, ND': '009', + 'Bowman County, ND': '011', + 'Burke County, ND': '013', + 'Burleigh County, ND': '015', + 'Cass County, ND': '017', + 'Cavalier County, ND': '019', + 'Dickey County, ND': '021', + 'Divide County, ND': '023', + 'Dunn County, ND': '025', + 'Eddy County, ND': '027', + 'Emmons County, ND': '029', + 'Foster County, ND': '031', + 'Golden Valley County, ND': '033', + 'Grand Forks County, ND': '035', + 'Grant County, ND': '037', + 'Griggs County, ND': '039', + 'Hettinger County, ND': '041', + 'Kidder County, ND': '043', + 'LaMoure County, ND': '045', + 'Logan County, ND': '047', + 'McHenry County, ND': '049', + 'McIntosh County, ND': '051', + 'McKenzie County, ND': '053', + 'McLean County, ND': '055', + 'Mercer County, ND': '057', + 'Morton County, ND': '059', + 'Mountrail County, ND': '061', + 'Nelson County, ND': '063', + 'Oliver County, ND': '065', + 'Pembina County, ND': '067', + 'Pierce County, ND': '069', + 'Ramsey County, ND': '071', + 'Ransom County, ND': '073', + 'Renville County, ND': '075', + 'Richland County, ND': '077', + 'Rolette County, ND': '079', + 'Sargent County, ND': '081', + 'Sheridan County, ND': '083', + 'Sioux County, ND': '085', + 'Slope County, ND': '087', + 'Stark County, ND': '089', + 'Steele County, ND': '091', + 'Stutsman County, ND': '093', + 'Towner County, ND': '095', + 'Traill County, ND': '097', + 'Walsh County, ND': '099', + 'Ward County, ND': '101', + 'Wells County, ND': '103', + 'Williams County, ND': '105'}, + 39: {'--All--': '%', + 'Adams County, OH': '001', + 'Allen County, OH': '003', + 'Ashland County, OH': '005', + 'Ashtabula County, OH': '007', + 'Athens County, OH': '009', + 'Auglaize County, OH': '011', + 'Belmont County, OH': '013', + 'Brown County, OH': '015', + 'Butler County, OH': '017', + 'Carroll County, OH': '019', + 'Champaign County, OH': '021', + 'Clark County, OH': '023', + 'Clermont County, OH': '025', + 'Clinton County, OH': '027', + 'Columbiana County, OH': '029', + 'Coshocton County, OH': '031', + 'Crawford County, OH': '033', + 'Cuyahoga County, OH': '035', + 'Darke County, OH': '037', + 'Defiance County, OH': '039', + 'Delaware County, OH': '041', + 'Erie County, OH': '043', + 'Fairfield County, OH': '045', + 'Fayette County, OH': '047', + 'Franklin County, OH': '049', + 'Fulton County, OH': '051', + 'Gallia County, OH': '053', + 'Geauga County, OH': '055', + 'Greene County, OH': '057', + 'Guernsey County, OH': '059', + 'Hamilton County, OH': '061', + 'Hancock County, OH': '063', + 'Hardin County, OH': '065', + 'Harrison County, OH': '067', + 'Henry County, OH': '069', + 'Highland County, OH': '071', + 'Hocking County, OH': '073', + 'Holmes County, OH': '075', + 'Huron County, OH': '077', + 'Jackson County, OH': '079', + 'Jefferson County, OH': '081', + 'Knox County, OH': '083', + 'Lake County, OH': '085', + 'Lawrence County, OH': '087', + 'Licking County, OH': '089', + 'Logan County, OH': '091', + 'Lorain County, OH': '093', + 'Lucas County, OH': '095', + 'Madison County, OH': '097', + 'Mahoning County, OH': '099', + 'Marion County, OH': '101', + 'Medina County, OH': '103', + 'Meigs County, OH': '105', + 'Mercer County, OH': '107', + 'Miami County, OH': '109', + 'Monroe County, OH': '111', + 'Montgomery County, OH': '113', + 'Morgan County, OH': '115', + 'Morrow County, OH': '117', + 'Muskingum County, OH': '119', + 'Noble County, OH': '121', + 'Ottawa County, OH': '123', + 'Paulding County, OH': '125', + 'Perry County, OH': '127', + 'Pickaway County, OH': '129', + 'Pike County, OH': '131', + 'Portage County, OH': '133', + 'Preble County, OH': '135', + 'Putnam County, OH': '137', + 'Richland County, OH': '139', + 'Ross County, OH': '141', + 'Sandusky County, OH': '143', + 'Scioto County, OH': '145', + 'Seneca County, OH': '147', + 'Shelby County, OH': '149', + 'Stark County, OH': '151', + 'Summit County, OH': '153', + 'Trumbull County, OH': '155', + 'Tuscarawas County, OH': '157', + 'Union County, OH': '159', + 'Van Wert County, OH': '161', + 'Vinton County, OH': '163', + 'Warren County, OH': '165', + 'Washington County, OH': '167', + 'Wayne County, OH': '169', + 'Williams County, OH': '171', + 'Wood County, OH': '173', + 'Wyandot County, OH': '175'}, + 40: {'--All--': '%', + 'Adair County, OK': '001', + 'Alfalfa County, OK': '003', + 'Atoka County, OK': '005', + 'Beaver County, OK': '007', + 'Beckham County, OK': '009', + 'Blaine County, OK': '011', + 'Bryan County, OK': '013', + 'Caddo County, OK': '015', + 'Canadian County, OK': '017', + 'Carter County, OK': '019', + 'Cherokee County, OK': '021', + 'Choctaw County, OK': '023', + 'Cimarron County, OK': '025', + 'Cleveland County, OK': '027', + 'Coal County, OK': '029', + 'Comanche County, OK': '031', + 'Cotton County, OK': '033', + 'Craig County, OK': '035', + 'Creek County, OK': '037', + 'Custer County, OK': '039', + 'Delaware County, OK': '041', + 'Dewey County, OK': '043', + 'Ellis County, OK': '045', + 'Garfield County, OK': '047', + 'Garvin County, OK': '049', + 'Grady County, OK': '051', + 'Grant County, OK': '053', + 'Greer County, OK': '055', + 'Harmon County, OK': '057', + 'Harper County, OK': '059', + 'Haskell County, OK': '061', + 'Hughes County, OK': '063', + 'Jackson County, OK': '065', + 'Jefferson County, OK': '067', + 'Johnston County, OK': '069', + 'Kay County, OK': '071', + 'Kingfisher County, OK': '073', + 'Kiowa County, OK': '075', + 'Latimer County, OK': '077', + 'Le Flore County, OK': '079', + 'Lincoln County, OK': '081', + 'Logan County, OK': '083', + 'Love County, OK': '085', + 'Major County, OK': '093', + 'Marshall County, OK': '095', + 'Mayes County, OK': '097', + 'McClain County, OK': '087', + 'McCurtain County, OK': '089', + 'McIntosh County, OK': '091', + 'Murray County, OK': '099', + 'Muskogee County, OK': '101', + 'Noble County, OK': '103', + 'Nowata County, OK': '105', + 'Okfuskee County, OK': '107', + 'Oklahoma County, OK': '109', + 'Okmulgee County, OK': '111', + 'Osage County, OK': '113', + 'Ottawa County, OK': '115', + 'Pawnee County, OK': '117', + 'Payne County, OK': '119', + 'Pittsburg County, OK': '121', + 'Pontotoc County, OK': '123', + 'Pottawatomie County, OK': '125', + 'Pushmataha County, OK': '127', + 'Roger Mills County, OK': '129', + 'Rogers County, OK': '131', + 'Seminole County, OK': '133', + 'Sequoyah County, OK': '135', + 'Stephens County, OK': '137', + 'Texas County, OK': '139', + 'Tillman County, OK': '141', + 'Tulsa County, OK': '143', + 'Wagoner County, OK': '145', + 'Washington County, OK': '147', + 'Washita County, OK': '149', + 'Woods County, OK': '151', + 'Woodward County, OK': '153'}, + 41: {'--All--': '%', + 'Baker County, OR': '001', + 'Benton County, OR': '003', + 'Clackamas County, OR': '005', + 'Clatsop County, OR': '007', + 'Columbia County, OR': '009', + 'Coos County, OR': '011', + 'Crook County, OR': '013', + 'Curry County, OR': '015', + 'Deschutes County, OR': '017', + 'Douglas County, OR': '019', + 'Gilliam County, OR': '021', + 'Grant County, OR': '023', + 'Harney County, OR': '025', + 'Hood River County, OR': '027', + 'Jackson County, OR': '029', + 'Jefferson County, OR': '031', + 'Josephine County, OR': '033', + 'Klamath County, OR': '035', + 'Lake County, OR': '037', + 'Lane County, OR': '039', + 'Lincoln County, OR': '041', + 'Linn County, OR': '043', + 'Malheur County, OR': '045', + 'Marion County, OR': '047', + 'Morrow County, OR': '049', + 'Multnomah County, OR': '051', + 'Polk County, OR': '053', + 'Sherman County, OR': '055', + 'Tillamook County, OR': '057', + 'Umatilla County, OR': '059', + 'Union County, OR': '061', + 'Wallowa County, OR': '063', + 'Wasco County, OR': '065', + 'Washington County, OR': '067', + 'Wheeler County, OR': '069', + 'Yamhill County, OR': '071'}, + 42: {'--All--': '%', + 'Adams County, PA': '001', + 'Allegheny County, PA': '003', + 'Armstrong County, PA': '005', + 'Beaver County, PA': '007', + 'Bedford County, PA': '009', + 'Berks County, PA': '011', + 'Blair County, PA': '013', + 'Bradford County, PA': '015', + 'Bucks County, PA': '017', + 'Butler County, PA': '019', + 'Cambria County, PA': '021', + 'Cameron County, PA': '023', + 'Carbon County, PA': '025', + 'Centre County, PA': '027', + 'Chester County, PA': '029', + 'Clarion County, PA': '031', + 'Clearfield County, PA': '033', + 'Clinton County, PA': '035', + 'Columbia County, PA': '037', + 'Crawford County, PA': '039', + 'Cumberland County, PA': '041', + 'Dauphin County, PA': '043', + 'Delaware County, PA': '045', + 'Elk County, PA': '047', + 'Erie County, PA': '049', + 'Fayette County, PA': '051', + 'Forest County, PA': '053', + 'Franklin County, PA': '055', + 'Fulton County, PA': '057', + 'Greene County, PA': '059', + 'Huntingdon County, PA': '061', + 'Indiana County, PA': '063', + 'Jefferson County, PA': '065', + 'Juniata County, PA': '067', + 'Lackawanna County, PA': '069', + 'Lancaster County, PA': '071', + 'Lawrence County, PA': '073', + 'Lebanon County, PA': '075', + 'Lehigh County, PA': '077', + 'Luzerne County, PA': '079', + 'Lycoming County, PA': '081', + 'McKean County, PA': '083', + 'Mercer County, PA': '085', + 'Mifflin County, PA': '087', + 'Monroe County, PA': '089', + 'Montgomery County, PA': '091', + 'Montour County, PA': '093', + 'Northampton County, PA': '095', + 'Northumberland County, PA': '097', + 'Perry County, PA': '099', + 'Philadelphia County/city, PA': '101', + 'Pike County, PA': '103', + 'Potter County, PA': '105', + 'Schuylkill County, PA': '107', + 'Snyder County, PA': '109', + 'Somerset County, PA': '111', + 'Sullivan County, PA': '113', + 'Susquehanna County, PA': '115', + 'Tioga County, PA': '117', + 'Union County, PA': '119', + 'Venango County, PA': '121', + 'Warren County, PA': '123', + 'Washington County, PA': '125', + 'Wayne County, PA': '127', + 'Westmoreland County, PA': '129', + 'Wyoming County, PA': '131', + 'York County, PA': '133'}, + 44: {'--All--': '%', + 'Bristol County, RI': '001', + 'Kent County, RI': '003', + 'Newport County, RI': '005', + 'Providence County, RI': '007', + 'Washington County, RI': '009'}, + 45: {'--All--': '%', + 'Abbeville County, SC': '001', + 'Aiken County, SC': '003', + 'Allendale County, SC': '005', + 'Anderson County, SC': '007', + 'Bamberg County, SC': '009', + 'Barnwell County, SC': '011', + 'Beaufort County, SC': '013', + 'Berkeley County, SC': '015', + 'Calhoun County, SC': '017', + 'Charleston County, SC': '019', + 'Cherokee County, SC': '021', + 'Chester County, SC': '023', + 'Chesterfield County, SC': '025', + 'Clarendon County, SC': '027', + 'Colleton County, SC': '029', + 'Darlington County, SC': '031', + 'Dillon County, SC': '033', + 'Dorchester County, SC': '035', + 'Edgefield County, SC': '037', + 'Fairfield County, SC': '039', + 'Florence County, SC': '041', + 'Georgetown County, SC': '043', + 'Greenville County, SC': '045', + 'Greenwood County, SC': '047', + 'Hampton County, SC': '049', + 'Horry County, SC': '051', + 'Jasper County, SC': '053', + 'Kershaw County, SC': '055', + 'Lancaster County, SC': '057', + 'Laurens County, SC': '059', + 'Lee County, SC': '061', + 'Lexington County, SC': '063', + 'Marion County, SC': '067', + 'Marlboro County, SC': '069', + 'McCormick County, SC': '065', + 'Newberry County, SC': '071', + 'Oconee County, SC': '073', + 'Orangeburg County, SC': '075', + 'Pickens County, SC': '077', + 'Richland County, SC': '079', + 'Saluda County, SC': '081', + 'Spartanburg County, SC': '083', + 'Sumter County, SC': '085', + 'Union County, SC': '087', + 'Williamsburg County, SC': '089', + 'York County, SC': '091'}, + 46: {'--All--': '%', + 'Aurora County, SD': '003', + 'Beadle County, SD': '005', + 'Bennett County, SD': '007', + 'Bon Homme County, SD': '009', + 'Brookings County, SD': '011', + 'Brown County, SD': '013', + 'Brule County, SD': '015', + 'Buffalo County, SD': '017', + 'Butte County, SD': '019', + 'Campbell County, SD': '021', + 'Charles Mix County, SD': '023', + 'Clark County, SD': '025', + 'Clay County, SD': '027', + 'Codington County, SD': '029', + 'Corson County, SD': '031', + 'Custer County, SD': '033', + 'Davison County, SD': '035', + 'Day County, SD': '037', + 'Deuel County, SD': '039', + 'Dewey County, SD': '041', + 'Douglas County, SD': '043', + 'Edmunds County, SD': '045', + 'Fall River County, SD': '047', + 'Faulk County, SD': '049', + 'Grant County, SD': '051', + 'Gregory County, SD': '053', + 'Haakon County, SD': '055', + 'Hamlin County, SD': '057', + 'Hand County, SD': '059', + 'Hanson County, SD': '061', + 'Harding County, SD': '063', + 'Hughes County, SD': '065', + 'Hutchinson County, SD': '067', + 'Hyde County, SD': '069', + 'Jackson County, SD': '071', + 'Jerauld County, SD': '073', + 'Jones County, SD': '075', + 'Kingsbury County, SD': '077', + 'Lake County, SD': '079', + 'Lawrence County, SD': '081', + 'Lincoln County, SD': '083', + 'Lyman County, SD': '085', + 'Marshall County, SD': '091', + 'McCook County, SD': '087', + 'McPherson County, SD': '089', + 'Meade County, SD': '093', + 'Mellette County, SD': '095', + 'Miner County, SD': '097', + 'Minnehaha County, SD': '099', + 'Moody County, SD': '101', + 'Pennington County, SD': '103', + 'Perkins County, SD': '105', + 'Potter County, SD': '107', + 'Roberts County, SD': '109', + 'Sanborn County, SD': '111', + 'Shannon County, SD': '113', + 'Spink County, SD': '115', + 'Stanley County, SD': '117', + 'Sully County, SD': '119', + 'Todd County, SD': '121', + 'Tripp County, SD': '123', + 'Turner County, SD': '125', + 'Union County, SD': '127', + 'Walworth County, SD': '129', + 'Yankton County, SD': '135', + 'Ziebach County, SD': '137'}, + 47: {'--All--': '%', + 'Anderson County, TN': '001', + 'Bedford County, TN': '003', + 'Benton County, TN': '005', + 'Bledsoe County, TN': '007', + 'Blount County, TN': '009', + 'Bradley County, TN': '011', + 'Campbell County, TN': '013', + 'Cannon County, TN': '015', + 'Carroll County, TN': '017', + 'Carter County, TN': '019', + 'Cheatham County, TN': '021', + 'Chester County, TN': '023', + 'Claiborne County, TN': '025', + 'Clay County, TN': '027', + 'Cocke County, TN': '029', + 'Coffee County, TN': '031', + 'Crockett County, TN': '033', + 'Cumberland County, TN': '035', + 'Davidson County, TN': '037', + 'DeKalb County, TN': '041', + 'Decatur County, TN': '039', + 'Dickson County, TN': '043', + 'Dyer County, TN': '045', + 'Fayette County, TN': '047', + 'Fentress County, TN': '049', + 'Franklin County, TN': '051', + 'Gibson County, TN': '053', + 'Giles County, TN': '055', + 'Grainger County, TN': '057', + 'Greene County, TN': '059', + 'Grundy County, TN': '061', + 'Hamblen County, TN': '063', + 'Hamilton County, TN': '065', + 'Hancock County, TN': '067', + 'Hardeman County, TN': '069', + 'Hardin County, TN': '071', + 'Hawkins County, TN': '073', + 'Haywood County, TN': '075', + 'Henderson County, TN': '077', + 'Henry County, TN': '079', + 'Hickman County, TN': '081', + 'Houston County, TN': '083', + 'Humphreys County, TN': '085', + 'Jackson County, TN': '087', + 'Jefferson County, TN': '089', + 'Johnson County, TN': '091', + 'Knox County, TN': '093', + 'Lake County, TN': '095', + 'Lauderdale County, TN': '097', + 'Lawrence County, TN': '099', + 'Lewis County, TN': '101', + 'Lincoln County, TN': '103', + 'Loudon County, TN': '105', + 'Macon County, TN': '111', + 'Madison County, TN': '113', + 'Marion County, TN': '115', + 'Marshall County, TN': '117', + 'Maury County, TN': '119', + 'McMinn County, TN': '107', + 'McNairy County, TN': '109', + 'Meigs County, TN': '121', + 'Monroe County, TN': '123', + 'Montgomery County, TN': '125', + 'Moore County, TN': '127', + 'Morgan County, TN': '129', + 'Obion County, TN': '131', + 'Overton County, TN': '133', + 'Perry County, TN': '135', + 'Pickett County, TN': '137', + 'Polk County, TN': '139', + 'Putnam County, TN': '141', + 'Rhea County, TN': '143', + 'Roane County, TN': '145', + 'Robertson County, TN': '147', + 'Rutherford County, TN': '149', + 'Scott County, TN': '151', + 'Sequatchie County, TN': '153', + 'Sevier County, TN': '155', + 'Shelby County, TN': '157', + 'Smith County, TN': '159', + 'Stewart County, TN': '161', + 'Sullivan County, TN': '163', + 'Sumner County, TN': '165', + 'Tipton County, TN': '167', + 'Trousdale County, TN': '169', + 'Unicoi County, TN': '171', + 'Union County, TN': '173', + 'Van Buren County, TN': '175', + 'Warren County, TN': '177', + 'Washington County, TN': '179', + 'Wayne County, TN': '181', + 'Weakley County, TN': '183', + 'White County, TN': '185', + 'Williamson County, TN': '187', + 'Wilson County, TN': '189'}, + 48: {'--All--': '%', + 'Anderson County, TX': '001', + 'Andrews County, TX': '003', + 'Angelina County, TX': '005', + 'Aransas County, TX': '007', + 'Archer County, TX': '009', + 'Armstrong County, TX': '011', + 'Atascosa County, TX': '013', + 'Austin County, TX': '015', + 'Bailey County, TX': '017', + 'Bandera County, TX': '019', + 'Bastrop County, TX': '021', + 'Baylor County, TX': '023', + 'Bee County, TX': '025', + 'Bell County, TX': '027', + 'Bexar County, TX': '029', + 'Blanco County, TX': '031', + 'Borden County, TX': '033', + 'Bosque County, TX': '035', + 'Bowie County, TX': '037', + 'Brazoria County, TX': '039', + 'Brazos County, TX': '041', + 'Brewster County, TX': '043', + 'Briscoe County, TX': '045', + 'Brooks County, TX': '047', + 'Brown County, TX': '049', + 'Burleson County, TX': '051', + 'Burnet County, TX': '053', + 'Caldwell County, TX': '055', + 'Calhoun County, TX': '057', + 'Callahan County, TX': '059', + 'Cameron County, TX': '061', + 'Camp County, TX': '063', + 'Carson County, TX': '065', + 'Cass County, TX': '067', + 'Castro County, TX': '069', + 'Chambers County, TX': '071', + 'Cherokee County, TX': '073', + 'Childress County, TX': '075', + 'Clay County, TX': '077', + 'Cochran County, TX': '079', + 'Coke County, TX': '081', + 'Coleman County, TX': '083', + 'Collin County, TX': '085', + 'Collingsworth County, TX': '087', + 'Colorado County, TX': '089', + 'Comal County, TX': '091', + 'Comanche County, TX': '093', + 'Concho County, TX': '095', + 'Cooke County, TX': '097', + 'Coryell County, TX': '099', + 'Cottle County, TX': '101', + 'Crane County, TX': '103', + 'Crockett County, TX': '105', + 'Crosby County, TX': '107', + 'Culberson County, TX': '109', + 'Dallam County, TX': '111', + 'Dallas County, TX': '113', + 'Dawson County, TX': '115', + 'DeWitt County, TX': '123', + 'Deaf Smith County, TX': '117', + 'Delta County, TX': '119', + 'Denton County, TX': '121', + 'Dickens County, TX': '125', + 'Dimmit County, TX': '127', + 'Donley County, TX': '129', + 'Duval County, TX': '131', + 'Eastland County, TX': '133', + 'Ector County, TX': '135', + 'Edwards County, TX': '137', + 'El Paso County, TX': '141', + 'Ellis County, TX': '139', + 'Erath County, TX': '143', + 'Falls County, TX': '145', + 'Fannin County, TX': '147', + 'Fayette County, TX': '149', + 'Fisher County, TX': '151', + 'Floyd County, TX': '153', + 'Foard County, TX': '155', + 'Fort Bend County, TX': '157', + 'Franklin County, TX': '159', + 'Freestone County, TX': '161', + 'Frio County, TX': '163', + 'Gaines County, TX': '165', + 'Galveston County, TX': '167', + 'Garza County, TX': '169', + 'Gillespie County, TX': '171', + 'Glasscock County, TX': '173', + 'Goliad County, TX': '175', + 'Gonzales County, TX': '177', + 'Gray County, TX': '179', + 'Grayson County, TX': '181', + 'Gregg County, TX': '183', + 'Grimes County, TX': '185', + 'Guadalupe County, TX': '187', + 'Hale County, TX': '189', + 'Hall County, TX': '191', + 'Hamilton County, TX': '193', + 'Hansford County, TX': '195', + 'Hardeman County, TX': '197', + 'Hardin County, TX': '199', + 'Harris County, TX': '201', + 'Harrison County, TX': '203', + 'Hartley County, TX': '205', + 'Haskell County, TX': '207', + 'Hays County, TX': '209', + 'Hemphill County, TX': '211', + 'Henderson County, TX': '213', + 'Hidalgo County, TX': '215', + 'Hill County, TX': '217', + 'Hockley County, TX': '219', + 'Hood County, TX': '221', + 'Hopkins County, TX': '223', + 'Houston County, TX': '225', + 'Howard County, TX': '227', + 'Hudspeth County, TX': '229', + 'Hunt County, TX': '231', + 'Hutchinson County, TX': '233', + 'Irion County, TX': '235', + 'Jack County, TX': '237', + 'Jackson County, TX': '239', + 'Jasper County, TX': '241', + 'Jeff Davis County, TX': '243', + 'Jefferson County, TX': '245', + 'Jim Hogg County, TX': '247', + 'Jim Wells County, TX': '249', + 'Johnson County, TX': '251', + 'Jones County, TX': '253', + 'Karnes County, TX': '255', + 'Kaufman County, TX': '257', + 'Kendall County, TX': '259', + 'Kenedy County, TX': '261', + 'Kent County, TX': '263', + 'Kerr County, TX': '265', + 'Kimble County, TX': '267', + 'King County, TX': '269', + 'Kinney County, TX': '271', + 'Kleberg County, TX': '273', + 'Knox County, TX': '275', + 'La Salle County, TX': '283', + 'Lamar County, TX': '277', + 'Lamb County, TX': '279', + 'Lampasas County, TX': '281', + 'Lavaca County, TX': '285', + 'Lee County, TX': '287', + 'Leon County, TX': '289', + 'Liberty County, TX': '291', + 'Limestone County, TX': '293', + 'Lipscomb County, TX': '295', + 'Live Oak County, TX': '297', + 'Llano County, TX': '299', + 'Loving County, TX': '301', + 'Lubbock County, TX': '303', + 'Lynn County, TX': '305', + 'Madison County, TX': '313', + 'Marion County, TX': '315', + 'Martin County, TX': '317', + 'Mason County, TX': '319', + 'Matagorda County, TX': '321', + 'Maverick County, TX': '323', + 'McCulloch County, TX': '307', + 'McLennan County, TX': '309', + 'McMullen County, TX': '311', + 'Medina County, TX': '325', + 'Menard County, TX': '327', + 'Midland County, TX': '329', + 'Milam County, TX': '331', + 'Mills County, TX': '333', + 'Mitchell County, TX': '335', + 'Montague County, TX': '337', + 'Montgomery County, TX': '339', + 'Moore County, TX': '341', + 'Morris County, TX': '343', + 'Motley County, TX': '345', + 'Nacogdoches County, TX': '347', + 'Navarro County, TX': '349', + 'Newton County, TX': '351', + 'Nolan County, TX': '353', + 'Nueces County, TX': '355', + 'Ochiltree County, TX': '357', + 'Oldham County, TX': '359', + 'Orange County, TX': '361', + 'Palo Pinto County, TX': '363', + 'Panola County, TX': '365', + 'Parker County, TX': '367', + 'Parmer County, TX': '369', + 'Pecos County, TX': '371', + 'Polk County, TX': '373', + 'Potter County, TX': '375', + 'Presidio County, TX': '377', + 'Rains County, TX': '379', + 'Randall County, TX': '381', + 'Reagan County, TX': '383', + 'Real County, TX': '385', + 'Red River County, TX': '387', + 'Reeves County, TX': '389', + 'Refugio County, TX': '391', + 'Roberts County, TX': '393', + 'Robertson County, TX': '395', + 'Rockwall County, TX': '397', + 'Runnels County, TX': '399', + 'Rusk County, TX': '401', + 'Sabine County, TX': '403', + 'San Augustine County, TX': '405', + 'San Jacinto County, TX': '407', + 'San Patricio County, TX': '409', + 'San Saba County, TX': '411', + 'Schleicher County, TX': '413', + 'Scurry County, TX': '415', + 'Shackelford County, TX': '417', + 'Shelby County, TX': '419', + 'Sherman County, TX': '421', + 'Smith County, TX': '423', + 'Somervell County, TX': '425', + 'Starr County, TX': '427', + 'Stephens County, TX': '429', + 'Sterling County, TX': '431', + 'Stonewall County, TX': '433', + 'Sutton County, TX': '435', + 'Swisher County, TX': '437', + 'Tarrant County, TX': '439', + 'Taylor County, TX': '441', + 'Terrell County, TX': '443', + 'Terry County, TX': '445', + 'Throckmorton County, TX': '447', + 'Titus County, TX': '449', + 'Tom Green County, TX': '451', + 'Travis County, TX': '453', + 'Trinity County, TX': '455', + 'Tyler County, TX': '457', + 'Upshur County, TX': '459', + 'Upton County, TX': '461', + 'Uvalde County, TX': '463', + 'Val Verde County, TX': '465', + 'Van Zandt County, TX': '467', + 'Victoria County, TX': '469', + 'Walker County, TX': '471', + 'Waller County, TX': '473', + 'Ward County, TX': '475', + 'Washington County, TX': '477', + 'Webb County, TX': '479', + 'Wharton County, TX': '481', + 'Wheeler County, TX': '483', + 'Wichita County, TX': '485', + 'Wilbarger County, TX': '487', + 'Willacy County, TX': '489', + 'Williamson County, TX': '491', + 'Wilson County, TX': '493', + 'Winkler County, TX': '495', + 'Wise County, TX': '497', + 'Wood County, TX': '499', + 'Yoakum County, TX': '501', + 'Young County, TX': '503', + 'Zapata County, TX': '505', + 'Zavala County, TX': '507'}, + 49: {'--All--': '%', + 'Beaver County, UT': '001', + 'Box Elder County, UT': '003', + 'Cache County, UT': '005', + 'Carbon County, UT': '007', + 'Daggett County, UT': '009', + 'Davis County, UT': '011', + 'Duchesne County, UT': '013', + 'Emery County, UT': '015', + 'Garfield County, UT': '017', + 'Grand County, UT': '019', + 'Iron County, UT': '021', + 'Juab County, UT': '023', + 'Kane County, UT': '025', + 'Millard County, UT': '027', + 'Morgan County, UT': '029', + 'Piute County, UT': '031', + 'Rich County, UT': '033', + 'Salt Lake County, UT': '035', + 'San Juan County, UT': '037', + 'Sanpete County, UT': '039', + 'Sevier County, UT': '041', + 'Summit County, UT': '043', + 'Tooele County, UT': '045', + 'Uintah County, UT': '047', + 'Utah County, UT': '049', + 'Wasatch County, UT': '051', + 'Washington County, UT': '053', + 'Wayne County, UT': '055', + 'Weber County, UT': '057'}, + 50: {'--All--': '%', + 'Addison County, VT': '001', + 'Bennington County, VT': '003', + 'Caledonia County, VT': '005', + 'Chittenden County, VT': '007', + 'Essex County, VT': '009', + 'Franklin County, VT': '011', + 'Grand Isle County, VT': '013', + 'Lamoille County, VT': '015', + 'Orange County, VT': '017', + 'Orleans County, VT': '019', + 'Rutland County, VT': '021', + 'Washington County, VT': '023', + 'Windham County, VT': '025', + 'Windsor County, VT': '027'}, + 51: {'--All--': '%', + 'Accomack County, VA': '001', + 'Albemarle County, VA': '003', + 'Alexandria city, VA': '510', + 'Alleghany County, VA': '005', + 'Amelia County, VA': '007', + 'Amherst County, VA': '009', + 'Appomattox County, VA': '011', + 'Arlington County, VA': '013', + 'Augusta County, VA': '015', + 'Bath County, VA': '017', + 'Bedford County, VA': '019', + 'Bedford city, VA': '515', + 'Bland County, VA': '021', + 'Botetourt County, VA': '023', + 'Bristol city, VA': '520', + 'Brunswick County, VA': '025', + 'Buchanan County, VA': '027', + 'Buckingham County, VA': '029', + 'Buena Vista city, VA': '530', + 'Campbell County, VA': '031', + 'Caroline County, VA': '033', + 'Carroll County, VA': '035', + 'Charles City County, VA': '036', + 'Charlotte County, VA': '037', + 'Charlottesville city, VA': '540', + 'Chesapeake city, VA': '550', + 'Chesterfield County, VA': '041', + 'Clarke County, VA': '043', + 'Colonial Heights city, VA': '570', + 'Covington city, VA': '580', + 'Craig County, VA': '045', + 'Culpeper County, VA': '047', + 'Cumberland County, VA': '049', + 'Danville city, VA': '590', + 'Dickenson County, VA': '051', + 'Dinwiddie County, VA': '053', + 'Emporia city, VA': '595', + 'Essex County, VA': '057', + 'Fairfax County, VA': '059', + 'Fairfax city, VA': '600', + 'Falls Church city, VA': '610', + 'Fauquier County, VA': '061', + 'Floyd County, VA': '063', + 'Fluvanna County, VA': '065', + 'Franklin County, VA': '067', + 'Franklin city, VA': '620', + 'Frederick County, VA': '069', + 'Fredericksburg city, VA': '630', + 'Galax city, VA': '640', + 'Giles County, VA': '071', + 'Gloucester County, VA': '073', + 'Goochland County, VA': '075', + 'Grayson County, VA': '077', + 'Greene County, VA': '079', + 'Greensville County, VA': '081', + 'Halifax County, VA': '083', + 'Hampton city, VA': '650', + 'Hanover County, VA': '085', + 'Harrisonburg city, VA': '660', + 'Henrico County, VA': '087', + 'Henry County, VA': '089', + 'Highland County, VA': '091', + 'Hopewell city, VA': '670', + 'Isle of Wight County, VA': '093', + 'James City County, VA': '095', + 'King George County, VA': '099', + 'King William County, VA': '101', + 'King and Queen County, VA': '097', + 'Lancaster County, VA': '103', + 'Lee County, VA': '105', + 'Lexington city, VA': '678', + 'Loudoun County, VA': '107', + 'Louisa County, VA': '109', + 'Lunenburg County, VA': '111', + 'Lynchburg city, VA': '680', + 'Madison County, VA': '113', + 'Manassas Park city, VA': '685', + 'Manassas city, VA': '683', + 'Martinsville city, VA': '690', + 'Mathews County, VA': '115', + 'Mecklenburg County, VA': '117', + 'Middlesex County, VA': '119', + 'Montgomery County, VA': '121', + 'Nelson County, VA': '125', + 'New Kent County, VA': '127', + 'Newport News city, VA': '700', + 'Norfolk city, VA': '710', + 'Northampton County, VA': '131', + 'Northumberland County, VA': '133', + 'Norton city, VA': '720', + 'Nottoway County, VA': '135', + 'Orange County, VA': '137', + 'Page County, VA': '139', + 'Patrick County, VA': '141', + 'Petersburg city, VA': '730', + 'Pittsylvania County, VA': '143', + 'Poquoson city, VA': '735', + 'Portsmouth city, VA': '740', + 'Powhatan County, VA': '145', + 'Prince Edward County, VA': '147', + 'Prince George County, VA': '149', + 'Prince William County, VA': '153', + 'Pulaski County, VA': '155', + 'Radford city, VA': '750', + 'Rappahannock County, VA': '157', + 'Richmond County, VA': '159', + 'Richmond city, VA': '760', + 'Roanoke County, VA': '161', + 'Roanoke city, VA': '770', + 'Rockbridge County, VA': '163', + 'Rockingham County, VA': '165', + 'Russell County, VA': '167', + 'Salem city, VA': '775', + 'Scott County, VA': '169', + 'Shenandoah County, VA': '171', + 'Smyth County, VA': '173', + 'Southampton County, VA': '175', + 'Spotsylvania County, VA': '177', + 'Stafford County, VA': '179', + 'Staunton city, VA': '790', + 'Suffolk city, VA': '800', + 'Surry County, VA': '181', + 'Sussex County, VA': '183', + 'Tazewell County, VA': '185', + 'Virginia Beach city, VA': '810', + 'Warren County, VA': '187', + 'Washington County, VA': '191', + 'Waynesboro city, VA': '820', + 'Westmoreland County, VA': '193', + 'Williamsburg city, VA': '830', + 'Winchester city, VA': '840', + 'Wise County, VA': '195', + 'Wythe County, VA': '197', + 'York County, VA': '199'}, + 53: {'--All--': '%', + 'Adams County, WA': '001', + 'Asotin County, WA': '003', + 'Benton County, WA': '005', + 'Chelan County, WA': '007', + 'Clallam County, WA': '009', + 'Clark County, WA': '011', + 'Columbia County, WA': '013', + 'Cowlitz County, WA': '015', + 'Douglas County, WA': '017', + 'Ferry County, WA': '019', + 'Franklin County, WA': '021', + 'Garfield County, WA': '023', + 'Grant County, WA': '025', + 'Grays Harbor County, WA': '027', + 'Island County, WA': '029', + 'Jefferson County, WA': '031', + 'King County, WA': '033', + 'Kitsap County, WA': '035', + 'Kittitas County, WA': '037', + 'Klickitat County, WA': '039', + 'Lewis County, WA': '041', + 'Lincoln County, WA': '043', + 'Mason County, WA': '045', + 'Okanogan County, WA': '047', + 'Pacific County, WA': '049', + 'Pend Oreille County, WA': '051', + 'Pierce County, WA': '053', + 'San Juan County, WA': '055', + 'Skagit County, WA': '057', + 'Skamania County, WA': '059', + 'Snohomish County, WA': '061', + 'Spokane County, WA': '063', + 'Stevens County, WA': '065', + 'Thurston County, WA': '067', + 'Wahkiakum County, WA': '069', + 'Walla Walla County, WA': '071', + 'Whatcom County, WA': '073', + 'Whitman County, WA': '075', + 'Yakima County, WA': '077'}, + 54: {'--All--': '%', + 'Barbour County, WV': '001', + 'Berkeley County, WV': '003', + 'Boone County, WV': '005', + 'Braxton County, WV': '007', + 'Brooke County, WV': '009', + 'Cabell County, WV': '011', + 'Calhoun County, WV': '013', + 'Clay County, WV': '015', + 'Doddridge County, WV': '017', + 'Fayette County, WV': '019', + 'Gilmer County, WV': '021', + 'Grant County, WV': '023', + 'Greenbrier County, WV': '025', + 'Hampshire County, WV': '027', + 'Hancock County, WV': '029', + 'Hardy County, WV': '031', + 'Harrison County, WV': '033', + 'Jackson County, WV': '035', + 'Jefferson County, WV': '037', + 'Kanawha County, WV': '039', + 'Lewis County, WV': '041', + 'Lincoln County, WV': '043', + 'Logan County, WV': '045', + 'Marion County, WV': '049', + 'Marshall County, WV': '051', + 'Mason County, WV': '053', + 'McDowell County, WV': '047', + 'Mercer County, WV': '055', + 'Mineral County, WV': '057', + 'Mingo County, WV': '059', + 'Monongalia County, WV': '061', + 'Monroe County, WV': '063', + 'Morgan County, WV': '065', + 'Nicholas County, WV': '067', + 'Ohio County, WV': '069', + 'Pendleton County, WV': '071', + 'Pleasants County, WV': '073', + 'Pocahontas County, WV': '075', + 'Preston County, WV': '077', + 'Putnam County, WV': '079', + 'Raleigh County, WV': '081', + 'Randolph County, WV': '083', + 'Ritchie County, WV': '085', + 'Roane County, WV': '087', + 'Summers County, WV': '089', + 'Taylor County, WV': '091', + 'Tucker County, WV': '093', + 'Tyler County, WV': '095', + 'Upshur County, WV': '097', + 'Wayne County, WV': '099', + 'Webster County, WV': '101', + 'Wetzel County, WV': '103', + 'Wirt County, WV': '105', + 'Wood County, WV': '107', + 'Wyoming County, WV': '109'}, + 55: {'--All--': '%', + 'Adams County, WI': '001', + 'Ashland County, WI': '003', + 'Barron County, WI': '005', + 'Bayfield County, WI': '007', + 'Brown County, WI': '009', + 'Buffalo County, WI': '011', + 'Burnett County, WI': '013', + 'Calumet County, WI': '015', + 'Chippewa County, WI': '017', + 'Clark County, WI': '019', + 'Columbia County, WI': '021', + 'Crawford County, WI': '023', + 'Dane County, WI': '025', + 'Dodge County, WI': '027', + 'Door County, WI': '029', + 'Douglas County, WI': '031', + 'Dunn County, WI': '033', + 'Eau Claire County, WI': '035', + 'Florence County, WI': '037', + 'Fond du Lac County, WI': '039', + 'Forest County, WI': '041', + 'Grant County, WI': '043', + 'Green County, WI': '045', + 'Green Lake County, WI': '047', + 'Iowa County, WI': '049', + 'Iron County, WI': '051', + 'Jackson County, WI': '053', + 'Jefferson County, WI': '055', + 'Juneau County, WI': '057', + 'Kenosha County, WI': '059', + 'Kewaunee County, WI': '061', + 'La Crosse County, WI': '063', + 'Lafayette County, WI': '065', + 'Langlade County, WI': '067', + 'Lincoln County, WI': '069', + 'Manitowoc County, WI': '071', + 'Marathon County, WI': '073', + 'Marinette County, WI': '075', + 'Marquette County, WI': '077', + 'Menominee County, WI': '078', + 'Milwaukee County, WI': '079', + 'Monroe County, WI': '081', + 'Oconto County, WI': '083', + 'Oneida County, WI': '085', + 'Outagamie County, WI': '087', + 'Ozaukee County, WI': '089', + 'Pepin County, WI': '091', + 'Pierce County, WI': '093', + 'Polk County, WI': '095', + 'Portage County, WI': '097', + 'Price County, WI': '099', + 'Racine County, WI': '101', + 'Richland County, WI': '103', + 'Rock County, WI': '105', + 'Rusk County, WI': '107', + 'Sauk County, WI': '111', + 'Sawyer County, WI': '113', + 'Shawano County, WI': '115', + 'Sheboygan County, WI': '117', + 'St. Croix County, WI': '109', + 'Taylor County, WI': '119', + 'Trempealeau County, WI': '121', + 'Vernon County, WI': '123', + 'Vilas County, WI': '125', + 'Walworth County, WI': '127', + 'Washburn County, WI': '129', + 'Washington County, WI': '131', + 'Waukesha County, WI': '133', + 'Waupaca County, WI': '135', + 'Waushara County, WI': '137', + 'Winnebago County, WI': '139', + 'Wood County, WI': '141'}, + 56: {'--All--': '%', + 'Albany County, WY': '001', + 'Big Horn County, WY': '003', + 'Campbell County, WY': '005', + 'Carbon County, WY': '007', + 'Converse County, WY': '009', + 'Crook County, WY': '011', + 'Fremont County, WY': '013', + 'Goshen County, WY': '015', + 'Hot Springs County, WY': '017', + 'Johnson County, WY': '019', + 'Laramie County, WY': '021', + 'Lincoln County, WY': '023', + 'Natrona County, WY': '025', + 'Niobrara County, WY': '027', + 'Park County, WY': '029', + 'Platte County, WY': '031', + 'Sheridan County, WY': '033', + 'Sublette County, WY': '035', + 'Sweetwater County, WY': '037', + 'Teton County, WY': '039', + 'Uinta County, WY': '041', + 'Washakie County, WY': '043', + 'Weston County, WY': '045'}, + 72: {'--All--': '%', + 'Adjuntas Municipio, PR': '001', + 'Aguada Municipio, PR': '003', + 'Aguadilla Municipio, PR': '005', + 'Aguas Buenas Municipio, PR': '007', + 'Aibonito Municipio, PR': '009', + 'Anasco Municipio, PR': '011', + 'Arecibo Municipio, PR': '013', + 'Arroyo Municipio, PR': '015', + 'Barceloneta Municipio, PR': '017', + 'Barranquitas Municipio, PR': '019', + 'Bayamon Municipio, PR': '021', + 'Cabo Rojo Municipio, PR': '023', + 'Caguas Municipio, PR': '025', + 'Camuy Municipio, PR': '027', + 'Canovanas Municipio, PR': '029', + 'Carolina Municipio, PR': '031', + 'Catano Municipio, PR': '033', + 'Cayey Municipio, PR': '035', + 'Ceiba Municipio, PR': '037', + 'Ciales Municipio, PR': '039', + 'Cidra Municipio, PR': '041', + 'Coamo Municipio, PR': '043', + 'Comerio Municipio, PR': '045', + 'Corozal Municipio, PR': '047', + 'Culebra Municipio, PR': '049', + 'Dorado Municipio, PR': '051', + 'Fajardo Municipio, PR': '053', + 'Florida Municipio, PR': '054', + 'Guanica Municipio, PR': '055', + 'Guayama Municipio, PR': '057', + 'Guayanilla Municipio, PR': '059', + 'Guaynabo Municipio, PR': '061', + 'Gurabo Municipio, PR': '063', + 'Hatillo Municipio, PR': '065', + 'Hormigueros Municipio, PR': '067', + 'Humacao Municipio, PR': '069', + 'Isabela Municipio, PR': '071', + 'Jayuya Municipio, PR': '073', + 'Juana Diaz Municipio, PR': '075', + 'Juncos Municipio, PR': '077', + 'Lajas Municipio, PR': '079', + 'Lares Municipio, PR': '081', + 'Las Marias Municipio, PR': '083', + 'Las Piedras Municipio, PR': '085', + 'Loiza Municipio, PR': '087', + 'Luquillo Municipio, PR': '089', + 'Manati Municipio, PR': '091', + 'Maricao Municipio, PR': '093', + 'Maunabo Municipio, PR': '095', + 'Mayaguez Municipio, PR': '097', + 'Moca Municipio, PR': '099', + 'Morovis Municipio, PR': '101', + 'Naguabo Municipio, PR': '103', + 'Naranjito Municipio, PR': '105', + 'Orocovis Municipio, PR': '107', + 'Patillas Municipio, PR': '109', + 'Penuelas Municipio, PR': '111', + 'Ponce Municipio, PR': '113', + 'Quebradillas Municipio, PR': '115', + 'Rincon Municipio, PR': '117', + 'Rio Grande Municipio, PR': '119', + 'Sabana Grande Municipio, PR': '121', + 'Salinas Municipio, PR': '123', + 'San German Municipio, PR': '125', + 'San Juan Municipio, PR': '127', + 'San Lorenzo Municipio, PR': '129', + 'San Sebastian Municipio, PR': '131', + 'Santa Isabel Municipio, PR': '133', + 'Toa Alta Municipio, PR': '135', + 'Toa Baja Municipio, PR': '137', + 'Trujillo Alto Municipio, PR': '139', + 'Utuado Municipio, PR': '141', + 'Vega Alta Municipio, PR': '143', + 'Vega Baja Municipio, PR': '145', + 'Vieques Municipio, PR': '147', + 'Villalba Municipio, PR': '149', + 'Yabucoa Municipio, PR': '151', + 'Yauco Municipio, PR': '153'}, + '01': {'Autauga County, AL': '001', + 'Baldwin County, AL': '003', + 'Barbour County, AL': '005', + 'Bibb County, AL': '007', + 'Blount County, AL': '009', + 'Bullock County, AL': '011', + 'Butler County, AL': '013', + 'Calhoun County, AL': '015', + 'Chambers County, AL': '017', + 'Cherokee County, AL': '019', + 'Chilton County, AL': '021', + 'Choctaw County, AL': '023', + 'Clarke County, AL': '025', + 'Clay County, AL': '027', + 'Cleburne County, AL': '029', + 'Coffee County, AL': '031', + 'Colbert County, AL': '033', + 'Conecuh County, AL': '035', + 'Coosa County, AL': '037', + 'Covington County, AL': '039', + 'Crenshaw County, AL': '041', + 'Cullman County, AL': '043', + 'Dale County, AL': '045', + 'Dallas County, AL': '047', + 'DeKalb County, AL': '049', + 'Elmore County, AL': '051', + 'Escambia County, AL': '053', + 'Etowah County, AL': '055', + 'Fayette County, AL': '057', + 'Franklin County, AL': '059', + 'Geneva County, AL': '061', + 'Greene County, AL': '063', + 'Hale County, AL': '065', + 'Henry County, AL': '067', + 'Houston County, AL': '069', + 'Jackson County, AL': '071', + 'Jefferson County, AL': '073', + 'Lamar County, AL': '075', + 'Lauderdale County, AL': '077', + 'Lawrence County, AL': '079', + 'Lee County, AL': '081', + 'Limestone County, AL': '083', + 'Lowndes County, AL': '085', + 'Macon County, AL': '087', + 'Madison County, AL': '089', + 'Marengo County, AL': '091', + 'Marion County, AL': '093', + 'Marshall County, AL': '095', + 'Mobile County, AL': '097', + 'Monroe County, AL': '099', + 'Montgomery County, AL': '101', + 'Morgan County, AL': '103', + 'Perry County, AL': '105', + 'Pickens County, AL': '107', + 'Pike County, AL': '109', + 'Randolph County, AL': '111', + 'Russell County, AL': '113', + 'Shelby County, AL': '117', + 'St. Clair County, AL': '115', + 'Sumter County, AL': '119', + 'Talladega County, AL': '121', + 'Tallapoosa County, AL': '123', + 'Tuscaloosa County, AL': '125', + 'Walker County, AL': '127', + 'Washington County, AL': '129', + 'Wilcox County, AL': '131', + 'Winston County, AL': '133'}, + '02': {'Aleutians East Borough, AK': '013', + 'Aleutians West Census Area, AK': '016', + 'Anchorage Borough/municipality, AK': '020', + 'Bethel Census Area, AK': '050', + 'Bristol Bay Borough, AK': '060', + 'Denali Borough, AK': '068', + 'Dillingham Census Area, AK': '070', + 'Fairbanks North Star Borough, AK': '090', + 'Haines Borough, AK': '100', + 'Juneau Borough/city, AK': '110', + 'Kenai Peninsula Borough, AK': '122', + 'Ketchikan Gateway Borough, AK': '130', + 'Kodiak Island Borough, AK': '150', + 'Lake and Peninsula Borough, AK': '164', + 'Matanuska-Susitna Borough, AK': '170', + 'Nome Census Area, AK': '180', + 'North Slope Borough, AK': '185', + 'Northwest Arctic Borough, AK': '188', + 'Prince of Wales-Outer Ketchikan Census Area, AK': '201', + 'Sitka Borough/city, AK': '220', + 'Skagway-Hoonah-Angoon Census Area, AK': '232', + 'Southeast Fairbanks Census Area, AK': '240', + 'Valdez-Cordova Census Area, AK': '261', + 'Wade Hampton Census Area, AK': '270', + 'Wrangell-Petersburg Census Area, AK': '280', + 'Yakutat Borough, AK': '282', + 'Yukon-Koyukuk Census Area, AK': '290'}, + '04': {'Apache County, AZ': '001', + 'Cochise County, AZ': '003', + 'Coconino County, AZ': '005', + 'Gila County, AZ': '007', + 'Graham County, AZ': '009', + 'Greenlee County, AZ': '011', + 'La Paz County, AZ': '012', + 'Maricopa County, AZ': '013', + 'Mohave County, AZ': '015', + 'Navajo County, AZ': '017', + 'Pima County, AZ': '019', + 'Pinal County, AZ': '021', + 'Santa Cruz County, AZ': '023', + 'Yavapai County, AZ': '025', + 'Yuma County, AZ': '027'}, + '05': {'Arkansas County, AR': '001', + 'Ashley County, AR': '003', + 'Baxter County, AR': '005', + 'Benton County, AR': '007', + 'Boone County, AR': '009', + 'Bradley County, AR': '011', + 'Calhoun County, AR': '013', + 'Carroll County, AR': '015', + 'Chicot County, AR': '017', + 'Clark County, AR': '019', + 'Clay County, AR': '021', + 'Cleburne County, AR': '023', + 'Cleveland County, AR': '025', + 'Columbia County, AR': '027', + 'Conway County, AR': '029', + 'Craighead County, AR': '031', + 'Crawford County, AR': '033', + 'Crittenden County, AR': '035', + 'Cross County, AR': '037', + 'Dallas County, AR': '039', + 'Desha County, AR': '041', + 'Drew County, AR': '043', + 'Faulkner County, AR': '045', + 'Franklin County, AR': '047', + 'Fulton County, AR': '049', + 'Garland County, AR': '051', + 'Grant County, AR': '053', + 'Greene County, AR': '055', + 'Hempstead County, AR': '057', + 'Hot Spring County, AR': '059', + 'Howard County, AR': '061', + 'Independence County, AR': '063', + 'Izard County, AR': '065', + 'Jackson County, AR': '067', + 'Jefferson County, AR': '069', + 'Johnson County, AR': '071', + 'Lafayette County, AR': '073', + 'Lawrence County, AR': '075', + 'Lee County, AR': '077', + 'Lincoln County, AR': '079', + 'Little River County, AR': '081', + 'Logan County, AR': '083', + 'Lonoke County, AR': '085', + 'Madison County, AR': '087', + 'Marion County, AR': '089', + 'Miller County, AR': '091', + 'Mississippi County, AR': '093', + 'Monroe County, AR': '095', + 'Montgomery County, AR': '097', + 'Nevada County, AR': '099', + 'Newton County, AR': '101', + 'Ouachita County, AR': '103', + 'Perry County, AR': '105', + 'Phillips County, AR': '107', + 'Pike County, AR': '109', + 'Poinsett County, AR': '111', + 'Polk County, AR': '113', + 'Pope County, AR': '115', + 'Prairie County, AR': '117', + 'Pulaski County, AR': '119', + 'Randolph County, AR': '121', + 'Saline County, AR': '125', + 'Scott County, AR': '127', + 'Searcy County, AR': '129', + 'Sebastian County, AR': '131', + 'Sevier County, AR': '133', + 'Sharp County, AR': '135', + 'St. Francis County, AR': '123', + 'Stone County, AR': '137', + 'Union County, AR': '139', + 'Van Buren County, AR': '141', + 'Washington County, AR': '143', + 'White County, AR': '145', + 'Woodruff County, AR': '147', + 'Yell County, AR': '149'}, + '06': {'Alameda County, CA': '001', + 'Alpine County, CA': '003', + 'Amador County, CA': '005', + 'Butte County, CA': '007', + 'Calaveras County, CA': '009', + 'Colusa County, CA': '011', + 'Contra Costa County, CA': '013', + 'Del Norte County, CA': '015', + 'El Dorado County, CA': '017', + 'Fresno County, CA': '019', + 'Glenn County, CA': '021', + 'Humboldt County, CA': '023', + 'Imperial County, CA': '025', + 'Inyo County, CA': '027', + 'Kern County, CA': '029', + 'Kings County, CA': '031', + 'Lake County, CA': '033', + 'Lassen County, CA': '035', + 'Los Angeles County, CA': '037', + 'Madera County, CA': '039', + 'Marin County, CA': '041', + 'Mariposa County, CA': '043', + 'Mendocino County, CA': '045', + 'Merced County, CA': '047', + 'Modoc County, CA': '049', + 'Mono County, CA': '051', + 'Monterey County, CA': '053', + 'Napa County, CA': '055', + 'Nevada County, CA': '057', + 'Orange County, CA': '059', + 'Placer County, CA': '061', + 'Plumas County, CA': '063', + 'Riverside County, CA': '065', + 'Sacramento County, CA': '067', + 'San Benito County, CA': '069', + 'San Bernardino County, CA': '071', + 'San Diego County, CA': '073', + 'San Francisco County/city, CA': '075', + 'San Joaquin County, CA': '077', + 'San Luis Obispo County, CA': '079', + 'San Mateo County, CA': '081', + 'Santa Barbara County, CA': '083', + 'Santa Clara County, CA': '085', + 'Santa Cruz County, CA': '087', + 'Shasta County, CA': '089', + 'Sierra County, CA': '091', + 'Siskiyou County, CA': '093', + 'Solano County, CA': '095', + 'Sonoma County, CA': '097', + 'Stanislaus County, CA': '099', + 'Sutter County, CA': '101', + 'Tehama County, CA': '103', + 'Trinity County, CA': '105', + 'Tulare County, CA': '107', + 'Tuolumne County, CA': '109', + 'Ventura County, CA': '111', + 'Yolo County, CA': '113', + 'Yuba County, CA': '115'}, + '08': {'Adams County, CO': '001', + 'Alamosa County, CO': '003', + 'Arapahoe County, CO': '005', + 'Archuleta County, CO': '007', + 'Baca County, CO': '009', + 'Bent County, CO': '011', + 'Boulder County, CO': '013', + 'Broomfield County/city, CO': '014', + 'Chaffee County, CO': '015', + 'Cheyenne County, CO': '017', + 'Clear Creek County, CO': '019', + 'Conejos County, CO': '021', + 'Costilla County, CO': '023', + 'Crowley County, CO': '025', + 'Custer County, CO': '027', + 'Delta County, CO': '029', + 'Denver County/city, CO': '031', + 'Dolores County, CO': '033', + 'Douglas County, CO': '035', + 'Eagle County, CO': '037', + 'El Paso County, CO': '041', + 'Elbert County, CO': '039', + 'Fremont County, CO': '043', + 'Garfield County, CO': '045', + 'Gilpin County, CO': '047', + 'Grand County, CO': '049', + 'Gunnison County, CO': '051', + 'Hinsdale County, CO': '053', + 'Huerfano County, CO': '055', + 'Jackson County, CO': '057', + 'Jefferson County, CO': '059', + 'Kiowa County, CO': '061', + 'Kit Carson County, CO': '063', + 'La Plata County, CO': '067', + 'Lake County, CO': '065', + 'Larimer County, CO': '069', + 'Las Animas County, CO': '071', + 'Lincoln County, CO': '073', + 'Logan County, CO': '075', + 'Mesa County, CO': '077', + 'Mineral County, CO': '079', + 'Moffat County, CO': '081', + 'Montezuma County, CO': '083', + 'Montrose County, CO': '085', + 'Morgan County, CO': '087', + 'Otero County, CO': '089', + 'Ouray County, CO': '091', + 'Park County, CO': '093', + 'Phillips County, CO': '095', + 'Pitkin County, CO': '097', + 'Prowers County, CO': '099', + 'Pueblo County, CO': '101', + 'Rio Blanco County, CO': '103', + 'Rio Grande County, CO': '105', + 'Routt County, CO': '107', + 'Saguache County, CO': '109', + 'San Juan County, CO': '111', + 'San Miguel County, CO': '113', + 'Sedgwick County, CO': '115', + 'Summit County, CO': '117', + 'Teller County, CO': '119', + 'Washington County, CO': '121', + 'Weld County, CO': '123', + 'Yuma County, CO': '125'}, + '09': {'Fairfield County, CT': '001', + 'Hartford County, CT': '003', + 'Litchfield County, CT': '005', + 'Middlesex County, CT': '007', + 'New Haven County, CT': '009', + 'New London County, CT': '011', + 'Tolland County, CT': '013', + 'Windham County, CT': '015'}, + '10': {'Kent County, DE': '001', + 'New Castle County, DE': '003', + 'Sussex County, DE': '005'}, + '11': {'District of Columbia': '001'}, + '12': {'Alachua County, FL': '001', + 'Baker County, FL': '003', + 'Bay County, FL': '005', + 'Bradford County, FL': '007', + 'Brevard County, FL': '009', + 'Broward County, FL': '011', + 'Calhoun County, FL': '013', + 'Charlotte County, FL': '015', + 'Citrus County, FL': '017', + 'Clay County, FL': '019', + 'Collier County, FL': '021', + 'Columbia County, FL': '023', + 'DeSoto County, FL': '027', + 'Dixie County, FL': '029', + 'Duval County, FL': '031', + 'Escambia County, FL': '033', + 'Flagler County, FL': '035', + 'Franklin County, FL': '037', + 'Gadsden County, FL': '039', + 'Gilchrist County, FL': '041', + 'Glades County, FL': '043', + 'Gulf County, FL': '045', + 'Hamilton County, FL': '047', + 'Hardee County, FL': '049', + 'Hendry County, FL': '051', + 'Hernando County, FL': '053', + 'Highlands County, FL': '055', + 'Hillsborough County, FL': '057', + 'Holmes County, FL': '059', + 'Indian River County, FL': '061', + 'Jackson County, FL': '063', + 'Jefferson County, FL': '065', + 'Lafayette County, FL': '067', + 'Lake County, FL': '069', + 'Lee County, FL': '071', + 'Leon County, FL': '073', + 'Levy County, FL': '075', + 'Liberty County, FL': '077', + 'Madison County, FL': '079', + 'Manatee County, FL': '081', + 'Marion County, FL': '083', + 'Martin County, FL': '085', + 'Miami-Dade County, FL': '086', + 'Monroe County, FL': '087', + 'Nassau County, FL': '089', + 'Okaloosa County, FL': '091', + 'Okeechobee County, FL': '093', + 'Orange County, FL': '095', + 'Osceola County, FL': '097', + 'Palm Beach County, FL': '099', + 'Pasco County, FL': '101', + 'Pinellas County, FL': '103', + 'Polk County, FL': '105', + 'Putnam County, FL': '107', + 'Santa Rosa County, FL': '113', + 'Sarasota County, FL': '115', + 'Seminole County, FL': '117', + 'St. Johns County, FL': '109', + 'St. Lucie County, FL': '111', + 'Sumter County, FL': '119', + 'Suwannee County, FL': '121', + 'Taylor County, FL': '123', + 'Union County, FL': '125', + 'Volusia County, FL': '127', + 'Wakulla County, FL': '129', + 'Walton County, FL': '131', + 'Washington County, FL': '133'}, + '13': {'Appling County, GA': '001', + 'Atkinson County, GA': '003', + 'Bacon County, GA': '005', + 'Baker County, GA': '007', + 'Baldwin County, GA': '009', + 'Banks County, GA': '011', + 'Barrow County, GA': '013', + 'Bartow County, GA': '015', + 'Ben Hill County, GA': '017', + 'Berrien County, GA': '019', + 'Bibb County, GA': '021', + 'Bleckley County, GA': '023', + 'Brantley County, GA': '025', + 'Brooks County, GA': '027', + 'Bryan County, GA': '029', + 'Bulloch County, GA': '031', + 'Burke County, GA': '033', + 'Butts County, GA': '035', + 'Calhoun County, GA': '037', + 'Camden County, GA': '039', + 'Candler County, GA': '043', + 'Carroll County, GA': '045', + 'Catoosa County, GA': '047', + 'Charlton County, GA': '049', + 'Chatham County, GA': '051', + 'Chattahoochee County, GA': '053', + 'Chattooga County, GA': '055', + 'Cherokee County, GA': '057', + 'Clarke County, GA': '059', + 'Clay County, GA': '061', + 'Clayton County, GA': '063', + 'Clinch County, GA': '065', + 'Cobb County, GA': '067', + 'Coffee County, GA': '069', + 'Colquitt County, GA': '071', + 'Columbia County, GA': '073', + 'Cook County, GA': '075', + 'Coweta County, GA': '077', + 'Crawford County, GA': '079', + 'Crisp County, GA': '081', + 'Dade County, GA': '083', + 'Dawson County, GA': '085', + 'DeKalb County, GA': '089', + 'Decatur County, GA': '087', + 'Dodge County, GA': '091', + 'Dooly County, GA': '093', + 'Dougherty County, GA': '095', + 'Douglas County, GA': '097', + 'Early County, GA': '099', + 'Echols County, GA': '101', + 'Effingham County, GA': '103', + 'Elbert County, GA': '105', + 'Emanuel County, GA': '107', + 'Evans County, GA': '109', + 'Fannin County, GA': '111', + 'Fayette County, GA': '113', + 'Floyd County, GA': '115', + 'Forsyth County, GA': '117', + 'Franklin County, GA': '119', + 'Fulton County, GA': '121', + 'Gilmer County, GA': '123', + 'Glascock County, GA': '125', + 'Glynn County, GA': '127', + 'Gordon County, GA': '129', + 'Grady County, GA': '131', + 'Greene County, GA': '133', + 'Gwinnett County, GA': '135', + 'Habersham County, GA': '137', + 'Hall County, GA': '139', + 'Hancock County, GA': '141', + 'Haralson County, GA': '143', + 'Harris County, GA': '145', + 'Hart County, GA': '147', + 'Heard County, GA': '149', + 'Henry County, GA': '151', + 'Houston County, GA': '153', + 'Irwin County, GA': '155', + 'Jackson County, GA': '157', + 'Jasper County, GA': '159', + 'Jeff Davis County, GA': '161', + 'Jefferson County, GA': '163', + 'Jenkins County, GA': '165', + 'Johnson County, GA': '167', + 'Jones County, GA': '169', + 'Lamar County, GA': '171', + 'Lanier County, GA': '173', + 'Laurens County, GA': '175', + 'Lee County, GA': '177', + 'Liberty County, GA': '179', + 'Lincoln County, GA': '181', + 'Long County, GA': '183', + 'Lowndes County, GA': '185', + 'Lumpkin County, GA': '187', + 'Macon County, GA': '193', + 'Madison County, GA': '195', + 'Marion County, GA': '197', + 'McDuffie County, GA': '189', + 'McIntosh County, GA': '191', + 'Meriwether County, GA': '199', + 'Miller County, GA': '201', + 'Mitchell County, GA': '205', + 'Monroe County, GA': '207', + 'Montgomery County, GA': '209', + 'Morgan County, GA': '211', + 'Murray County, GA': '213', + 'Muscogee County, GA': '215', + 'Newton County, GA': '217', + 'Oconee County, GA': '219', + 'Oglethorpe County, GA': '221', + 'Paulding County, GA': '223', + 'Peach County, GA': '225', + 'Pickens County, GA': '227', + 'Pierce County, GA': '229', + 'Pike County, GA': '231', + 'Polk County, GA': '233', + 'Pulaski County, GA': '235', + 'Putnam County, GA': '237', + 'Quitman County, GA': '239', + 'Rabun County, GA': '241', + 'Randolph County, GA': '243', + 'Richmond County, GA': '245', + 'Rockdale County, GA': '247', + 'Schley County, GA': '249', + 'Screven County, GA': '251', + 'Seminole County, GA': '253', + 'Spalding County, GA': '255', + 'Stephens County, GA': '257', + 'Stewart County, GA': '259', + 'Sumter County, GA': '261', + 'Talbot County, GA': '263', + 'Taliaferro County, GA': '265', + 'Tattnall County, GA': '267', + 'Taylor County, GA': '269', + 'Telfair County, GA': '271', + 'Terrell County, GA': '273', + 'Thomas County, GA': '275', + 'Tift County, GA': '277', + 'Toombs County, GA': '279', + 'Towns County, GA': '281', + 'Treutlen County, GA': '283', + 'Troup County, GA': '285', + 'Turner County, GA': '287', + 'Twiggs County, GA': '289', + 'Union County, GA': '291', + 'Upson County, GA': '293', + 'Walker County, GA': '295', + 'Walton County, GA': '297', + 'Ware County, GA': '299', + 'Warren County, GA': '301', + 'Washington County, GA': '303', + 'Wayne County, GA': '305', + 'Webster County, GA': '307', + 'Wheeler County, GA': '309', + 'White County, GA': '311', + 'Whitfield County, GA': '313', + 'Wilcox County, GA': '315', + 'Wilkes County, GA': '317', + 'Wilkinson County, GA': '319', + 'Worth County, GA': '321'}, + '15': {'Hawaii County, HI': '001', + 'Honolulu County/city, HI': '003', + 'Kauai County, HI': '007', + 'Maui County, HI': '009'}, + '16': {'Ada County, ID': '001', + 'Adams County, ID': '003', + 'Bannock County, ID': '005', + 'Bear Lake County, ID': '007', + 'Benewah County, ID': '009', + 'Bingham County, ID': '011', + 'Blaine County, ID': '013', + 'Boise County, ID': '015', + 'Bonner County, ID': '017', + 'Bonneville County, ID': '019', + 'Boundary County, ID': '021', + 'Butte County, ID': '023', + 'Camas County, ID': '025', + 'Canyon County, ID': '027', + 'Caribou County, ID': '029', + 'Cassia County, ID': '031', + 'Clark County, ID': '033', + 'Clearwater County, ID': '035', + 'Custer County, ID': '037', + 'Elmore County, ID': '039', + 'Franklin County, ID': '041', + 'Fremont County, ID': '043', + 'Gem County, ID': '045', + 'Gooding County, ID': '047', + 'Idaho County, ID': '049', + 'Jefferson County, ID': '051', + 'Jerome County, ID': '053', + 'Kootenai County, ID': '055', + 'Latah County, ID': '057', + 'Lemhi County, ID': '059', + 'Lewis County, ID': '061', + 'Lincoln County, ID': '063', + 'Madison County, ID': '065', + 'Minidoka County, ID': '067', + 'Nez Perce County, ID': '069', + 'Oneida County, ID': '071', + 'Owyhee County, ID': '073', + 'Payette County, ID': '075', + 'Power County, ID': '077', + 'Shoshone County, ID': '079', + 'Teton County, ID': '081', + 'Twin Falls County, ID': '083', + 'Valley County, ID': '085', + 'Washington County, ID': '087'}, + '17': {'Adams County, IL': '001', + 'Alexander County, IL': '003', + 'Bond County, IL': '005', + 'Boone County, IL': '007', + 'Brown County, IL': '009', + 'Bureau County, IL': '011', + 'Calhoun County, IL': '013', + 'Carroll County, IL': '015', + 'Cass County, IL': '017', + 'Champaign County, IL': '019', + 'Christian County, IL': '021', + 'Clark County, IL': '023', + 'Clay County, IL': '025', + 'Clinton County, IL': '027', + 'Coles County, IL': '029', + 'Cook County, IL': '031', + 'Crawford County, IL': '033', + 'Cumberland County, IL': '035', + 'De Witt County, IL': '039', + 'DeKalb County, IL': '037', + 'Douglas County, IL': '041', + 'DuPage County, IL': '043', + 'Edgar County, IL': '045', + 'Edwards County, IL': '047', + 'Effingham County, IL': '049', + 'Fayette County, IL': '051', + 'Ford County, IL': '053', + 'Franklin County, IL': '055', + 'Fulton County, IL': '057', + 'Gallatin County, IL': '059', + 'Greene County, IL': '061', + 'Grundy County, IL': '063', + 'Hamilton County, IL': '065', + 'Hancock County, IL': '067', + 'Hardin County, IL': '069', + 'Henderson County, IL': '071', + 'Henry County, IL': '073', + 'Iroquois County, IL': '075', + 'Jackson County, IL': '077', + 'Jasper County, IL': '079', + 'Jefferson County, IL': '081', + 'Jersey County, IL': '083', + 'Jo Daviess County, IL': '085', + 'Johnson County, IL': '087', + 'Kane County, IL': '089', + 'Kankakee County, IL': '091', + 'Kendall County, IL': '093', + 'Knox County, IL': '095', + 'La Salle County, IL': '099', + 'Lake County, IL': '097', + 'Lawrence County, IL': '101', + 'Lee County, IL': '103', + 'Livingston County, IL': '105', + 'Logan County, IL': '107', + 'Macon County, IL': '115', + 'Macoupin County, IL': '117', + 'Madison County, IL': '119', + 'Marion County, IL': '121', + 'Marshall County, IL': '123', + 'Mason County, IL': '125', + 'Massac County, IL': '127', + 'McDonough County, IL': '109', + 'McHenry County, IL': '111', + 'McLean County, IL': '113', + 'Menard County, IL': '129', + 'Mercer County, IL': '131', + 'Monroe County, IL': '133', + 'Montgomery County, IL': '135', + 'Morgan County, IL': '137', + 'Moultrie County, IL': '139', + 'Ogle County, IL': '141', + 'Peoria County, IL': '143', + 'Perry County, IL': '145', + 'Piatt County, IL': '147', + 'Pike County, IL': '149', + 'Pope County, IL': '151', + 'Pulaski County, IL': '153', + 'Putnam County, IL': '155', + 'Randolph County, IL': '157', + 'Richland County, IL': '159', + 'Rock Island County, IL': '161', + 'Saline County, IL': '165', + 'Sangamon County, IL': '167', + 'Schuyler County, IL': '169', + 'Scott County, IL': '171', + 'Shelby County, IL': '173', + 'St. Clair County, IL': '163', + 'Stark County, IL': '175', + 'Stephenson County, IL': '177', + 'Tazewell County, IL': '179', + 'Union County, IL': '181', + 'Vermilion County, IL': '183', + 'Wabash County, IL': '185', + 'Warren County, IL': '187', + 'Washington County, IL': '189', + 'Wayne County, IL': '191', + 'White County, IL': '193', + 'Whiteside County, IL': '195', + 'Will County, IL': '197', + 'Williamson County, IL': '199', + 'Winnebago County, IL': '201', + 'Woodford County, IL': '203'}, + '18': {'Adams County, IN': '001', + 'Allen County, IN': '003', + 'Bartholomew County, IN': '005', + 'Benton County, IN': '007', + 'Blackford County, IN': '009', + 'Boone County, IN': '011', + 'Brown County, IN': '013', + 'Carroll County, IN': '015', + 'Cass County, IN': '017', + 'Clark County, IN': '019', + 'Clay County, IN': '021', + 'Clinton County, IN': '023', + 'Crawford County, IN': '025', + 'Daviess County, IN': '027', + 'DeKalb County, IN': '033', + 'Dearborn County, IN': '029', + 'Decatur County, IN': '031', + 'Delaware County, IN': '035', + 'Dubois County, IN': '037', + 'Elkhart County, IN': '039', + 'Fayette County, IN': '041', + 'Floyd County, IN': '043', + 'Fountain County, IN': '045', + 'Franklin County, IN': '047', + 'Fulton County, IN': '049', + 'Gibson County, IN': '051', + 'Grant County, IN': '053', + 'Greene County, IN': '055', + 'Hamilton County, IN': '057', + 'Hancock County, IN': '059', + 'Harrison County, IN': '061', + 'Hendricks County, IN': '063', + 'Henry County, IN': '065', + 'Howard County, IN': '067', + 'Huntington County, IN': '069', + 'Jackson County, IN': '071', + 'Jasper County, IN': '073', + 'Jay County, IN': '075', + 'Jefferson County, IN': '077', + 'Jennings County, IN': '079', + 'Johnson County, IN': '081', + 'Knox County, IN': '083', + 'Kosciusko County, IN': '085', + 'LaGrange County, IN': '087', + 'LaPorte County, IN': '091', + 'Lake County, IN': '089', + 'Lawrence County, IN': '093', + 'Madison County, IN': '095', + 'Marion County, IN': '097', + 'Marshall County, IN': '099', + 'Martin County, IN': '101', + 'Miami County, IN': '103', + 'Monroe County, IN': '105', + 'Montgomery County, IN': '107', + 'Morgan County, IN': '109', + 'Newton County, IN': '111', + 'Noble County, IN': '113', + 'Ohio County, IN': '115', + 'Orange County, IN': '117', + 'Owen County, IN': '119', + 'Parke County, IN': '121', + 'Perry County, IN': '123', + 'Pike County, IN': '125', + 'Porter County, IN': '127', + 'Posey County, IN': '129', + 'Pulaski County, IN': '131', + 'Putnam County, IN': '133', + 'Randolph County, IN': '135', + 'Ripley County, IN': '137', + 'Rush County, IN': '139', + 'Scott County, IN': '143', + 'Shelby County, IN': '145', + 'Spencer County, IN': '147', + 'St. Joseph County, IN': '141', + 'Starke County, IN': '149', + 'Steuben County, IN': '151', + 'Sullivan County, IN': '153', + 'Switzerland County, IN': '155', + 'Tippecanoe County, IN': '157', + 'Tipton County, IN': '159', + 'Union County, IN': '161', + 'Vanderburgh County, IN': '163', + 'Vermillion County, IN': '165', + 'Vigo County, IN': '167', + 'Wabash County, IN': '169', + 'Warren County, IN': '171', + 'Warrick County, IN': '173', + 'Washington County, IN': '175', + 'Wayne County, IN': '177', + 'Wells County, IN': '179', + 'White County, IN': '181', + 'Whitley County, IN': '183'}, + '19': {'Adair County, IA': '001', + 'Adams County, IA': '003', + 'Allamakee County, IA': '005', + 'Appanoose County, IA': '007', + 'Audubon County, IA': '009', + 'Benton County, IA': '011', + 'Black Hawk County, IA': '013', + 'Boone County, IA': '015', + 'Bremer County, IA': '017', + 'Buchanan County, IA': '019', + 'Buena Vista County, IA': '021', + 'Butler County, IA': '023', + 'Calhoun County, IA': '025', + 'Carroll County, IA': '027', + 'Cass County, IA': '029', + 'Cedar County, IA': '031', + 'Cerro Gordo County, IA': '033', + 'Cherokee County, IA': '035', + 'Chickasaw County, IA': '037', + 'Clarke County, IA': '039', + 'Clay County, IA': '041', + 'Clayton County, IA': '043', + 'Clinton County, IA': '045', + 'Crawford County, IA': '047', + 'Dallas County, IA': '049', + 'Davis County, IA': '051', + 'Decatur County, IA': '053', + 'Delaware County, IA': '055', + 'Des Moines County, IA': '057', + 'Dickinson County, IA': '059', + 'Dubuque County, IA': '061', + 'Emmet County, IA': '063', + 'Fayette County, IA': '065', + 'Floyd County, IA': '067', + 'Franklin County, IA': '069', + 'Fremont County, IA': '071', + 'Greene County, IA': '073', + 'Grundy County, IA': '075', + 'Guthrie County, IA': '077', + 'Hamilton County, IA': '079', + 'Hancock County, IA': '081', + 'Hardin County, IA': '083', + 'Harrison County, IA': '085', + 'Henry County, IA': '087', + 'Howard County, IA': '089', + 'Humboldt County, IA': '091', + 'Ida County, IA': '093', + 'Iowa County, IA': '095', + 'Jackson County, IA': '097', + 'Jasper County, IA': '099', + 'Jefferson County, IA': '101', + 'Johnson County, IA': '103', + 'Jones County, IA': '105', + 'Keokuk County, IA': '107', + 'Kossuth County, IA': '109', + 'Lee County, IA': '111', + 'Linn County, IA': '113', + 'Louisa County, IA': '115', + 'Lucas County, IA': '117', + 'Lyon County, IA': '119', + 'Madison County, IA': '121', + 'Mahaska County, IA': '123', + 'Marion County, IA': '125', + 'Marshall County, IA': '127', + 'Mills County, IA': '129', + 'Mitchell County, IA': '131', + 'Monona County, IA': '133', + 'Monroe County, IA': '135', + 'Montgomery County, IA': '137', + 'Muscatine County, IA': '139', + "O'Brien County, IA": '141', + 'Osceola County, IA': '143', + 'Page County, IA': '145', + 'Palo Alto County, IA': '147', + 'Plymouth County, IA': '149', + 'Pocahontas County, IA': '151', + 'Polk County, IA': '153', + 'Pottawattamie County, IA': '155', + 'Poweshiek County, IA': '157', + 'Ringgold County, IA': '159', + 'Sac County, IA': '161', + 'Scott County, IA': '163', + 'Shelby County, IA': '165', + 'Sioux County, IA': '167', + 'Story County, IA': '169', + 'Tama County, IA': '171', + 'Taylor County, IA': '173', + 'Union County, IA': '175', + 'Van Buren County, IA': '177', + 'Wapello County, IA': '179', + 'Warren County, IA': '181', + 'Washington County, IA': '183', + 'Wayne County, IA': '185', + 'Webster County, IA': '187', + 'Winnebago County, IA': '189', + 'Winneshiek County, IA': '191', + 'Woodbury County, IA': '193', + 'Worth County, IA': '195', + 'Wright County, IA': '197'}, + '20': {'Allen County, KS': '001', + 'Anderson County, KS': '003', + 'Atchison County, KS': '005', + 'Barber County, KS': '007', + 'Barton County, KS': '009', + 'Bourbon County, KS': '011', + 'Brown County, KS': '013', + 'Butler County, KS': '015', + 'Chase County, KS': '017', + 'Chautauqua County, KS': '019', + 'Cherokee County, KS': '021', + 'Cheyenne County, KS': '023', + 'Clark County, KS': '025', + 'Clay County, KS': '027', + 'Cloud County, KS': '029', + 'Coffey County, KS': '031', + 'Comanche County, KS': '033', + 'Cowley County, KS': '035', + 'Crawford County, KS': '037', + 'Decatur County, KS': '039', + 'Dickinson County, KS': '041', + 'Doniphan County, KS': '043', + 'Douglas County, KS': '045', + 'Edwards County, KS': '047', + 'Elk County, KS': '049', + 'Ellis County, KS': '051', + 'Ellsworth County, KS': '053', + 'Finney County, KS': '055', + 'Ford County, KS': '057', + 'Franklin County, KS': '059', + 'Geary County, KS': '061', + 'Gove County, KS': '063', + 'Graham County, KS': '065', + 'Grant County, KS': '067', + 'Gray County, KS': '069', + 'Greeley County, KS': '071', + 'Greenwood County, KS': '073', + 'Hamilton County, KS': '075', + 'Harper County, KS': '077', + 'Harvey County, KS': '079', + 'Haskell County, KS': '081', + 'Hodgeman County, KS': '083', + 'Jackson County, KS': '085', + 'Jefferson County, KS': '087', + 'Jewell County, KS': '089', + 'Johnson County, KS': '091', + 'Kearny County, KS': '093', + 'Kingman County, KS': '095', + 'Kiowa County, KS': '097', + 'Labette County, KS': '099', + 'Lane County, KS': '101', + 'Leavenworth County, KS': '103', + 'Lincoln County, KS': '105', + 'Linn County, KS': '107', + 'Logan County, KS': '109', + 'Lyon County, KS': '111', + 'Marion County, KS': '115', + 'Marshall County, KS': '117', + 'McPherson County, KS': '113', + 'Meade County, KS': '119', + 'Miami County, KS': '121', + 'Mitchell County, KS': '123', + 'Montgomery County, KS': '125', + 'Morris County, KS': '127', + 'Morton County, KS': '129', + 'Nemaha County, KS': '131', + 'Neosho County, KS': '133', + 'Ness County, KS': '135', + 'Norton County, KS': '137', + 'Osage County, KS': '139', + 'Osborne County, KS': '141', + 'Ottawa County, KS': '143', + 'Pawnee County, KS': '145', + 'Phillips County, KS': '147', + 'Pottawatomie County, KS': '149', + 'Pratt County, KS': '151', + 'Rawlins County, KS': '153', + 'Reno County, KS': '155', + 'Republic County, KS': '157', + 'Rice County, KS': '159', + 'Riley County, KS': '161', + 'Rooks County, KS': '163', + 'Rush County, KS': '165', + 'Russell County, KS': '167', + 'Saline County, KS': '169', + 'Scott County, KS': '171', + 'Sedgwick County, KS': '173', + 'Seward County, KS': '175', + 'Shawnee County, KS': '177', + 'Sheridan County, KS': '179', + 'Sherman County, KS': '181', + 'Smith County, KS': '183', + 'Stafford County, KS': '185', + 'Stanton County, KS': '187', + 'Stevens County, KS': '189', + 'Sumner County, KS': '191', + 'Thomas County, KS': '193', + 'Trego County, KS': '195', + 'Wabaunsee County, KS': '197', + 'Wallace County, KS': '199', + 'Washington County, KS': '201', + 'Wichita County, KS': '203', + 'Wilson County, KS': '205', + 'Woodson County, KS': '207', + 'Wyandotte County, KS': '209'}, + '21': {'Adair County, KY': '001', + 'Allen County, KY': '003', + 'Anderson County, KY': '005', + 'Ballard County, KY': '007', + 'Barren County, KY': '009', + 'Bath County, KY': '011', + 'Bell County, KY': '013', + 'Boone County, KY': '015', + 'Bourbon County, KY': '017', + 'Boyd County, KY': '019', + 'Boyle County, KY': '021', + 'Bracken County, KY': '023', + 'Breathitt County, KY': '025', + 'Breckinridge County, KY': '027', + 'Bullitt County, KY': '029', + 'Butler County, KY': '031', + 'Caldwell County, KY': '033', + 'Calloway County, KY': '035', + 'Campbell County, KY': '037', + 'Carlisle County, KY': '039', + 'Carroll County, KY': '041', + 'Carter County, KY': '043', + 'Casey County, KY': '045', + 'Christian County, KY': '047', + 'Clark County, KY': '049', + 'Clay County, KY': '051', + 'Clinton County, KY': '053', + 'Crittenden County, KY': '055', + 'Cumberland County, KY': '057', + 'Daviess County, KY': '059', + 'Edmonson County, KY': '061', + 'Elliott County, KY': '063', + 'Estill County, KY': '065', + 'Fayette County, KY': '067', + 'Fleming County, KY': '069', + 'Floyd County, KY': '071', + 'Franklin County, KY': '073', + 'Fulton County, KY': '075', + 'Gallatin County, KY': '077', + 'Garrard County, KY': '079', + 'Grant County, KY': '081', + 'Graves County, KY': '083', + 'Grayson County, KY': '085', + 'Green County, KY': '087', + 'Greenup County, KY': '089', + 'Hancock County, KY': '091', + 'Hardin County, KY': '093', + 'Harlan County, KY': '095', + 'Harrison County, KY': '097', + 'Hart County, KY': '099', + 'Henderson County, KY': '101', + 'Henry County, KY': '103', + 'Hickman County, KY': '105', + 'Hopkins County, KY': '107', + 'Jackson County, KY': '109', + 'Jefferson County, KY': '111', + 'Jessamine County, KY': '113', + 'Johnson County, KY': '115', + 'Kenton County, KY': '117', + 'Knott County, KY': '119', + 'Knox County, KY': '121', + 'Larue County, KY': '123', + 'Laurel County, KY': '125', + 'Lawrence County, KY': '127', + 'Lee County, KY': '129', + 'Leslie County, KY': '131', + 'Letcher County, KY': '133', + 'Lewis County, KY': '135', + 'Lincoln County, KY': '137', + 'Livingston County, KY': '139', + 'Logan County, KY': '141', + 'Lyon County, KY': '143', + 'Madison County, KY': '151', + 'Magoffin County, KY': '153', + 'Marion County, KY': '155', + 'Marshall County, KY': '157', + 'Martin County, KY': '159', + 'Mason County, KY': '161', + 'McCracken County, KY': '145', + 'McCreary County, KY': '147', + 'McLean County, KY': '149', + 'Meade County, KY': '163', + 'Menifee County, KY': '165', + 'Mercer County, KY': '167', + 'Metcalfe County, KY': '169', + 'Monroe County, KY': '171', + 'Montgomery County, KY': '173', + 'Morgan County, KY': '175', + 'Muhlenberg County, KY': '177', + 'Nelson County, KY': '179', + 'Nicholas County, KY': '181', + 'Ohio County, KY': '183', + 'Oldham County, KY': '185', + 'Owen County, KY': '187', + 'Owsley County, KY': '189', + 'Pendleton County, KY': '191', + 'Perry County, KY': '193', + 'Pike County, KY': '195', + 'Powell County, KY': '197', + 'Pulaski County, KY': '199', + 'Robertson County, KY': '201', + 'Rockcastle County, KY': '203', + 'Rowan County, KY': '205', + 'Russell County, KY': '207', + 'Scott County, KY': '209', + 'Shelby County, KY': '211', + 'Simpson County, KY': '213', + 'Spencer County, KY': '215', + 'Taylor County, KY': '217', + 'Todd County, KY': '219', + 'Trigg County, KY': '221', + 'Trimble County, KY': '223', + 'Union County, KY': '225', + 'Warren County, KY': '227', + 'Washington County, KY': '229', + 'Wayne County, KY': '231', + 'Webster County, KY': '233', + 'Whitley County, KY': '235', + 'Wolfe County, KY': '237', + 'Woodford County, KY': '239'}, + '22': {'Acadia Parish, LA': '001', + 'Allen Parish, LA': '003', + 'Ascension Parish, LA': '005', + 'Assumption Parish, LA': '007', + 'Avoyelles Parish, LA': '009', + 'Beauregard Parish, LA': '011', + 'Bienville Parish, LA': '013', + 'Bossier Parish, LA': '015', + 'Caddo Parish, LA': '017', + 'Calcasieu Parish, LA': '019', + 'Caldwell Parish, LA': '021', + 'Cameron Parish, LA': '023', + 'Catahoula Parish, LA': '025', + 'Claiborne Parish, LA': '027', + 'Concordia Parish, LA': '029', + 'De Soto Parish, LA': '031', + 'East Baton Rouge Parish, LA': '033', + 'East Carroll Parish, LA': '035', + 'East Feliciana Parish, LA': '037', + 'Evangeline Parish, LA': '039', + 'Franklin Parish, LA': '041', + 'Grant Parish, LA': '043', + 'Iberia Parish, LA': '045', + 'Iberville Parish, LA': '047', + 'Jackson Parish, LA': '049', + 'Jefferson Davis Parish, LA': '053', + 'Jefferson Parish, LA': '051', + 'La Salle Parish, LA': '059', + 'Lafayette Parish, LA': '055', + 'Lafourche Parish, LA': '057', + 'Lincoln Parish, LA': '061', + 'Livingston Parish, LA': '063', + 'Madison Parish, LA': '065', + 'Morehouse Parish, LA': '067', + 'Natchitoches Parish, LA': '069', + 'Orleans Parish, LA': '071', + 'Ouachita Parish, LA': '073', + 'Plaquemines Parish, LA': '075', + 'Pointe Coupee Parish, LA': '077', + 'Rapides Parish, LA': '079', + 'Red River Parish, LA': '081', + 'Richland Parish, LA': '083', + 'Sabine Parish, LA': '085', + 'St. Bernard Parish, LA': '087', + 'St. Charles Parish, LA': '089', + 'St. Helena Parish, LA': '091', + 'St. James Parish, LA': '093', + 'St. John the Baptist Parish, LA': '095', + 'St. Landry Parish, LA': '097', + 'St. Martin Parish, LA': '099', + 'St. Mary Parish, LA': '101', + 'St. Tammany Parish, LA': '103', + 'Tangipahoa Parish, LA': '105', + 'Tensas Parish, LA': '107', + 'Terrebonne Parish, LA': '109', + 'Union Parish, LA': '111', + 'Vermilion Parish, LA': '113', + 'Vernon Parish, LA': '115', + 'Washington Parish, LA': '117', + 'Webster Parish, LA': '119', + 'West Baton Rouge Parish, LA': '121', + 'West Carroll Parish, LA': '123', + 'West Feliciana Parish, LA': '125', + 'Winn Parish, LA': '127'}, + '23': {'Androscoggin County, ME': '001', + 'Aroostook County, ME': '003', + 'Cumberland County, ME': '005', + 'Franklin County, ME': '007', + 'Hancock County, ME': '009', + 'Kennebec County, ME': '011', + 'Knox County, ME': '013', + 'Lincoln County, ME': '015', + 'Oxford County, ME': '017', + 'Penobscot County, ME': '019', + 'Piscataquis County, ME': '021', + 'Sagadahoc County, ME': '023', + 'Somerset County, ME': '025', + 'Waldo County, ME': '027', + 'Washington County, ME': '029', + 'York County, ME': '031'}, + '24': {'Allegany County, MD': '001', + 'Anne Arundel County, MD': '003', + 'Baltimore County, MD': '005', + 'Baltimore city, MD': '510', + 'Calvert County, MD': '009', + 'Caroline County, MD': '011', + 'Carroll County, MD': '013', + 'Cecil County, MD': '015', + 'Charles County, MD': '017', + 'Dorchester County, MD': '019', + 'Frederick County, MD': '021', + 'Garrett County, MD': '023', + 'Harford County, MD': '025', + 'Howard County, MD': '027', + 'Kent County, MD': '029', + 'Montgomery County, MD': '031', + "Prince George's County, MD": '033', + "Queen Anne's County, MD": '035', + 'Somerset County, MD': '039', + "St. Mary's County, MD": '037', + 'Talbot County, MD': '041', + 'Washington County, MD': '043', + 'Wicomico County, MD': '045', + 'Worcester County, MD': '047'}, + '25': {'Barnstable County, MA': '001', + 'Berkshire County, MA': '003', + 'Bristol County, MA': '005', + 'Dukes County, MA': '007', + 'Essex County, MA': '009', + 'Franklin County, MA': '011', + 'Hampden County, MA': '013', + 'Hampshire County, MA': '015', + 'Middlesex County, MA': '017', + 'Nantucket County/town, MA': '019', + 'Norfolk County, MA': '021', + 'Plymouth County, MA': '023', + 'Suffolk County, MA': '025', + 'Worcester County, MA': '027'}, + '26': {'Alcona County, MI': '001', + 'Alger County, MI': '003', + 'Allegan County, MI': '005', + 'Alpena County, MI': '007', + 'Antrim County, MI': '009', + 'Arenac County, MI': '011', + 'Baraga County, MI': '013', + 'Barry County, MI': '015', + 'Bay County, MI': '017', + 'Benzie County, MI': '019', + 'Berrien County, MI': '021', + 'Branch County, MI': '023', + 'Calhoun County, MI': '025', + 'Cass County, MI': '027', + 'Charlevoix County, MI': '029', + 'Cheboygan County, MI': '031', + 'Chippewa County, MI': '033', + 'Clare County, MI': '035', + 'Clinton County, MI': '037', + 'Crawford County, MI': '039', + 'Delta County, MI': '041', + 'Dickinson County, MI': '043', + 'Eaton County, MI': '045', + 'Emmet County, MI': '047', + 'Genesee County, MI': '049', + 'Gladwin County, MI': '051', + 'Gogebic County, MI': '053', + 'Grand Traverse County, MI': '055', + 'Gratiot County, MI': '057', + 'Hillsdale County, MI': '059', + 'Houghton County, MI': '061', + 'Huron County, MI': '063', + 'Ingham County, MI': '065', + 'Ionia County, MI': '067', + 'Iosco County, MI': '069', + 'Iron County, MI': '071', + 'Isabella County, MI': '073', + 'Jackson County, MI': '075', + 'Kalamazoo County, MI': '077', + 'Kalkaska County, MI': '079', + 'Kent County, MI': '081', + 'Keweenaw County, MI': '083', + 'Lake County, MI': '085', + 'Lapeer County, MI': '087', + 'Leelanau County, MI': '089', + 'Lenawee County, MI': '091', + 'Livingston County, MI': '093', + 'Luce County, MI': '095', + 'Mackinac County, MI': '097', + 'Macomb County, MI': '099', + 'Manistee County, MI': '101', + 'Marquette County, MI': '103', + 'Mason County, MI': '105', + 'Mecosta County, MI': '107', + 'Menominee County, MI': '109', + 'Midland County, MI': '111', + 'Missaukee County, MI': '113', + 'Monroe County, MI': '115', + 'Montcalm County, MI': '117', + 'Montmorency County, MI': '119', + 'Muskegon County, MI': '121', + 'Newaygo County, MI': '123', + 'Oakland County, MI': '125', + 'Oceana County, MI': '127', + 'Ogemaw County, MI': '129', + 'Ontonagon County, MI': '131', + 'Osceola County, MI': '133', + 'Oscoda County, MI': '135', + 'Otsego County, MI': '137', + 'Ottawa County, MI': '139', + 'Presque Isle County, MI': '141', + 'Roscommon County, MI': '143', + 'Saginaw County, MI': '145', + 'Sanilac County, MI': '151', + 'Schoolcraft County, MI': '153', + 'Shiawassee County, MI': '155', + 'St. Clair County, MI': '147', + 'St. Joseph County, MI': '149', + 'Tuscola County, MI': '157', + 'Van Buren County, MI': '159', + 'Washtenaw County, MI': '161', + 'Wayne County, MI': '163', + 'Wexford County, MI': '165'}, + '27': {'Aitkin County, MN': '001', + 'Anoka County, MN': '003', + 'Becker County, MN': '005', + 'Beltrami County, MN': '007', + 'Benton County, MN': '009', + 'Big Stone County, MN': '011', + 'Blue Earth County, MN': '013', + 'Brown County, MN': '015', + 'Carlton County, MN': '017', + 'Carver County, MN': '019', + 'Cass County, MN': '021', + 'Chippewa County, MN': '023', + 'Chisago County, MN': '025', + 'Clay County, MN': '027', + 'Clearwater County, MN': '029', + 'Cook County, MN': '031', + 'Cottonwood County, MN': '033', + 'Crow Wing County, MN': '035', + 'Dakota County, MN': '037', + 'Dodge County, MN': '039', + 'Douglas County, MN': '041', + 'Faribault County, MN': '043', + 'Fillmore County, MN': '045', + 'Freeborn County, MN': '047', + 'Goodhue County, MN': '049', + 'Grant County, MN': '051', + 'Hennepin County, MN': '053', + 'Houston County, MN': '055', + 'Hubbard County, MN': '057', + 'Isanti County, MN': '059', + 'Itasca County, MN': '061', + 'Jackson County, MN': '063', + 'Kanabec County, MN': '065', + 'Kandiyohi County, MN': '067', + 'Kittson County, MN': '069', + 'Koochiching County, MN': '071', + 'Lac qui Parle County, MN': '073', + 'Lake County, MN': '075', + 'Lake of the Woods County, MN': '077', + 'Le Sueur County, MN': '079', + 'Lincoln County, MN': '081', + 'Lyon County, MN': '083', + 'Mahnomen County, MN': '087', + 'Marshall County, MN': '089', + 'Martin County, MN': '091', + 'McLeod County, MN': '085', + 'Meeker County, MN': '093', + 'Mille Lacs County, MN': '095', + 'Morrison County, MN': '097', + 'Mower County, MN': '099', + 'Murray County, MN': '101', + 'Nicollet County, MN': '103', + 'Nobles County, MN': '105', + 'Norman County, MN': '107', + 'Olmsted County, MN': '109', + 'Otter Tail County, MN': '111', + 'Pennington County, MN': '113', + 'Pine County, MN': '115', + 'Pipestone County, MN': '117', + 'Polk County, MN': '119', + 'Pope County, MN': '121', + 'Ramsey County, MN': '123', + 'Red Lake County, MN': '125', + 'Redwood County, MN': '127', + 'Renville County, MN': '129', + 'Rice County, MN': '131', + 'Rock County, MN': '133', + 'Roseau County, MN': '135', + 'Scott County, MN': '139', + 'Sherburne County, MN': '141', + 'Sibley County, MN': '143', + 'St. Louis County, MN': '137', + 'Stearns County, MN': '145', + 'Steele County, MN': '147', + 'Stevens County, MN': '149', + 'Swift County, MN': '151', + 'Todd County, MN': '153', + 'Traverse County, MN': '155', + 'Wabasha County, MN': '157', + 'Wadena County, MN': '159', + 'Waseca County, MN': '161', + 'Washington County, MN': '163', + 'Watonwan County, MN': '165', + 'Wilkin County, MN': '167', + 'Winona County, MN': '169', + 'Wright County, MN': '171', + 'Yellow Medicine County, MN': '173'}, + '28': {'Adams County, MS': '001', + 'Alcorn County, MS': '003', + 'Amite County, MS': '005', + 'Attala County, MS': '007', + 'Benton County, MS': '009', + 'Bolivar County, MS': '011', + 'Calhoun County, MS': '013', + 'Carroll County, MS': '015', + 'Chickasaw County, MS': '017', + 'Choctaw County, MS': '019', + 'Claiborne County, MS': '021', + 'Clarke County, MS': '023', + 'Clay County, MS': '025', + 'Coahoma County, MS': '027', + 'Copiah County, MS': '029', + 'Covington County, MS': '031', + 'DeSoto County, MS': '033', + 'Forrest County, MS': '035', + 'Franklin County, MS': '037', + 'George County, MS': '039', + 'Greene County, MS': '041', + 'Grenada County, MS': '043', + 'Hancock County, MS': '045', + 'Harrison County, MS': '047', + 'Hinds County, MS': '049', + 'Holmes County, MS': '051', + 'Humphreys County, MS': '053', + 'Issaquena County, MS': '055', + 'Itawamba County, MS': '057', + 'Jackson County, MS': '059', + 'Jasper County, MS': '061', + 'Jefferson County, MS': '063', + 'Jefferson Davis County, MS': '065', + 'Jones County, MS': '067', + 'Kemper County, MS': '069', + 'Lafayette County, MS': '071', + 'Lamar County, MS': '073', + 'Lauderdale County, MS': '075', + 'Lawrence County, MS': '077', + 'Leake County, MS': '079', + 'Lee County, MS': '081', + 'Leflore County, MS': '083', + 'Lincoln County, MS': '085', + 'Lowndes County, MS': '087', + 'Madison County, MS': '089', + 'Marion County, MS': '091', + 'Marshall County, MS': '093', + 'Monroe County, MS': '095', + 'Montgomery County, MS': '097', + 'Neshoba County, MS': '099', + 'Newton County, MS': '101', + 'Noxubee County, MS': '103', + 'Oktibbeha County, MS': '105', + 'Panola County, MS': '107', + 'Pearl River County, MS': '109', + 'Perry County, MS': '111', + 'Pike County, MS': '113', + 'Pontotoc County, MS': '115', + 'Prentiss County, MS': '117', + 'Quitman County, MS': '119', + 'Rankin County, MS': '121', + 'Scott County, MS': '123', + 'Sharkey County, MS': '125', + 'Simpson County, MS': '127', + 'Smith County, MS': '129', + 'Stone County, MS': '131', + 'Sunflower County, MS': '133', + 'Tallahatchie County, MS': '135', + 'Tate County, MS': '137', + 'Tippah County, MS': '139', + 'Tishomingo County, MS': '141', + 'Tunica County, MS': '143', + 'Union County, MS': '145', + 'Walthall County, MS': '147', + 'Warren County, MS': '149', + 'Washington County, MS': '151', + 'Wayne County, MS': '153', + 'Webster County, MS': '155', + 'Wilkinson County, MS': '157', + 'Winston County, MS': '159', + 'Yalobusha County, MS': '161', + 'Yazoo County, MS': '163'}, + '29': {'Adair County, MO': '001', + 'Andrew County, MO': '003', + 'Atchison County, MO': '005', + 'Audrain County, MO': '007', + 'Barry County, MO': '009', + 'Barton County, MO': '011', + 'Bates County, MO': '013', + 'Benton County, MO': '015', + 'Bollinger County, MO': '017', + 'Boone County, MO': '019', + 'Buchanan County, MO': '021', + 'Butler County, MO': '023', + 'Caldwell County, MO': '025', + 'Callaway County, MO': '027', + 'Camden County, MO': '029', + 'Cape Girardeau County, MO': '031', + 'Carroll County, MO': '033', + 'Carter County, MO': '035', + 'Cass County, MO': '037', + 'Cedar County, MO': '039', + 'Chariton County, MO': '041', + 'Christian County, MO': '043', + 'Clark County, MO': '045', + 'Clay County, MO': '047', + 'Clinton County, MO': '049', + 'Cole County, MO': '051', + 'Cooper County, MO': '053', + 'Crawford County, MO': '055', + 'Dade County, MO': '057', + 'Dallas County, MO': '059', + 'Daviess County, MO': '061', + 'DeKalb County, MO': '063', + 'Dent County, MO': '065', + 'Douglas County, MO': '067', + 'Dunklin County, MO': '069', + 'Franklin County, MO': '071', + 'Gasconade County, MO': '073', + 'Gentry County, MO': '075', + 'Greene County, MO': '077', + 'Grundy County, MO': '079', + 'Harrison County, MO': '081', + 'Henry County, MO': '083', + 'Hickory County, MO': '085', + 'Holt County, MO': '087', + 'Howard County, MO': '089', + 'Howell County, MO': '091', + 'Iron County, MO': '093', + 'Jackson County, MO': '095', + 'Jasper County, MO': '097', + 'Jefferson County, MO': '099', + 'Johnson County, MO': '101', + 'Knox County, MO': '103', + 'Laclede County, MO': '105', + 'Lafayette County, MO': '107', + 'Lawrence County, MO': '109', + 'Lewis County, MO': '111', + 'Lincoln County, MO': '113', + 'Linn County, MO': '115', + 'Livingston County, MO': '117', + 'Macon County, MO': '121', + 'Madison County, MO': '123', + 'Maries County, MO': '125', + 'Marion County, MO': '127', + 'McDonald County, MO': '119', + 'Mercer County, MO': '129', + 'Miller County, MO': '131', + 'Mississippi County, MO': '133', + 'Moniteau County, MO': '135', + 'Monroe County, MO': '137', + 'Montgomery County, MO': '139', + 'Morgan County, MO': '141', + 'New Madrid County, MO': '143', + 'Newton County, MO': '145', + 'Nodaway County, MO': '147', + 'Oregon County, MO': '149', + 'Osage County, MO': '151', + 'Ozark County, MO': '153', + 'Pemiscot County, MO': '155', + 'Perry County, MO': '157', + 'Pettis County, MO': '159', + 'Phelps County, MO': '161', + 'Pike County, MO': '163', + 'Platte County, MO': '165', + 'Polk County, MO': '167', + 'Pulaski County, MO': '169', + 'Putnam County, MO': '171', + 'Ralls County, MO': '173', + 'Randolph County, MO': '175', + 'Ray County, MO': '177', + 'Reynolds County, MO': '179', + 'Ripley County, MO': '181', + 'Saline County, MO': '195', + 'Schuyler County, MO': '197', + 'Scotland County, MO': '199', + 'Scott County, MO': '201', + 'Shannon County, MO': '203', + 'Shelby County, MO': '205', + 'St. Charles County, MO': '183', + 'St. Clair County, MO': '185', + 'St. Francois County, MO': '187', + 'St. Louis County, MO': '189', + 'St. Louis city, MO': '510', + 'Ste. Genevieve County, MO': '186', + 'Stoddard County, MO': '207', + 'Stone County, MO': '209', + 'Sullivan County, MO': '211', + 'Taney County, MO': '213', + 'Texas County, MO': '215', + 'Vernon County, MO': '217', + 'Warren County, MO': '219', + 'Washington County, MO': '221', + 'Wayne County, MO': '223', + 'Webster County, MO': '225', + 'Worth County, MO': '227', + 'Wright County, MO': '229'}, + '30': {'Beaverhead County, MT': '001', + 'Big Horn County, MT': '003', + 'Blaine County, MT': '005', + 'Broadwater County, MT': '007', + 'Carbon County, MT': '009', + 'Carter County, MT': '011', + 'Cascade County, MT': '013', + 'Chouteau County, MT': '015', + 'Custer County, MT': '017', + 'Daniels County, MT': '019', + 'Dawson County, MT': '021', + 'Deer Lodge County, MT': '023', + 'Fallon County, MT': '025', + 'Fergus County, MT': '027', + 'Flathead County, MT': '029', + 'Gallatin County, MT': '031', + 'Garfield County, MT': '033', + 'Glacier County, MT': '035', + 'Golden Valley County, MT': '037', + 'Granite County, MT': '039', + 'Hill County, MT': '041', + 'Jefferson County, MT': '043', + 'Judith Basin County, MT': '045', + 'Lake County, MT': '047', + 'Lewis and Clark County, MT': '049', + 'Liberty County, MT': '051', + 'Lincoln County, MT': '053', + 'Madison County, MT': '057', + 'McCone County, MT': '055', + 'Meagher County, MT': '059', + 'Mineral County, MT': '061', + 'Missoula County, MT': '063', + 'Musselshell County, MT': '065', + 'Park County, MT': '067', + 'Petroleum County, MT': '069', + 'Phillips County, MT': '071', + 'Pondera County, MT': '073', + 'Powder River County, MT': '075', + 'Powell County, MT': '077', + 'Prairie County, MT': '079', + 'Ravalli County, MT': '081', + 'Richland County, MT': '083', + 'Roosevelt County, MT': '085', + 'Rosebud County, MT': '087', + 'Sanders County, MT': '089', + 'Sheridan County, MT': '091', + 'Silver Bow County, MT': '093', + 'Stillwater County, MT': '095', + 'Sweet Grass County, MT': '097', + 'Teton County, MT': '099', + 'Toole County, MT': '101', + 'Treasure County, MT': '103', + 'Valley County, MT': '105', + 'Wheatland County, MT': '107', + 'Wibaux County, MT': '109', + 'Yellowstone County, MT': '111'}, + '31': {'Adams County, NE': '001', + 'Antelope County, NE': '003', + 'Arthur County, NE': '005', + 'Banner County, NE': '007', + 'Blaine County, NE': '009', + 'Boone County, NE': '011', + 'Box Butte County, NE': '013', + 'Boyd County, NE': '015', + 'Brown County, NE': '017', + 'Buffalo County, NE': '019', + 'Burt County, NE': '021', + 'Butler County, NE': '023', + 'Cass County, NE': '025', + 'Cedar County, NE': '027', + 'Chase County, NE': '029', + 'Cherry County, NE': '031', + 'Cheyenne County, NE': '033', + 'Clay County, NE': '035', + 'Colfax County, NE': '037', + 'Cuming County, NE': '039', + 'Custer County, NE': '041', + 'Dakota County, NE': '043', + 'Dawes County, NE': '045', + 'Dawson County, NE': '047', + 'Deuel County, NE': '049', + 'Dixon County, NE': '051', + 'Dodge County, NE': '053', + 'Douglas County, NE': '055', + 'Dundy County, NE': '057', + 'Fillmore County, NE': '059', + 'Franklin County, NE': '061', + 'Frontier County, NE': '063', + 'Furnas County, NE': '065', + 'Gage County, NE': '067', + 'Garden County, NE': '069', + 'Garfield County, NE': '071', + 'Gosper County, NE': '073', + 'Grant County, NE': '075', + 'Greeley County, NE': '077', + 'Hall County, NE': '079', + 'Hamilton County, NE': '081', + 'Harlan County, NE': '083', + 'Hayes County, NE': '085', + 'Hitchcock County, NE': '087', + 'Holt County, NE': '089', + 'Hooker County, NE': '091', + 'Howard County, NE': '093', + 'Jefferson County, NE': '095', + 'Johnson County, NE': '097', + 'Kearney County, NE': '099', + 'Keith County, NE': '101', + 'Keya Paha County, NE': '103', + 'Kimball County, NE': '105', + 'Knox County, NE': '107', + 'Lancaster County, NE': '109', + 'Lincoln County, NE': '111', + 'Logan County, NE': '113', + 'Loup County, NE': '115', + 'Madison County, NE': '119', + 'McPherson County, NE': '117', + 'Merrick County, NE': '121', + 'Morrill County, NE': '123', + 'Nance County, NE': '125', + 'Nemaha County, NE': '127', + 'Nuckolls County, NE': '129', + 'Otoe County, NE': '131', + 'Pawnee County, NE': '133', + 'Perkins County, NE': '135', + 'Phelps County, NE': '137', + 'Pierce County, NE': '139', + 'Platte County, NE': '141', + 'Polk County, NE': '143', + 'Red Willow County, NE': '145', + 'Richardson County, NE': '147', + 'Rock County, NE': '149', + 'Saline County, NE': '151', + 'Sarpy County, NE': '153', + 'Saunders County, NE': '155', + 'Scotts Bluff County, NE': '157', + 'Seward County, NE': '159', + 'Sheridan County, NE': '161', + 'Sherman County, NE': '163', + 'Sioux County, NE': '165', + 'Stanton County, NE': '167', + 'Thayer County, NE': '169', + 'Thomas County, NE': '171', + 'Thurston County, NE': '173', + 'Valley County, NE': '175', + 'Washington County, NE': '177', + 'Wayne County, NE': '179', + 'Webster County, NE': '181', + 'Wheeler County, NE': '183', + 'York County, NE': '185'}, + '32': {'Carson City, NV': '510', + 'Churchill County, NV': '001', + 'Clark County, NV': '003', + 'Douglas County, NV': '005', + 'Elko County, NV': '007', + 'Esmeralda County, NV': '009', + 'Eureka County, NV': '011', + 'Humboldt County, NV': '013', + 'Lander County, NV': '015', + 'Lincoln County, NV': '017', + 'Lyon County, NV': '019', + 'Mineral County, NV': '021', + 'Nye County, NV': '023', + 'Pershing County, NV': '027', + 'Storey County, NV': '029', + 'Washoe County, NV': '031', + 'White Pine County, NV': '033'}, + '33': {'Belknap County, NH': '001', + 'Carroll County, NH': '003', + 'Cheshire County, NH': '005', + 'Coos County, NH': '007', + 'Grafton County, NH': '009', + 'Hillsborough County, NH': '011', + 'Merrimack County, NH': '013', + 'Rockingham County, NH': '015', + 'Strafford County, NH': '017', + 'Sullivan County, NH': '019'}, + '34': {'Atlantic County, NJ': '001', + 'Bergen County, NJ': '003', + 'Burlington County, NJ': '005', + 'Camden County, NJ': '007', + 'Cape May County, NJ': '009', + 'Cumberland County, NJ': '011', + 'Essex County, NJ': '013', + 'Gloucester County, NJ': '015', + 'Hudson County, NJ': '017', + 'Hunterdon County, NJ': '019', + 'Mercer County, NJ': '021', + 'Middlesex County, NJ': '023', + 'Monmouth County, NJ': '025', + 'Morris County, NJ': '027', + 'Ocean County, NJ': '029', + 'Passaic County, NJ': '031', + 'Salem County, NJ': '033', + 'Somerset County, NJ': '035', + 'Sussex County, NJ': '037', + 'Union County, NJ': '039', + 'Warren County, NJ': '041'}, + '35': {'Bernalillo County, NM': '001', + 'Catron County, NM': '003', + 'Chaves County, NM': '005', + 'Cibola County, NM': '006', + 'Colfax County, NM': '007', + 'Curry County, NM': '009', + 'DeBaca County, NM': '011', + 'Dona Ana County, NM': '013', + 'Eddy County, NM': '015', + 'Grant County, NM': '017', + 'Guadalupe County, NM': '019', + 'Harding County, NM': '021', + 'Hidalgo County, NM': '023', + 'Lea County, NM': '025', + 'Lincoln County, NM': '027', + 'Los Alamos County, NM': '028', + 'Luna County, NM': '029', + 'McKinley County, NM': '031', + 'Mora County, NM': '033', + 'Otero County, NM': '035', + 'Quay County, NM': '037', + 'Rio Arriba County, NM': '039', + 'Roosevelt County, NM': '041', + 'San Juan County, NM': '045', + 'San Miguel County, NM': '047', + 'Sandoval County, NM': '043', + 'Santa Fe County, NM': '049', + 'Sierra County, NM': '051', + 'Socorro County, NM': '053', + 'Taos County, NM': '055', + 'Torrance County, NM': '057', + 'Union County, NM': '059', + 'Valencia County, NM': '061'}, + '36': {'Albany County, NY': '001', + 'Allegany County, NY': '003', + 'Bronx County, NY': '005', + 'Broome County, NY': '007', + 'Cattaraugus County, NY': '009', + 'Cayuga County, NY': '011', + 'Chautauqua County, NY': '013', + 'Chemung County, NY': '015', + 'Chenango County, NY': '017', + 'Clinton County, NY': '019', + 'Columbia County, NY': '021', + 'Cortland County, NY': '023', + 'Delaware County, NY': '025', + 'Dutchess County, NY': '027', + 'Erie County, NY': '029', + 'Essex County, NY': '031', + 'Franklin County, NY': '033', + 'Fulton County, NY': '035', + 'Genesee County, NY': '037', + 'Greene County, NY': '039', + 'Hamilton County, NY': '041', + 'Herkimer County, NY': '043', + 'Jefferson County, NY': '045', + 'Kings County, NY': '047', + 'Lewis County, NY': '049', + 'Livingston County, NY': '051', + 'Madison County, NY': '053', + 'Monroe County, NY': '055', + 'Montgomery County, NY': '057', + 'Nassau County, NY': '059', + 'New York County, NY': '061', + 'Niagara County, NY': '063', + 'Oneida County, NY': '065', + 'Onondaga County, NY': '067', + 'Ontario County, NY': '069', + 'Orange County, NY': '071', + 'Orleans County, NY': '073', + 'Oswego County, NY': '075', + 'Otsego County, NY': '077', + 'Putnam County, NY': '079', + 'Queens County, NY': '081', + 'Rensselaer County, NY': '083', + 'Richmond County, NY': '085', + 'Rockland County, NY': '087', + 'Saratoga County, NY': '091', + 'Schenectady County, NY': '093', + 'Schoharie County, NY': '095', + 'Schuyler County, NY': '097', + 'Seneca County, NY': '099', + 'St. Lawrence County, NY': '089', + 'Steuben County, NY': '101', + 'Suffolk County, NY': '103', + 'Sullivan County, NY': '105', + 'Tioga County, NY': '107', + 'Tompkins County, NY': '109', + 'Ulster County, NY': '111', + 'Warren County, NY': '113', + 'Washington County, NY': '115', + 'Wayne County, NY': '117', + 'Westchester County, NY': '119', + 'Wyoming County, NY': '121', + 'Yates County, NY': '123'}, + '37': {'Alamance County, NC': '001', + 'Alexander County, NC': '003', + 'Alleghany County, NC': '005', + 'Anson County, NC': '007', + 'Ashe County, NC': '009', + 'Avery County, NC': '011', + 'Beaufort County, NC': '013', + 'Bertie County, NC': '015', + 'Bladen County, NC': '017', + 'Brunswick County, NC': '019', + 'Buncombe County, NC': '021', + 'Burke County, NC': '023', + 'Cabarrus County, NC': '025', + 'Caldwell County, NC': '027', + 'Camden County, NC': '029', + 'Carteret County, NC': '031', + 'Caswell County, NC': '033', + 'Catawba County, NC': '035', + 'Chatham County, NC': '037', + 'Cherokee County, NC': '039', + 'Chowan County, NC': '041', + 'Clay County, NC': '043', + 'Cleveland County, NC': '045', + 'Columbus County, NC': '047', + 'Craven County, NC': '049', + 'Cumberland County, NC': '051', + 'Currituck County, NC': '053', + 'Dare County, NC': '055', + 'Davidson County, NC': '057', + 'Davie County, NC': '059', + 'Duplin County, NC': '061', + 'Durham County, NC': '063', + 'Edgecombe County, NC': '065', + 'Forsyth County, NC': '067', + 'Franklin County, NC': '069', + 'Gaston County, NC': '071', + 'Gates County, NC': '073', + 'Graham County, NC': '075', + 'Granville County, NC': '077', + 'Greene County, NC': '079', + 'Guilford County, NC': '081', + 'Halifax County, NC': '083', + 'Harnett County, NC': '085', + 'Haywood County, NC': '087', + 'Henderson County, NC': '089', + 'Hertford County, NC': '091', + 'Hoke County, NC': '093', + 'Hyde County, NC': '095', + 'Iredell County, NC': '097', + 'Jackson County, NC': '099', + 'Johnston County, NC': '101', + 'Jones County, NC': '103', + 'Lee County, NC': '105', + 'Lenoir County, NC': '107', + 'Lincoln County, NC': '109', + 'Macon County, NC': '113', + 'Madison County, NC': '115', + 'Martin County, NC': '117', + 'McDowell County, NC': '111', + 'Mecklenburg County, NC': '119', + 'Mitchell County, NC': '121', + 'Montgomery County, NC': '123', + 'Moore County, NC': '125', + 'Nash County, NC': '127', + 'New Hanover County, NC': '129', + 'Northampton County, NC': '131', + 'Onslow County, NC': '133', + 'Orange County, NC': '135', + 'Pamlico County, NC': '137', + 'Pasquotank County, NC': '139', + 'Pender County, NC': '141', + 'Perquimans County, NC': '143', + 'Person County, NC': '145', + 'Pitt County, NC': '147', + 'Polk County, NC': '149', + 'Randolph County, NC': '151', + 'Richmond County, NC': '153', + 'Robeson County, NC': '155', + 'Rockingham County, NC': '157', + 'Rowan County, NC': '159', + 'Rutherford County, NC': '161', + 'Sampson County, NC': '163', + 'Scotland County, NC': '165', + 'Stanly County, NC': '167', + 'Stokes County, NC': '169', + 'Surry County, NC': '171', + 'Swain County, NC': '173', + 'Transylvania County, NC': '175', + 'Tyrrell County, NC': '177', + 'Union County, NC': '179', + 'Vance County, NC': '181', + 'Wake County, NC': '183', + 'Warren County, NC': '185', + 'Washington County, NC': '187', + 'Watauga County, NC': '189', + 'Wayne County, NC': '191', + 'Wilkes County, NC': '193', + 'Wilson County, NC': '195', + 'Yadkin County, NC': '197', + 'Yancey County, NC': '199'}, + '38': {'Adams County, ND': '001', + 'Barnes County, ND': '003', + 'Benson County, ND': '005', + 'Billings County, ND': '007', + 'Bottineau County, ND': '009', + 'Bowman County, ND': '011', + 'Burke County, ND': '013', + 'Burleigh County, ND': '015', + 'Cass County, ND': '017', + 'Cavalier County, ND': '019', + 'Dickey County, ND': '021', + 'Divide County, ND': '023', + 'Dunn County, ND': '025', + 'Eddy County, ND': '027', + 'Emmons County, ND': '029', + 'Foster County, ND': '031', + 'Golden Valley County, ND': '033', + 'Grand Forks County, ND': '035', + 'Grant County, ND': '037', + 'Griggs County, ND': '039', + 'Hettinger County, ND': '041', + 'Kidder County, ND': '043', + 'LaMoure County, ND': '045', + 'Logan County, ND': '047', + 'McHenry County, ND': '049', + 'McIntosh County, ND': '051', + 'McKenzie County, ND': '053', + 'McLean County, ND': '055', + 'Mercer County, ND': '057', + 'Morton County, ND': '059', + 'Mountrail County, ND': '061', + 'Nelson County, ND': '063', + 'Oliver County, ND': '065', + 'Pembina County, ND': '067', + 'Pierce County, ND': '069', + 'Ramsey County, ND': '071', + 'Ransom County, ND': '073', + 'Renville County, ND': '075', + 'Richland County, ND': '077', + 'Rolette County, ND': '079', + 'Sargent County, ND': '081', + 'Sheridan County, ND': '083', + 'Sioux County, ND': '085', + 'Slope County, ND': '087', + 'Stark County, ND': '089', + 'Steele County, ND': '091', + 'Stutsman County, ND': '093', + 'Towner County, ND': '095', + 'Traill County, ND': '097', + 'Walsh County, ND': '099', + 'Ward County, ND': '101', + 'Wells County, ND': '103', + 'Williams County, ND': '105'}, + '39': {'Adams County, OH': '001', + 'Allen County, OH': '003', + 'Ashland County, OH': '005', + 'Ashtabula County, OH': '007', + 'Athens County, OH': '009', + 'Auglaize County, OH': '011', + 'Belmont County, OH': '013', + 'Brown County, OH': '015', + 'Butler County, OH': '017', + 'Carroll County, OH': '019', + 'Champaign County, OH': '021', + 'Clark County, OH': '023', + 'Clermont County, OH': '025', + 'Clinton County, OH': '027', + 'Columbiana County, OH': '029', + 'Coshocton County, OH': '031', + 'Crawford County, OH': '033', + 'Cuyahoga County, OH': '035', + 'Darke County, OH': '037', + 'Defiance County, OH': '039', + 'Delaware County, OH': '041', + 'Erie County, OH': '043', + 'Fairfield County, OH': '045', + 'Fayette County, OH': '047', + 'Franklin County, OH': '049', + 'Fulton County, OH': '051', + 'Gallia County, OH': '053', + 'Geauga County, OH': '055', + 'Greene County, OH': '057', + 'Guernsey County, OH': '059', + 'Hamilton County, OH': '061', + 'Hancock County, OH': '063', + 'Hardin County, OH': '065', + 'Harrison County, OH': '067', + 'Henry County, OH': '069', + 'Highland County, OH': '071', + 'Hocking County, OH': '073', + 'Holmes County, OH': '075', + 'Huron County, OH': '077', + 'Jackson County, OH': '079', + 'Jefferson County, OH': '081', + 'Knox County, OH': '083', + 'Lake County, OH': '085', + 'Lawrence County, OH': '087', + 'Licking County, OH': '089', + 'Logan County, OH': '091', + 'Lorain County, OH': '093', + 'Lucas County, OH': '095', + 'Madison County, OH': '097', + 'Mahoning County, OH': '099', + 'Marion County, OH': '101', + 'Medina County, OH': '103', + 'Meigs County, OH': '105', + 'Mercer County, OH': '107', + 'Miami County, OH': '109', + 'Monroe County, OH': '111', + 'Montgomery County, OH': '113', + 'Morgan County, OH': '115', + 'Morrow County, OH': '117', + 'Muskingum County, OH': '119', + 'Noble County, OH': '121', + 'Ottawa County, OH': '123', + 'Paulding County, OH': '125', + 'Perry County, OH': '127', + 'Pickaway County, OH': '129', + 'Pike County, OH': '131', + 'Portage County, OH': '133', + 'Preble County, OH': '135', + 'Putnam County, OH': '137', + 'Richland County, OH': '139', + 'Ross County, OH': '141', + 'Sandusky County, OH': '143', + 'Scioto County, OH': '145', + 'Seneca County, OH': '147', + 'Shelby County, OH': '149', + 'Stark County, OH': '151', + 'Summit County, OH': '153', + 'Trumbull County, OH': '155', + 'Tuscarawas County, OH': '157', + 'Union County, OH': '159', + 'Van Wert County, OH': '161', + 'Vinton County, OH': '163', + 'Warren County, OH': '165', + 'Washington County, OH': '167', + 'Wayne County, OH': '169', + 'Williams County, OH': '171', + 'Wood County, OH': '173', + 'Wyandot County, OH': '175'}, + '40': {'Adair County, OK': '001', + 'Alfalfa County, OK': '003', + 'Atoka County, OK': '005', + 'Beaver County, OK': '007', + 'Beckham County, OK': '009', + 'Blaine County, OK': '011', + 'Bryan County, OK': '013', + 'Caddo County, OK': '015', + 'Canadian County, OK': '017', + 'Carter County, OK': '019', + 'Cherokee County, OK': '021', + 'Choctaw County, OK': '023', + 'Cimarron County, OK': '025', + 'Cleveland County, OK': '027', + 'Coal County, OK': '029', + 'Comanche County, OK': '031', + 'Cotton County, OK': '033', + 'Craig County, OK': '035', + 'Creek County, OK': '037', + 'Custer County, OK': '039', + 'Delaware County, OK': '041', + 'Dewey County, OK': '043', + 'Ellis County, OK': '045', + 'Garfield County, OK': '047', + 'Garvin County, OK': '049', + 'Grady County, OK': '051', + 'Grant County, OK': '053', + 'Greer County, OK': '055', + 'Harmon County, OK': '057', + 'Harper County, OK': '059', + 'Haskell County, OK': '061', + 'Hughes County, OK': '063', + 'Jackson County, OK': '065', + 'Jefferson County, OK': '067', + 'Johnston County, OK': '069', + 'Kay County, OK': '071', + 'Kingfisher County, OK': '073', + 'Kiowa County, OK': '075', + 'Latimer County, OK': '077', + 'Le Flore County, OK': '079', + 'Lincoln County, OK': '081', + 'Logan County, OK': '083', + 'Love County, OK': '085', + 'Major County, OK': '093', + 'Marshall County, OK': '095', + 'Mayes County, OK': '097', + 'McClain County, OK': '087', + 'McCurtain County, OK': '089', + 'McIntosh County, OK': '091', + 'Murray County, OK': '099', + 'Muskogee County, OK': '101', + 'Noble County, OK': '103', + 'Nowata County, OK': '105', + 'Okfuskee County, OK': '107', + 'Oklahoma County, OK': '109', + 'Okmulgee County, OK': '111', + 'Osage County, OK': '113', + 'Ottawa County, OK': '115', + 'Pawnee County, OK': '117', + 'Payne County, OK': '119', + 'Pittsburg County, OK': '121', + 'Pontotoc County, OK': '123', + 'Pottawatomie County, OK': '125', + 'Pushmataha County, OK': '127', + 'Roger Mills County, OK': '129', + 'Rogers County, OK': '131', + 'Seminole County, OK': '133', + 'Sequoyah County, OK': '135', + 'Stephens County, OK': '137', + 'Texas County, OK': '139', + 'Tillman County, OK': '141', + 'Tulsa County, OK': '143', + 'Wagoner County, OK': '145', + 'Washington County, OK': '147', + 'Washita County, OK': '149', + 'Woods County, OK': '151', + 'Woodward County, OK': '153'}, + '41': {'Baker County, OR': '001', + 'Benton County, OR': '003', + 'Clackamas County, OR': '005', + 'Clatsop County, OR': '007', + 'Columbia County, OR': '009', + 'Coos County, OR': '011', + 'Crook County, OR': '013', + 'Curry County, OR': '015', + 'Deschutes County, OR': '017', + 'Douglas County, OR': '019', + 'Gilliam County, OR': '021', + 'Grant County, OR': '023', + 'Harney County, OR': '025', + 'Hood River County, OR': '027', + 'Jackson County, OR': '029', + 'Jefferson County, OR': '031', + 'Josephine County, OR': '033', + 'Klamath County, OR': '035', + 'Lake County, OR': '037', + 'Lane County, OR': '039', + 'Lincoln County, OR': '041', + 'Linn County, OR': '043', + 'Malheur County, OR': '045', + 'Marion County, OR': '047', + 'Morrow County, OR': '049', + 'Multnomah County, OR': '051', + 'Polk County, OR': '053', + 'Sherman County, OR': '055', + 'Tillamook County, OR': '057', + 'Umatilla County, OR': '059', + 'Union County, OR': '061', + 'Wallowa County, OR': '063', + 'Wasco County, OR': '065', + 'Washington County, OR': '067', + 'Wheeler County, OR': '069', + 'Yamhill County, OR': '071'}, + '42': {'Adams County, PA': '001', + 'Allegheny County, PA': '003', + 'Armstrong County, PA': '005', + 'Beaver County, PA': '007', + 'Bedford County, PA': '009', + 'Berks County, PA': '011', + 'Blair County, PA': '013', + 'Bradford County, PA': '015', + 'Bucks County, PA': '017', + 'Butler County, PA': '019', + 'Cambria County, PA': '021', + 'Cameron County, PA': '023', + 'Carbon County, PA': '025', + 'Centre County, PA': '027', + 'Chester County, PA': '029', + 'Clarion County, PA': '031', + 'Clearfield County, PA': '033', + 'Clinton County, PA': '035', + 'Columbia County, PA': '037', + 'Crawford County, PA': '039', + 'Cumberland County, PA': '041', + 'Dauphin County, PA': '043', + 'Delaware County, PA': '045', + 'Elk County, PA': '047', + 'Erie County, PA': '049', + 'Fayette County, PA': '051', + 'Forest County, PA': '053', + 'Franklin County, PA': '055', + 'Fulton County, PA': '057', + 'Greene County, PA': '059', + 'Huntingdon County, PA': '061', + 'Indiana County, PA': '063', + 'Jefferson County, PA': '065', + 'Juniata County, PA': '067', + 'Lackawanna County, PA': '069', + 'Lancaster County, PA': '071', + 'Lawrence County, PA': '073', + 'Lebanon County, PA': '075', + 'Lehigh County, PA': '077', + 'Luzerne County, PA': '079', + 'Lycoming County, PA': '081', + 'McKean County, PA': '083', + 'Mercer County, PA': '085', + 'Mifflin County, PA': '087', + 'Monroe County, PA': '089', + 'Montgomery County, PA': '091', + 'Montour County, PA': '093', + 'Northampton County, PA': '095', + 'Northumberland County, PA': '097', + 'Perry County, PA': '099', + 'Philadelphia County/city, PA': '101', + 'Pike County, PA': '103', + 'Potter County, PA': '105', + 'Schuylkill County, PA': '107', + 'Snyder County, PA': '109', + 'Somerset County, PA': '111', + 'Sullivan County, PA': '113', + 'Susquehanna County, PA': '115', + 'Tioga County, PA': '117', + 'Union County, PA': '119', + 'Venango County, PA': '121', + 'Warren County, PA': '123', + 'Washington County, PA': '125', + 'Wayne County, PA': '127', + 'Westmoreland County, PA': '129', + 'Wyoming County, PA': '131', + 'York County, PA': '133'}, + '44': {'Bristol County, RI': '001', + 'Kent County, RI': '003', + 'Newport County, RI': '005', + 'Providence County, RI': '007', + 'Washington County, RI': '009'}, + '45': {'Abbeville County, SC': '001', + 'Aiken County, SC': '003', + 'Allendale County, SC': '005', + 'Anderson County, SC': '007', + 'Bamberg County, SC': '009', + 'Barnwell County, SC': '011', + 'Beaufort County, SC': '013', + 'Berkeley County, SC': '015', + 'Calhoun County, SC': '017', + 'Charleston County, SC': '019', + 'Cherokee County, SC': '021', + 'Chester County, SC': '023', + 'Chesterfield County, SC': '025', + 'Clarendon County, SC': '027', + 'Colleton County, SC': '029', + 'Darlington County, SC': '031', + 'Dillon County, SC': '033', + 'Dorchester County, SC': '035', + 'Edgefield County, SC': '037', + 'Fairfield County, SC': '039', + 'Florence County, SC': '041', + 'Georgetown County, SC': '043', + 'Greenville County, SC': '045', + 'Greenwood County, SC': '047', + 'Hampton County, SC': '049', + 'Horry County, SC': '051', + 'Jasper County, SC': '053', + 'Kershaw County, SC': '055', + 'Lancaster County, SC': '057', + 'Laurens County, SC': '059', + 'Lee County, SC': '061', + 'Lexington County, SC': '063', + 'Marion County, SC': '067', + 'Marlboro County, SC': '069', + 'McCormick County, SC': '065', + 'Newberry County, SC': '071', + 'Oconee County, SC': '073', + 'Orangeburg County, SC': '075', + 'Pickens County, SC': '077', + 'Richland County, SC': '079', + 'Saluda County, SC': '081', + 'Spartanburg County, SC': '083', + 'Sumter County, SC': '085', + 'Union County, SC': '087', + 'Williamsburg County, SC': '089', + 'York County, SC': '091'}, + '46': {'Aurora County, SD': '003', + 'Beadle County, SD': '005', + 'Bennett County, SD': '007', + 'Bon Homme County, SD': '009', + 'Brookings County, SD': '011', + 'Brown County, SD': '013', + 'Brule County, SD': '015', + 'Buffalo County, SD': '017', + 'Butte County, SD': '019', + 'Campbell County, SD': '021', + 'Charles Mix County, SD': '023', + 'Clark County, SD': '025', + 'Clay County, SD': '027', + 'Codington County, SD': '029', + 'Corson County, SD': '031', + 'Custer County, SD': '033', + 'Davison County, SD': '035', + 'Day County, SD': '037', + 'Deuel County, SD': '039', + 'Dewey County, SD': '041', + 'Douglas County, SD': '043', + 'Edmunds County, SD': '045', + 'Fall River County, SD': '047', + 'Faulk County, SD': '049', + 'Grant County, SD': '051', + 'Gregory County, SD': '053', + 'Haakon County, SD': '055', + 'Hamlin County, SD': '057', + 'Hand County, SD': '059', + 'Hanson County, SD': '061', + 'Harding County, SD': '063', + 'Hughes County, SD': '065', + 'Hutchinson County, SD': '067', + 'Hyde County, SD': '069', + 'Jackson County, SD': '071', + 'Jerauld County, SD': '073', + 'Jones County, SD': '075', + 'Kingsbury County, SD': '077', + 'Lake County, SD': '079', + 'Lawrence County, SD': '081', + 'Lincoln County, SD': '083', + 'Lyman County, SD': '085', + 'Marshall County, SD': '091', + 'McCook County, SD': '087', + 'McPherson County, SD': '089', + 'Meade County, SD': '093', + 'Mellette County, SD': '095', + 'Miner County, SD': '097', + 'Minnehaha County, SD': '099', + 'Moody County, SD': '101', + 'Pennington County, SD': '103', + 'Perkins County, SD': '105', + 'Potter County, SD': '107', + 'Roberts County, SD': '109', + 'Sanborn County, SD': '111', + 'Shannon County, SD': '113', + 'Spink County, SD': '115', + 'Stanley County, SD': '117', + 'Sully County, SD': '119', + 'Todd County, SD': '121', + 'Tripp County, SD': '123', + 'Turner County, SD': '125', + 'Union County, SD': '127', + 'Walworth County, SD': '129', + 'Yankton County, SD': '135', + 'Ziebach County, SD': '137'}, + '47': {'Anderson County, TN': '001', + 'Bedford County, TN': '003', + 'Benton County, TN': '005', + 'Bledsoe County, TN': '007', + 'Blount County, TN': '009', + 'Bradley County, TN': '011', + 'Campbell County, TN': '013', + 'Cannon County, TN': '015', + 'Carroll County, TN': '017', + 'Carter County, TN': '019', + 'Cheatham County, TN': '021', + 'Chester County, TN': '023', + 'Claiborne County, TN': '025', + 'Clay County, TN': '027', + 'Cocke County, TN': '029', + 'Coffee County, TN': '031', + 'Crockett County, TN': '033', + 'Cumberland County, TN': '035', + 'Davidson County, TN': '037', + 'DeKalb County, TN': '041', + 'Decatur County, TN': '039', + 'Dickson County, TN': '043', + 'Dyer County, TN': '045', + 'Fayette County, TN': '047', + 'Fentress County, TN': '049', + 'Franklin County, TN': '051', + 'Gibson County, TN': '053', + 'Giles County, TN': '055', + 'Grainger County, TN': '057', + 'Greene County, TN': '059', + 'Grundy County, TN': '061', + 'Hamblen County, TN': '063', + 'Hamilton County, TN': '065', + 'Hancock County, TN': '067', + 'Hardeman County, TN': '069', + 'Hardin County, TN': '071', + 'Hawkins County, TN': '073', + 'Haywood County, TN': '075', + 'Henderson County, TN': '077', + 'Henry County, TN': '079', + 'Hickman County, TN': '081', + 'Houston County, TN': '083', + 'Humphreys County, TN': '085', + 'Jackson County, TN': '087', + 'Jefferson County, TN': '089', + 'Johnson County, TN': '091', + 'Knox County, TN': '093', + 'Lake County, TN': '095', + 'Lauderdale County, TN': '097', + 'Lawrence County, TN': '099', + 'Lewis County, TN': '101', + 'Lincoln County, TN': '103', + 'Loudon County, TN': '105', + 'Macon County, TN': '111', + 'Madison County, TN': '113', + 'Marion County, TN': '115', + 'Marshall County, TN': '117', + 'Maury County, TN': '119', + 'McMinn County, TN': '107', + 'McNairy County, TN': '109', + 'Meigs County, TN': '121', + 'Monroe County, TN': '123', + 'Montgomery County, TN': '125', + 'Moore County, TN': '127', + 'Morgan County, TN': '129', + 'Obion County, TN': '131', + 'Overton County, TN': '133', + 'Perry County, TN': '135', + 'Pickett County, TN': '137', + 'Polk County, TN': '139', + 'Putnam County, TN': '141', + 'Rhea County, TN': '143', + 'Roane County, TN': '145', + 'Robertson County, TN': '147', + 'Rutherford County, TN': '149', + 'Scott County, TN': '151', + 'Sequatchie County, TN': '153', + 'Sevier County, TN': '155', + 'Shelby County, TN': '157', + 'Smith County, TN': '159', + 'Stewart County, TN': '161', + 'Sullivan County, TN': '163', + 'Sumner County, TN': '165', + 'Tipton County, TN': '167', + 'Trousdale County, TN': '169', + 'Unicoi County, TN': '171', + 'Union County, TN': '173', + 'Van Buren County, TN': '175', + 'Warren County, TN': '177', + 'Washington County, TN': '179', + 'Wayne County, TN': '181', + 'Weakley County, TN': '183', + 'White County, TN': '185', + 'Williamson County, TN': '187', + 'Wilson County, TN': '189'}, + '48': {'Anderson County, TX': '001', + 'Andrews County, TX': '003', + 'Angelina County, TX': '005', + 'Aransas County, TX': '007', + 'Archer County, TX': '009', + 'Armstrong County, TX': '011', + 'Atascosa County, TX': '013', + 'Austin County, TX': '015', + 'Bailey County, TX': '017', + 'Bandera County, TX': '019', + 'Bastrop County, TX': '021', + 'Baylor County, TX': '023', + 'Bee County, TX': '025', + 'Bell County, TX': '027', + 'Bexar County, TX': '029', + 'Blanco County, TX': '031', + 'Borden County, TX': '033', + 'Bosque County, TX': '035', + 'Bowie County, TX': '037', + 'Brazoria County, TX': '039', + 'Brazos County, TX': '041', + 'Brewster County, TX': '043', + 'Briscoe County, TX': '045', + 'Brooks County, TX': '047', + 'Brown County, TX': '049', + 'Burleson County, TX': '051', + 'Burnet County, TX': '053', + 'Caldwell County, TX': '055', + 'Calhoun County, TX': '057', + 'Callahan County, TX': '059', + 'Cameron County, TX': '061', + 'Camp County, TX': '063', + 'Carson County, TX': '065', + 'Cass County, TX': '067', + 'Castro County, TX': '069', + 'Chambers County, TX': '071', + 'Cherokee County, TX': '073', + 'Childress County, TX': '075', + 'Clay County, TX': '077', + 'Cochran County, TX': '079', + 'Coke County, TX': '081', + 'Coleman County, TX': '083', + 'Collin County, TX': '085', + 'Collingsworth County, TX': '087', + 'Colorado County, TX': '089', + 'Comal County, TX': '091', + 'Comanche County, TX': '093', + 'Concho County, TX': '095', + 'Cooke County, TX': '097', + 'Coryell County, TX': '099', + 'Cottle County, TX': '101', + 'Crane County, TX': '103', + 'Crockett County, TX': '105', + 'Crosby County, TX': '107', + 'Culberson County, TX': '109', + 'Dallam County, TX': '111', + 'Dallas County, TX': '113', + 'Dawson County, TX': '115', + 'DeWitt County, TX': '123', + 'Deaf Smith County, TX': '117', + 'Delta County, TX': '119', + 'Denton County, TX': '121', + 'Dickens County, TX': '125', + 'Dimmit County, TX': '127', + 'Donley County, TX': '129', + 'Duval County, TX': '131', + 'Eastland County, TX': '133', + 'Ector County, TX': '135', + 'Edwards County, TX': '137', + 'El Paso County, TX': '141', + 'Ellis County, TX': '139', + 'Erath County, TX': '143', + 'Falls County, TX': '145', + 'Fannin County, TX': '147', + 'Fayette County, TX': '149', + 'Fisher County, TX': '151', + 'Floyd County, TX': '153', + 'Foard County, TX': '155', + 'Fort Bend County, TX': '157', + 'Franklin County, TX': '159', + 'Freestone County, TX': '161', + 'Frio County, TX': '163', + 'Gaines County, TX': '165', + 'Galveston County, TX': '167', + 'Garza County, TX': '169', + 'Gillespie County, TX': '171', + 'Glasscock County, TX': '173', + 'Goliad County, TX': '175', + 'Gonzales County, TX': '177', + 'Gray County, TX': '179', + 'Grayson County, TX': '181', + 'Gregg County, TX': '183', + 'Grimes County, TX': '185', + 'Guadalupe County, TX': '187', + 'Hale County, TX': '189', + 'Hall County, TX': '191', + 'Hamilton County, TX': '193', + 'Hansford County, TX': '195', + 'Hardeman County, TX': '197', + 'Hardin County, TX': '199', + 'Harris County, TX': '201', + 'Harrison County, TX': '203', + 'Hartley County, TX': '205', + 'Haskell County, TX': '207', + 'Hays County, TX': '209', + 'Hemphill County, TX': '211', + 'Henderson County, TX': '213', + 'Hidalgo County, TX': '215', + 'Hill County, TX': '217', + 'Hockley County, TX': '219', + 'Hood County, TX': '221', + 'Hopkins County, TX': '223', + 'Houston County, TX': '225', + 'Howard County, TX': '227', + 'Hudspeth County, TX': '229', + 'Hunt County, TX': '231', + 'Hutchinson County, TX': '233', + 'Irion County, TX': '235', + 'Jack County, TX': '237', + 'Jackson County, TX': '239', + 'Jasper County, TX': '241', + 'Jeff Davis County, TX': '243', + 'Jefferson County, TX': '245', + 'Jim Hogg County, TX': '247', + 'Jim Wells County, TX': '249', + 'Johnson County, TX': '251', + 'Jones County, TX': '253', + 'Karnes County, TX': '255', + 'Kaufman County, TX': '257', + 'Kendall County, TX': '259', + 'Kenedy County, TX': '261', + 'Kent County, TX': '263', + 'Kerr County, TX': '265', + 'Kimble County, TX': '267', + 'King County, TX': '269', + 'Kinney County, TX': '271', + 'Kleberg County, TX': '273', + 'Knox County, TX': '275', + 'La Salle County, TX': '283', + 'Lamar County, TX': '277', + 'Lamb County, TX': '279', + 'Lampasas County, TX': '281', + 'Lavaca County, TX': '285', + 'Lee County, TX': '287', + 'Leon County, TX': '289', + 'Liberty County, TX': '291', + 'Limestone County, TX': '293', + 'Lipscomb County, TX': '295', + 'Live Oak County, TX': '297', + 'Llano County, TX': '299', + 'Loving County, TX': '301', + 'Lubbock County, TX': '303', + 'Lynn County, TX': '305', + 'Madison County, TX': '313', + 'Marion County, TX': '315', + 'Martin County, TX': '317', + 'Mason County, TX': '319', + 'Matagorda County, TX': '321', + 'Maverick County, TX': '323', + 'McCulloch County, TX': '307', + 'McLennan County, TX': '309', + 'McMullen County, TX': '311', + 'Medina County, TX': '325', + 'Menard County, TX': '327', + 'Midland County, TX': '329', + 'Milam County, TX': '331', + 'Mills County, TX': '333', + 'Mitchell County, TX': '335', + 'Montague County, TX': '337', + 'Montgomery County, TX': '339', + 'Moore County, TX': '341', + 'Morris County, TX': '343', + 'Motley County, TX': '345', + 'Nacogdoches County, TX': '347', + 'Navarro County, TX': '349', + 'Newton County, TX': '351', + 'Nolan County, TX': '353', + 'Nueces County, TX': '355', + 'Ochiltree County, TX': '357', + 'Oldham County, TX': '359', + 'Orange County, TX': '361', + 'Palo Pinto County, TX': '363', + 'Panola County, TX': '365', + 'Parker County, TX': '367', + 'Parmer County, TX': '369', + 'Pecos County, TX': '371', + 'Polk County, TX': '373', + 'Potter County, TX': '375', + 'Presidio County, TX': '377', + 'Rains County, TX': '379', + 'Randall County, TX': '381', + 'Reagan County, TX': '383', + 'Real County, TX': '385', + 'Red River County, TX': '387', + 'Reeves County, TX': '389', + 'Refugio County, TX': '391', + 'Roberts County, TX': '393', + 'Robertson County, TX': '395', + 'Rockwall County, TX': '397', + 'Runnels County, TX': '399', + 'Rusk County, TX': '401', + 'Sabine County, TX': '403', + 'San Augustine County, TX': '405', + 'San Jacinto County, TX': '407', + 'San Patricio County, TX': '409', + 'San Saba County, TX': '411', + 'Schleicher County, TX': '413', + 'Scurry County, TX': '415', + 'Shackelford County, TX': '417', + 'Shelby County, TX': '419', + 'Sherman County, TX': '421', + 'Smith County, TX': '423', + 'Somervell County, TX': '425', + 'Starr County, TX': '427', + 'Stephens County, TX': '429', + 'Sterling County, TX': '431', + 'Stonewall County, TX': '433', + 'Sutton County, TX': '435', + 'Swisher County, TX': '437', + 'Tarrant County, TX': '439', + 'Taylor County, TX': '441', + 'Terrell County, TX': '443', + 'Terry County, TX': '445', + 'Throckmorton County, TX': '447', + 'Titus County, TX': '449', + 'Tom Green County, TX': '451', + 'Travis County, TX': '453', + 'Trinity County, TX': '455', + 'Tyler County, TX': '457', + 'Upshur County, TX': '459', + 'Upton County, TX': '461', + 'Uvalde County, TX': '463', + 'Val Verde County, TX': '465', + 'Van Zandt County, TX': '467', + 'Victoria County, TX': '469', + 'Walker County, TX': '471', + 'Waller County, TX': '473', + 'Ward County, TX': '475', + 'Washington County, TX': '477', + 'Webb County, TX': '479', + 'Wharton County, TX': '481', + 'Wheeler County, TX': '483', + 'Wichita County, TX': '485', + 'Wilbarger County, TX': '487', + 'Willacy County, TX': '489', + 'Williamson County, TX': '491', + 'Wilson County, TX': '493', + 'Winkler County, TX': '495', + 'Wise County, TX': '497', + 'Wood County, TX': '499', + 'Yoakum County, TX': '501', + 'Young County, TX': '503', + 'Zapata County, TX': '505', + 'Zavala County, TX': '507'}, + '49': {'Beaver County, UT': '001', + 'Box Elder County, UT': '003', + 'Cache County, UT': '005', + 'Carbon County, UT': '007', + 'Daggett County, UT': '009', + 'Davis County, UT': '011', + 'Duchesne County, UT': '013', + 'Emery County, UT': '015', + 'Garfield County, UT': '017', + 'Grand County, UT': '019', + 'Iron County, UT': '021', + 'Juab County, UT': '023', + 'Kane County, UT': '025', + 'Millard County, UT': '027', + 'Morgan County, UT': '029', + 'Piute County, UT': '031', + 'Rich County, UT': '033', + 'Salt Lake County, UT': '035', + 'San Juan County, UT': '037', + 'Sanpete County, UT': '039', + 'Sevier County, UT': '041', + 'Summit County, UT': '043', + 'Tooele County, UT': '045', + 'Uintah County, UT': '047', + 'Utah County, UT': '049', + 'Wasatch County, UT': '051', + 'Washington County, UT': '053', + 'Wayne County, UT': '055', + 'Weber County, UT': '057'}, + '50': {'Addison County, VT': '001', + 'Bennington County, VT': '003', + 'Caledonia County, VT': '005', + 'Chittenden County, VT': '007', + 'Essex County, VT': '009', + 'Franklin County, VT': '011', + 'Grand Isle County, VT': '013', + 'Lamoille County, VT': '015', + 'Orange County, VT': '017', + 'Orleans County, VT': '019', + 'Rutland County, VT': '021', + 'Washington County, VT': '023', + 'Windham County, VT': '025', + 'Windsor County, VT': '027'}, + '51': {'Accomack County, VA': '001', + 'Albemarle County, VA': '003', + 'Alexandria city, VA': '510', + 'Alleghany County, VA': '005', + 'Amelia County, VA': '007', + 'Amherst County, VA': '009', + 'Appomattox County, VA': '011', + 'Arlington County, VA': '013', + 'Augusta County, VA': '015', + 'Bath County, VA': '017', + 'Bedford County, VA': '019', + 'Bedford city, VA': '515', + 'Bland County, VA': '021', + 'Botetourt County, VA': '023', + 'Bristol city, VA': '520', + 'Brunswick County, VA': '025', + 'Buchanan County, VA': '027', + 'Buckingham County, VA': '029', + 'Buena Vista city, VA': '530', + 'Campbell County, VA': '031', + 'Caroline County, VA': '033', + 'Carroll County, VA': '035', + 'Charles City County, VA': '036', + 'Charlotte County, VA': '037', + 'Charlottesville city, VA': '540', + 'Chesapeake city, VA': '550', + 'Chesterfield County, VA': '041', + 'Clarke County, VA': '043', + 'Colonial Heights city, VA': '570', + 'Covington city, VA': '580', + 'Craig County, VA': '045', + 'Culpeper County, VA': '047', + 'Cumberland County, VA': '049', + 'Danville city, VA': '590', + 'Dickenson County, VA': '051', + 'Dinwiddie County, VA': '053', + 'Emporia city, VA': '595', + 'Essex County, VA': '057', + 'Fairfax County, VA': '059', + 'Fairfax city, VA': '600', + 'Falls Church city, VA': '610', + 'Fauquier County, VA': '061', + 'Floyd County, VA': '063', + 'Fluvanna County, VA': '065', + 'Franklin County, VA': '067', + 'Franklin city, VA': '620', + 'Frederick County, VA': '069', + 'Fredericksburg city, VA': '630', + 'Galax city, VA': '640', + 'Giles County, VA': '071', + 'Gloucester County, VA': '073', + 'Goochland County, VA': '075', + 'Grayson County, VA': '077', + 'Greene County, VA': '079', + 'Greensville County, VA': '081', + 'Halifax County, VA': '083', + 'Hampton city, VA': '650', + 'Hanover County, VA': '085', + 'Harrisonburg city, VA': '660', + 'Henrico County, VA': '087', + 'Henry County, VA': '089', + 'Highland County, VA': '091', + 'Hopewell city, VA': '670', + 'Isle of Wight County, VA': '093', + 'James City County, VA': '095', + 'King George County, VA': '099', + 'King William County, VA': '101', + 'King and Queen County, VA': '097', + 'Lancaster County, VA': '103', + 'Lee County, VA': '105', + 'Lexington city, VA': '678', + 'Loudoun County, VA': '107', + 'Louisa County, VA': '109', + 'Lunenburg County, VA': '111', + 'Lynchburg city, VA': '680', + 'Madison County, VA': '113', + 'Manassas Park city, VA': '685', + 'Manassas city, VA': '683', + 'Martinsville city, VA': '690', + 'Mathews County, VA': '115', + 'Mecklenburg County, VA': '117', + 'Middlesex County, VA': '119', + 'Montgomery County, VA': '121', + 'Nelson County, VA': '125', + 'New Kent County, VA': '127', + 'Newport News city, VA': '700', + 'Norfolk city, VA': '710', + 'Northampton County, VA': '131', + 'Northumberland County, VA': '133', + 'Norton city, VA': '720', + 'Nottoway County, VA': '135', + 'Orange County, VA': '137', + 'Page County, VA': '139', + 'Patrick County, VA': '141', + 'Petersburg city, VA': '730', + 'Pittsylvania County, VA': '143', + 'Poquoson city, VA': '735', + 'Portsmouth city, VA': '740', + 'Powhatan County, VA': '145', + 'Prince Edward County, VA': '147', + 'Prince George County, VA': '149', + 'Prince William County, VA': '153', + 'Pulaski County, VA': '155', + 'Radford city, VA': '750', + 'Rappahannock County, VA': '157', + 'Richmond County, VA': '159', + 'Richmond city, VA': '760', + 'Roanoke County, VA': '161', + 'Roanoke city, VA': '770', + 'Rockbridge County, VA': '163', + 'Rockingham County, VA': '165', + 'Russell County, VA': '167', + 'Salem city, VA': '775', + 'Scott County, VA': '169', + 'Shenandoah County, VA': '171', + 'Smyth County, VA': '173', + 'Southampton County, VA': '175', + 'Spotsylvania County, VA': '177', + 'Stafford County, VA': '179', + 'Staunton city, VA': '790', + 'Suffolk city, VA': '800', + 'Surry County, VA': '181', + 'Sussex County, VA': '183', + 'Tazewell County, VA': '185', + 'Virginia Beach city, VA': '810', + 'Warren County, VA': '187', + 'Washington County, VA': '191', + 'Waynesboro city, VA': '820', + 'Westmoreland County, VA': '193', + 'Williamsburg city, VA': '830', + 'Winchester city, VA': '840', + 'Wise County, VA': '195', + 'Wythe County, VA': '197', + 'York County, VA': '199'}, + '53': {'Adams County, WA': '001', + 'Asotin County, WA': '003', + 'Benton County, WA': '005', + 'Chelan County, WA': '007', + 'Clallam County, WA': '009', + 'Clark County, WA': '011', + 'Columbia County, WA': '013', + 'Cowlitz County, WA': '015', + 'Douglas County, WA': '017', + 'Ferry County, WA': '019', + 'Franklin County, WA': '021', + 'Garfield County, WA': '023', + 'Grant County, WA': '025', + 'Grays Harbor County, WA': '027', + 'Island County, WA': '029', + 'Jefferson County, WA': '031', + 'King County, WA': '033', + 'Kitsap County, WA': '035', + 'Kittitas County, WA': '037', + 'Klickitat County, WA': '039', + 'Lewis County, WA': '041', + 'Lincoln County, WA': '043', + 'Mason County, WA': '045', + 'Okanogan County, WA': '047', + 'Pacific County, WA': '049', + 'Pend Oreille County, WA': '051', + 'Pierce County, WA': '053', + 'San Juan County, WA': '055', + 'Skagit County, WA': '057', + 'Skamania County, WA': '059', + 'Snohomish County, WA': '061', + 'Spokane County, WA': '063', + 'Stevens County, WA': '065', + 'Thurston County, WA': '067', + 'Wahkiakum County, WA': '069', + 'Walla Walla County, WA': '071', + 'Whatcom County, WA': '073', + 'Whitman County, WA': '075', + 'Yakima County, WA': '077'}, + '54': {'Barbour County, WV': '001', + 'Berkeley County, WV': '003', + 'Boone County, WV': '005', + 'Braxton County, WV': '007', + 'Brooke County, WV': '009', + 'Cabell County, WV': '011', + 'Calhoun County, WV': '013', + 'Clay County, WV': '015', + 'Doddridge County, WV': '017', + 'Fayette County, WV': '019', + 'Gilmer County, WV': '021', + 'Grant County, WV': '023', + 'Greenbrier County, WV': '025', + 'Hampshire County, WV': '027', + 'Hancock County, WV': '029', + 'Hardy County, WV': '031', + 'Harrison County, WV': '033', + 'Jackson County, WV': '035', + 'Jefferson County, WV': '037', + 'Kanawha County, WV': '039', + 'Lewis County, WV': '041', + 'Lincoln County, WV': '043', + 'Logan County, WV': '045', + 'Marion County, WV': '049', + 'Marshall County, WV': '051', + 'Mason County, WV': '053', + 'McDowell County, WV': '047', + 'Mercer County, WV': '055', + 'Mineral County, WV': '057', + 'Mingo County, WV': '059', + 'Monongalia County, WV': '061', + 'Monroe County, WV': '063', + 'Morgan County, WV': '065', + 'Nicholas County, WV': '067', + 'Ohio County, WV': '069', + 'Pendleton County, WV': '071', + 'Pleasants County, WV': '073', + 'Pocahontas County, WV': '075', + 'Preston County, WV': '077', + 'Putnam County, WV': '079', + 'Raleigh County, WV': '081', + 'Randolph County, WV': '083', + 'Ritchie County, WV': '085', + 'Roane County, WV': '087', + 'Summers County, WV': '089', + 'Taylor County, WV': '091', + 'Tucker County, WV': '093', + 'Tyler County, WV': '095', + 'Upshur County, WV': '097', + 'Wayne County, WV': '099', + 'Webster County, WV': '101', + 'Wetzel County, WV': '103', + 'Wirt County, WV': '105', + 'Wood County, WV': '107', + 'Wyoming County, WV': '109'}, + '55': {'Adams County, WI': '001', + 'Ashland County, WI': '003', + 'Barron County, WI': '005', + 'Bayfield County, WI': '007', + 'Brown County, WI': '009', + 'Buffalo County, WI': '011', + 'Burnett County, WI': '013', + 'Calumet County, WI': '015', + 'Chippewa County, WI': '017', + 'Clark County, WI': '019', + 'Columbia County, WI': '021', + 'Crawford County, WI': '023', + 'Dane County, WI': '025', + 'Dodge County, WI': '027', + 'Door County, WI': '029', + 'Douglas County, WI': '031', + 'Dunn County, WI': '033', + 'Eau Claire County, WI': '035', + 'Florence County, WI': '037', + 'Fond du Lac County, WI': '039', + 'Forest County, WI': '041', + 'Grant County, WI': '043', + 'Green County, WI': '045', + 'Green Lake County, WI': '047', + 'Iowa County, WI': '049', + 'Iron County, WI': '051', + 'Jackson County, WI': '053', + 'Jefferson County, WI': '055', + 'Juneau County, WI': '057', + 'Kenosha County, WI': '059', + 'Kewaunee County, WI': '061', + 'La Crosse County, WI': '063', + 'Lafayette County, WI': '065', + 'Langlade County, WI': '067', + 'Lincoln County, WI': '069', + 'Manitowoc County, WI': '071', + 'Marathon County, WI': '073', + 'Marinette County, WI': '075', + 'Marquette County, WI': '077', + 'Menominee County, WI': '078', + 'Milwaukee County, WI': '079', + 'Monroe County, WI': '081', + 'Oconto County, WI': '083', + 'Oneida County, WI': '085', + 'Outagamie County, WI': '087', + 'Ozaukee County, WI': '089', + 'Pepin County, WI': '091', + 'Pierce County, WI': '093', + 'Polk County, WI': '095', + 'Portage County, WI': '097', + 'Price County, WI': '099', + 'Racine County, WI': '101', + 'Richland County, WI': '103', + 'Rock County, WI': '105', + 'Rusk County, WI': '107', + 'Sauk County, WI': '111', + 'Sawyer County, WI': '113', + 'Shawano County, WI': '115', + 'Sheboygan County, WI': '117', + 'St. Croix County, WI': '109', + 'Taylor County, WI': '119', + 'Trempealeau County, WI': '121', + 'Vernon County, WI': '123', + 'Vilas County, WI': '125', + 'Walworth County, WI': '127', + 'Washburn County, WI': '129', + 'Washington County, WI': '131', + 'Waukesha County, WI': '133', + 'Waupaca County, WI': '135', + 'Waushara County, WI': '137', + 'Winnebago County, WI': '139', + 'Wood County, WI': '141'}, + '56': {'Albany County, WY': '001', + 'Big Horn County, WY': '003', + 'Campbell County, WY': '005', + 'Carbon County, WY': '007', + 'Converse County, WY': '009', + 'Crook County, WY': '011', + 'Fremont County, WY': '013', + 'Goshen County, WY': '015', + 'Hot Springs County, WY': '017', + 'Johnson County, WY': '019', + 'Laramie County, WY': '021', + 'Lincoln County, WY': '023', + 'Natrona County, WY': '025', + 'Niobrara County, WY': '027', + 'Park County, WY': '029', + 'Platte County, WY': '031', + 'Sheridan County, WY': '033', + 'Sublette County, WY': '035', + 'Sweetwater County, WY': '037', + 'Teton County, WY': '039', + 'Uinta County, WY': '041', + 'Washakie County, WY': '043', + 'Weston County, WY': '045'}, + '72': {'Adjuntas Municipio, PR': '001', + 'Aguada Municipio, PR': '003', + 'Aguadilla Municipio, PR': '005', + 'Aguas Buenas Municipio, PR': '007', + 'Aibonito Municipio, PR': '009', + 'Anasco Municipio, PR': '011', + 'Arecibo Municipio, PR': '013', + 'Arroyo Municipio, PR': '015', + 'Barceloneta Municipio, PR': '017', + 'Barranquitas Municipio, PR': '019', + 'Bayamon Municipio, PR': '021', + 'Cabo Rojo Municipio, PR': '023', + 'Caguas Municipio, PR': '025', + 'Camuy Municipio, PR': '027', + 'Canovanas Municipio, PR': '029', + 'Carolina Municipio, PR': '031', + 'Catano Municipio, PR': '033', + 'Cayey Municipio, PR': '035', + 'Ceiba Municipio, PR': '037', + 'Ciales Municipio, PR': '039', + 'Cidra Municipio, PR': '041', + 'Coamo Municipio, PR': '043', + 'Comerio Municipio, PR': '045', + 'Corozal Municipio, PR': '047', + 'Culebra Municipio, PR': '049', + 'Dorado Municipio, PR': '051', + 'Fajardo Municipio, PR': '053', + 'Florida Municipio, PR': '054', + 'Guanica Municipio, PR': '055', + 'Guayama Municipio, PR': '057', + 'Guayanilla Municipio, PR': '059', + 'Guaynabo Municipio, PR': '061', + 'Gurabo Municipio, PR': '063', + 'Hatillo Municipio, PR': '065', + 'Hormigueros Municipio, PR': '067', + 'Humacao Municipio, PR': '069', + 'Isabela Municipio, PR': '071', + 'Jayuya Municipio, PR': '073', + 'Juana Diaz Municipio, PR': '075', + 'Juncos Municipio, PR': '077', + 'Lajas Municipio, PR': '079', + 'Lares Municipio, PR': '081', + 'Las Marias Municipio, PR': '083', + 'Las Piedras Municipio, PR': '085', + 'Loiza Municipio, PR': '087', + 'Luquillo Municipio, PR': '089', + 'Manati Municipio, PR': '091', + 'Maricao Municipio, PR': '093', + 'Maunabo Municipio, PR': '095', + 'Mayaguez Municipio, PR': '097', + 'Moca Municipio, PR': '099', + 'Morovis Municipio, PR': '101', + 'Naguabo Municipio, PR': '103', + 'Naranjito Municipio, PR': '105', + 'Orocovis Municipio, PR': '107', + 'Patillas Municipio, PR': '109', + 'Penuelas Municipio, PR': '111', + 'Ponce Municipio, PR': '113', + 'Quebradillas Municipio, PR': '115', + 'Rincon Municipio, PR': '117', + 'Rio Grande Municipio, PR': '119', + 'Sabana Grande Municipio, PR': '121', + 'Salinas Municipio, PR': '123', + 'San German Municipio, PR': '125', + 'San Juan Municipio, PR': '127', + 'San Lorenzo Municipio, PR': '129', + 'San Sebastian Municipio, PR': '131', + 'Santa Isabel Municipio, PR': '133', + 'Toa Alta Municipio, PR': '135', + 'Toa Baja Municipio, PR': '137', + 'Trujillo Alto Municipio, PR': '139', + 'Utuado Municipio, PR': '141', + 'Vega Alta Municipio, PR': '143', + 'Vega Baja Municipio, PR': '145', + 'Vieques Municipio, PR': '147', + 'Villalba Municipio, PR': '149', + 'Yabucoa Municipio, PR': '151', + 'Yauco Municipio, PR': '153'}, + "CA01": {'--All--': '%', }, + "CA02": {'--All--': '%', }, + "CA03": {'--All--': '%', }, + "CA04": {'--All--': '%', }, + "CA05": {'--All--': '%', }, + "CA13": {'--All--': '%', }, + "CA07": {'--All--': '%', }, + "CA14": {'--All--': '%', }, + "CA08": {'--All--': '%', }, + "CA09": {'--All--': '%', }, + "CA10": {'--All--': '%', }, + "CA11": {'--All--': '%', }, + "CA12": {'--All--': '%', }, +} + + +if __name__ == "__main__": + from sys import argv + from pprint import PrettyPrinter + pp = PrettyPrinter(indent=2) + + import csv + fipsreader = csv.reader(open(argv[1], 'rb'), delimiter=',', quotechar='"') + for row in fipsreader: + try: + FIPS_COUNTIES[int(row[1])][row[3]] = row[2] + except KeyError: + FIPS_COUNTIES[int(row[1])] = {'--All--': '%'} + FIPS_COUNTIES[int(row[1])][row[3]] = row[2] + + pp.pprint(FIPS_COUNTIES) diff --git a/chirp/ui/importdialog.py b/chirp/ui/importdialog.py new file mode 100644 index 0000000..7a52852 --- /dev/null +++ b/chirp/ui/importdialog.py @@ -0,0 +1,652 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +import pango +import logging + +from chirp import errors, chirp_common, import_logic +from chirp.ui import common + +LOG = logging.getLogger(__name__) + + +class WaitWindow(gtk.Window): + def __init__(self, msg, parent=None): + gtk.Window.__init__(self) + self.set_title("Please Wait") + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + if parent: + self.set_transient_for(parent) + self.set_position(gtk.WIN_POS_CENTER_ON_PARENT) + else: + self.set_position(gtk.WIN_POS_CENTER) + + vbox = gtk.VBox(False, 2) + + l = gtk.Label(msg) + l.show() + vbox.pack_start(l) + + self.prog = gtk.ProgressBar() + self.prog.show() + vbox.pack_start(self.prog) + + vbox.show() + self.add(vbox) + + def grind(self): + while gtk.events_pending(): + gtk.main_iteration(False) + + self.prog.pulse() + + def set(self, fraction): + while gtk.events_pending(): + gtk.main_iteration(False) + + self.prog.set_fraction(fraction) + + +class ImportMemoryBankJob(common.RadioJob): + def __init__(self, cb, dst_mem, src_radio, src_mem): + common.RadioJob.__init__(self, cb, None) + self.__dst_mem = dst_mem + self.__src_radio = src_radio + self.__src_mem = src_mem + + def execute(self, radio): + import_logic.import_bank(radio, self.__src_radio, + self.__dst_mem, self.__src_mem) + if self.cb: + gobject.idle_add(self.cb, *self.cb_args) + + +class ImportDialog(gtk.Dialog): + + def _check_for_dupe(self, location): + iter = self.__store.get_iter_first() + while iter: + imp, loc = self.__store.get(iter, self.col_import, self.col_nloc) + if imp and loc == location: + return True + iter = self.__store.iter_next(iter) + + return False + + def _toggle(self, rend, path, col): + iter = self.__store.get_iter(path) + imp, nloc = self.__store.get(iter, self.col_import, self.col_nloc) + if not imp and self._check_for_dupe(nloc): + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_property("text", + _("Location {number} is already being imported. " + "Choose another value for 'New Location' " + "before selection 'Import'").format(number=nloc)) + d.run() + d.destroy() + else: + self.__store[path][col] = not imp + + def _render(self, _, rend, model, iter, colnum): + newloc, imp = model.get(iter, self.col_nloc, self.col_import) + lo, hi = self.dst_radio.get_features().memory_bounds + + rend.set_property("text", "%i" % newloc) + if newloc in self.used_list and imp: + rend.set_property("foreground", "goldenrod") + rend.set_property("weight", pango.WEIGHT_BOLD) + elif newloc < lo or newloc > hi: + rend.set_property("foreground", "red") + rend.set_property("weight", pango.WEIGHT_BOLD) + else: + rend.set_property("foreground", "black") + rend.set_property("weight", pango.WEIGHT_NORMAL) + + def _edited(self, rend, path, new, col): + iter = self.__store.get_iter(path) + + if col == self.col_nloc: + nloc, = self.__store.get(iter, self.col_nloc) + + try: + val = int(new) + except ValueError: + common.show_error(_("Invalid value. Must be an integer.")) + return + + if val == nloc: + return + + if self._check_for_dupe(val): + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_property("text", + _("Location {number} is already being " + "imported").format(number=val)) + d.run() + d.destroy() + return + + self.record_use_of(val) + + elif col == self.col_name or col == self.col_comm: + val = str(new) + + else: + return + + self.__store.set(iter, col, val) + + def get_import_list(self): + import_list = [] + iter = self.__store.get_iter_first() + while iter: + old, new, name, comm, enb = \ + self.__store.get(iter, self.col_oloc, self.col_nloc, + self.col_name, self.col_comm, self.col_import) + if enb: + import_list.append((old, new, name, comm)) + iter = self.__store.iter_next(iter) + + return import_list + + def ensure_calls(self, dst_rthread, import_list): + rlist_changed = False + ulist_changed = False + + if not isinstance(self.dst_radio, chirp_common.IcomDstarSupport): + return + + ulist = self.dst_radio.get_urcall_list() + rlist = self.dst_radio.get_repeater_call_list() + + for old, new in import_list: + mem = self.src_radio.get_memory(old) + if isinstance(mem, chirp_common.DVMemory): + if mem.dv_urcall not in ulist: + LOG.debug("Adding %s to ucall list" % mem.dv_urcall) + ulist.append(mem.dv_urcall) + ulist_changed = True + if mem.dv_rpt1call not in rlist: + LOG.debug("Adding %s to rcall list" % mem.dv_rpt1call) + rlist.append(mem.dv_rpt1call) + rlist_changed = True + if mem.dv_rpt2call not in rlist: + LOG.debug("Adding %s to rcall list" % mem.dv_rpt2call) + rlist.append(mem.dv_rpt2call) + rlist_changed = True + + if ulist_changed: + job = common.RadioJob(None, "set_urcall_list", ulist) + job.set_desc(_("Updating URCALL list")) + dst_rthread._qsubmit(job, 0) + + if rlist_changed: + job = common.RadioJob(None, "set_repeater_call_list", ulist) + job.set_desc(_("Updating RPTCALL list")) + dst_rthread._qsubmit(job, 0) + + return + + def _convert_power(self, dst_levels, src_levels, mem): + if not dst_levels: + mem.power = None + return + elif not mem.power: + # Source radio does not support power levels, so choose the + # first (highest) level from the destination radio. + mem.power = dst_levels[0] + return "" + + # If both radios support power levels, we need to decide how to + # convert the source power level to a valid one for the destination + # radio. To do that, find the absolute level of the source value + # and calculate the different between it and all the levels of the + # destination, choosing the one that matches most closely. + + deltas = [abs(mem.power - power) for power in dst_levels] + mem.power = dst_levels[deltas.index(min(deltas))] + + def do_soft_conversions(self, dst_features, src_features, mem): + self._convert_power(dst_features.valid_power_levels, + src_features.valid_power_levels, + mem) + + return mem + + def do_import_banks(self): + try: + dst_banks = self.dst_radio.get_banks() + src_banks = self.src_radio.get_banks() + if not dst_banks or not src_banks: + raise Exception() + except Exception: + LOG.error("One or more of the radios doesn't support banks") + return + + if not len(dst_banks) != len(src_banks): + LOG.warn("Source and destination radios have " + "a different number of banks") + else: + self.dst_radio.set_banks(src_banks) + + def do_import(self, dst_rthread): + i = 0 + error_messages = {} + import_list = self.get_import_list() + + src_features = self.src_radio.get_features() + + for old, new, name, comm in import_list: + i += 1 + LOG.debug("%sing %i -> %i" % (self.ACTION, old, new)) + + src = self.src_radio.get_memory(old) + + try: + mem = import_logic.import_mem(self.dst_radio, + src_features, + src, + {"number": new, + "name": name, + "comment": comm}) + except import_logic.ImportError as e: + LOG.error("Import error: %s", e) + error_messages[new] = str(e) + continue + + job = common.RadioJob(None, "set_memory", mem) + desc = _("Setting memory {number}").format(number=mem.number) + job.set_desc(desc) + dst_rthread._qsubmit(job, 0) + + job = ImportMemoryBankJob(None, mem, self.src_radio, src) + job.set_desc(_("Importing bank information")) + dst_rthread._qsubmit(job, 0) + + if list(error_messages.keys()): + msg = _("Error importing memories:") + "\r\n" + for num, msgs in list(error_messages.items()): + msg += "%s: %s" % (num, ",".join(msgs)) + common.show_error(msg) + + return i + + def make_view(self): + editable = [self.col_nloc, self.col_name, self.col_comm] + + self.__store = gtk.ListStore(gobject.TYPE_BOOLEAN, # Import + gobject.TYPE_INT, # Source loc + gobject.TYPE_INT, # Destination loc + gobject.TYPE_STRING, # Name + gobject.TYPE_STRING, # Frequency + gobject.TYPE_STRING, # Comment + gobject.TYPE_BOOLEAN, + gobject.TYPE_STRING) + self.__view = gtk.TreeView(self.__store) + self.__view.show() + + tips = gtk.Tooltips() + + for k in list(self.caps.keys()): + t = self.types[k] + + if t == gobject.TYPE_BOOLEAN: + rend = gtk.CellRendererToggle() + rend.connect("toggled", self._toggle, k) + column = gtk.TreeViewColumn(self.caps[k], rend, + active=k, + sensitive=self.col_okay, + activatable=self.col_okay) + else: + rend = gtk.CellRendererText() + if k in editable: + rend.set_property("editable", True) + rend.connect("edited", self._edited, k) + column = gtk.TreeViewColumn(self.caps[k], rend, + text=k, + sensitive=self.col_okay) + + if k == self.col_nloc: + column.set_cell_data_func(rend, self._render, k) + + if k in list(self.tips.keys()): + LOG.debug("Doing %s" % k) + lab = gtk.Label(self.caps[k]) + column.set_widget(lab) + tips.set_tip(lab, self.tips[k]) + lab.show() + column.set_sort_column_id(k) + self.__view.append_column(column) + + self.__view.set_tooltip_column(self.col_tmsg) + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add(self.__view) + sw.show() + + return sw + + def __select_all(self, button, state): + iter = self.__store.get_iter_first() + while iter: + _state, okay, = self.__store.get(iter, + self.col_import, + self.col_okay) + if state is None: + _state = not _state and okay + else: + _state = state and okay + self.__store.set(iter, self.col_import, _state) + iter = self.__store.iter_next(iter) + + def __incrnew(self, button, delta): + iter = self.__store.get_iter_first() + while iter: + pos = self.__store.get(iter, self.col_nloc)[0] + pos += delta + if pos < 0: + pos = 0 + self.__store.set(iter, self.col_nloc, pos) + iter = self.__store.iter_next(iter) + + def __autonew(self, button): + pos = self.dst_radio.get_features().memory_bounds[0] + iter = self.__store.get_iter_first() + while iter: + selected, okay = self.__store.get(iter, + self.col_import, self.col_okay) + if selected and okay: + self.__store.set(iter, self.col_nloc, pos) + pos += 1 + iter = self.__store.iter_next(iter) + + def __revrnew(self, button): + positions = [] + iter = self.__store.get_iter_first() + while iter: + positions.append(self.__store.get(iter, self.col_nloc)[0]) + iter = self.__store.iter_next(iter) + + iter = self.__store.get_iter_first() + while iter: + self.__store.set(iter, self.col_nloc, positions.pop()) + iter = self.__store.iter_next(iter) + + def make_select(self): + hbox = gtk.HBox(True, 2) + + all = gtk.Button(_("All")) + all.connect("clicked", self.__select_all, True) + all.set_size_request(50, 25) + all.show() + hbox.pack_start(all, 0, 0, 0) + + none = gtk.Button(_("None")) + none.connect("clicked", self.__select_all, False) + none.set_size_request(50, 25) + none.show() + hbox.pack_start(none, 0, 0, 0) + + inv = gtk.Button(_("Inverse")) + inv.connect("clicked", self.__select_all, None) + inv.set_size_request(50, 25) + inv.show() + hbox.pack_start(inv, 0, 0, 0) + + frame = gtk.Frame(_("Select")) + frame.show() + frame.add(hbox) + hbox.show() + + return frame + + def make_adjust(self): + hbox = gtk.HBox(True, 2) + + incr = gtk.Button("+100") + incr.connect("clicked", self.__incrnew, 100) + incr.set_size_request(50, 25) + incr.show() + hbox.pack_start(incr, 0, 0, 0) + + incr = gtk.Button("+10") + incr.connect("clicked", self.__incrnew, 10) + incr.set_size_request(50, 25) + incr.show() + hbox.pack_start(incr, 0, 0, 0) + + incr = gtk.Button("+1") + incr.connect("clicked", self.__incrnew, 1) + incr.set_size_request(50, 25) + incr.show() + hbox.pack_start(incr, 0, 0, 0) + + decr = gtk.Button("-1") + decr.connect("clicked", self.__incrnew, -1) + decr.set_size_request(50, 25) + decr.show() + hbox.pack_start(decr, 0, 0, 0) + + decr = gtk.Button("-10") + decr.connect("clicked", self.__incrnew, -10) + decr.set_size_request(50, 25) + decr.show() + hbox.pack_start(decr, 0, 0, 0) + + decr = gtk.Button("-100") + decr.connect("clicked", self.__incrnew, -100) + decr.set_size_request(50, 25) + decr.show() + hbox.pack_start(decr, 0, 0, 0) + + auto = gtk.Button(_("Auto")) + auto.connect("clicked", self.__autonew) + auto.set_size_request(50, 25) + auto.show() + hbox.pack_start(auto, 0, 0, 0) + + revr = gtk.Button(_("Reverse")) + revr.connect("clicked", self.__revrnew) + revr.set_size_request(50, 25) + revr.show() + hbox.pack_start(revr, 0, 0, 0) + + frame = gtk.Frame(_("Adjust New Location")) + frame.show() + frame.add(hbox) + hbox.show() + + return frame + + def make_options(self): + hbox = gtk.HBox(True, 2) + + confirm = gtk.CheckButton(_("Confirm overwrites")) + confirm.connect("toggled", __set_confirm) + confirm.show() + + hbox.pack_start(confirm, 0, 0, 0) + + frame = gtk.Frame(_("Options")) + frame.add(hbox) + frame.show() + hbox.show() + + return frame + + def make_controls(self): + hbox = gtk.HBox(False, 2) + + hbox.pack_start(self.make_select(), 0, 0, 0) + hbox.pack_start(self.make_adjust(), 0, 0, 0) + # hbox.pack_start(self.make_options(), 0, 0, 0) + hbox.show() + + return hbox + + def build_ui(self): + self.vbox.pack_start(self.make_view(), 1, 1, 1) + self.vbox.pack_start(self.make_controls(), 0, 0, 0) + + def record_use_of(self, number): + lo, hi = self.dst_radio.get_features().memory_bounds + + if number < lo or number > hi: + return + + try: + mem = self.dst_radio.get_memory(number) + if mem and not mem.empty and number not in self.used_list: + self.used_list.append(number) + except errors.InvalidMemoryLocation: + LOG.error("Location %i empty or at limit of destination radio" % + number) + except errors.InvalidDataError as e: + LOG.error("Got error from radio, assuming %i beyond limits: %s" % + (number, e)) + + def populate_list(self): + start, end = self.src_radio.get_features().memory_bounds + for i in range(start, end+1): + if end > 50 and i % (end/50) == 0: + self.ww.set(float(i) / end) + try: + mem = self.src_radio.get_memory(i) + except errors.InvalidMemoryLocation as e: + continue + except Exception as e: + self.__store.append(row=(False, + i, + i, + "ERROR", + chirp_common.format_freq(0), + "", + False, + str(e), + )) + self.record_use_of(i) + continue + if mem.empty: + continue + + self.ww.set(float(i) / end) + try: + msgs = self.dst_radio.validate_memory( + import_logic.import_mem(self.dst_radio, + self.src_radio.get_features(), + mem)) + except import_logic.DestNotCompatible: + msgs = self.dst_radio.validate_memory(mem) + errs = [x for x in msgs + if isinstance(x, chirp_common.ValidationError)] + if errs: + msg = _("Cannot be imported because") + ":\r\n" + msg += ",".join(errs) + else: + errs = [] + msg = "Memory can be imported into target" + + self.__store.append(row=(not bool(msgs), + mem.number, + mem.number, + mem.name, + chirp_common.format_freq(mem.freq), + mem.comment, + not bool(errs), + msg + )) + self.record_use_of(mem.number) + + TITLE = _("Import From File") + ACTION = _("Import") + + def __init__(self, src_radio, dst_radio, parent=None): + gtk.Dialog.__init__(self, + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + title=self.TITLE, + parent=parent) + + self.col_import = 0 + self.col_nloc = 1 + self.col_oloc = 2 + self.col_name = 3 + self.col_freq = 4 + self.col_comm = 5 + self.col_okay = 6 + self.col_tmsg = 7 + + self.caps = { + self.col_import: self.ACTION, + self.col_nloc: _("To"), + self.col_oloc: _("From"), + self.col_name: _("Name"), + self.col_freq: _("Frequency"), + self.col_comm: _("Comment"), + } + + self.tips = { + self.col_nloc: _("Location memory will be imported into"), + self.col_oloc: _("Location of memory in the file being imported"), + } + + self.types = { + self.col_import: gobject.TYPE_BOOLEAN, + self.col_oloc: gobject.TYPE_INT, + self.col_nloc: gobject.TYPE_INT, + self.col_name: gobject.TYPE_STRING, + self.col_freq: gobject.TYPE_STRING, + self.col_comm: gobject.TYPE_STRING, + self.col_okay: gobject.TYPE_BOOLEAN, + self.col_tmsg: gobject.TYPE_STRING, + } + + self.src_radio = src_radio + self.dst_radio = dst_radio + + self.used_list = [] + self.not_used_list = [] + + self.build_ui() + self.set_default_size(600, 400) + + self.ww = WaitWindow(_("Preparing memory list..."), parent=parent) + self.ww.show() + self.ww.grind() + + self.populate_list() + + self.ww.hide() + + +class ExportDialog(ImportDialog): + TITLE = _("Export To File") + ACTION = _("Export") + +if __name__ == "__main__": + from chirp.ui import editorset + import sys + + f = sys.argv[1] + rc = editorset.radio_class_from_file(f) + radio = rc(f) + + d = ImportDialog(radio) + d.run() + + print(d.get_import_list()) diff --git a/chirp/ui/inputdialog.py b/chirp/ui/inputdialog.py new file mode 100644 index 0000000..29a2971 --- /dev/null +++ b/chirp/ui/inputdialog.py @@ -0,0 +1,157 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import logging + +from chirp.ui.miscwidgets import make_choice +from chirp.ui import reporting + +LOG = logging.getLogger(__name__) + + +class TextInputDialog(gtk.Dialog): + def respond_ok(self, _): + self.response(gtk.RESPONSE_OK) + + def __init__(self, **args): + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + gtk.Dialog.__init__(self, buttons=buttons, **args) + + self.label = gtk.Label() + self.label.set_size_request(300, 100) + # pylint: disable-msg=E1101 + self.vbox.pack_start(self.label, 1, 1, 0) + + self.text = gtk.Entry() + self.text.connect("activate", self.respond_ok, None) + # pylint: disable-msg=E1101 + self.vbox.pack_start(self.text, 1, 1, 0) + + self.label.show() + self.text.show() + + +class ChoiceDialog(gtk.Dialog): + editable = False + + def __init__(self, choices, **args): + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + gtk.Dialog.__init__(self, buttons=buttons, **args) + + self.label = gtk.Label() + self.label.set_size_request(300, 100) + # pylint: disable-msg=E1101 + self.vbox.pack_start(self.label, 1, 1, 0) + self.label.show() + + try: + default = choices[0] + except IndexError: + default = None + + self.choice = make_choice(sorted(choices), self.editable, default) + # pylint: disable-msg=E1101 + self.vbox.pack_start(self.choice, 1, 1, 0) + self.choice.show() + + self.set_default_response(gtk.RESPONSE_OK) + + +class EditableChoiceDialog(ChoiceDialog): + editable = True + + def __init__(self, choices, **args): + ChoiceDialog.__init__(self, choices, **args) + + self.choice.child.set_activates_default(True) + + +class ExceptionDialog(gtk.MessageDialog): + def __init__(self, exception, **args): + gtk.MessageDialog.__init__(self, buttons=gtk.BUTTONS_OK, + type=gtk.MESSAGE_ERROR, **args) + self.set_property("text", _("An error has occurred")) + self.format_secondary_text(str(exception)) + + import traceback + import sys + reporting.report_exception(traceback.format_exc(limit=30)) + LOG.error("--- Exception Dialog: %s ---" % exception) + LOG.error(traceback.format_exc(limit=100)) + LOG.error("----------------------------") + + +class FieldDialog(gtk.Dialog): + def __init__(self, **kwargs): + if "buttons" not in kwargs.keys(): + kwargs["buttons"] = (gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + + self.__fields = {} + self.set_default_response(gtk.RESPONSE_OK) + + gtk.Dialog.__init__(self, **kwargs) + + def response(self, _): + LOG.debug("Blocking response") + return + + def add_field(self, label, widget, validator=None): + box = gtk.HBox(True, 2) + + lab = gtk.Label(label) + lab.show() + + widget.set_size_request(150, -1) + widget.show() + + box.pack_start(lab, 0, 0, 0) + box.pack_start(widget, 0, 0, 0) + box.show() + + # pylint: disable-msg=E1101 + self.vbox.pack_start(box, 0, 0, 0) + + self.__fields[label] = widget + + def get_field(self, label): + return self.__fields.get(label, None) + + +class OverwriteDialog(gtk.MessageDialog): + def __init__(self, filename): + gtk.Dialog.__init__(self, + buttons=(_("Overwrite"), gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + + self.set_property("text", _("File Exists")) + + text = \ + _("The file {name} already exists. " + "Do you want to overwrite it?").format(name=filename) + + self.format_secondary_text(text) + +if __name__ == "__main__": + # pylint: disable-msg=C0103 + d = FieldDialog(buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK)) + d.add_field("Foo", gtk.Entry()) + d.add_field("Bar", make_choice(["A", "B"])) + d.run() + gtk.main() + d.destroy() diff --git a/chirp/ui/mainapp.py b/chirp/ui/mainapp.py new file mode 100644 index 0000000..4f8db4a --- /dev/null +++ b/chirp/ui/mainapp.py @@ -0,0 +1,2177 @@ +# Copyright 2008 Dan Smith +# Copyright 2012 Tom Hayward +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from datetime import datetime +import os +import tempfile +import webbrowser +from glob import glob +import shutil +import time +import logging +import gtk +import gobject +import sys + +from chirp.ui import inputdialog, common +from chirp.ui import compat +from chirp import platform, directory, util +from chirp.drivers import generic_csv, repeaterbook +from chirp.drivers import ic9x, kenwood_live, idrp, vx7, vx5, vx6 +from chirp.drivers import icf, ic9x_icf +from chirp import CHIRP_VERSION, chirp_common, detect, errors +from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips +from chirp.ui import bandplans + +gobject.threads_init() + +LOG = logging.getLogger(__name__) + +if __name__ == "__main__": + sys.path.insert(0, "..") + +try: + import serial +except ImportError as e: + common.log_exception() + common.show_error("\nThe Pyserial module is not installed!") + + +CONF = config.get() + +KEEP_RECENT = 8 + +RB_BANDS = { + "--All--": 0, + "10 meters (29MHz)": 29, + "6 meters (54MHz)": 5, + "2 meters (144MHz)": 14, + "1.25 meters (220MHz)": 22, + "70 centimeters (440MHz)": 4, + "33 centimeters (900MHz)": 9, + "23 centimeters (1.2GHz)": 12, +} + + +def key_bands(band): + if band.startswith("-"): + return -1 + + amount, units, mhz = band.split(" ") + scale = units == "meters" and 100 or 1 + + return 100000 - (float(amount) * scale) + + +class ModifiedError(Exception): + pass + + +class ChirpMain(gtk.Window): + + def get_current_editorset(self): + page = self.tabs.get_current_page() + if page is not None: + return self.tabs.get_nth_page(page) + else: + return None + + def ev_tab_switched(self, pagenum=None): + def set_action_sensitive(action, sensitive): + self.menu_ag.get_action(action).set_sensitive(sensitive) + + if pagenum is not None: + eset = self.tabs.get_nth_page(pagenum) + else: + eset = self.get_current_editorset() + + upload_sens = bool(eset and + isinstance(eset.radio, chirp_common.CloneModeRadio)) + + if not eset or isinstance(eset.radio, chirp_common.LiveRadio): + save_sens = False + elif isinstance(eset.radio, chirp_common.NetworkSourceRadio): + save_sens = False + else: + save_sens = True + + for i in ["import", "importsrc", "stock"]: + set_action_sensitive(i, + eset is not None and not eset.get_read_only()) + + for i in ["save", "saveas"]: + set_action_sensitive(i, save_sens) + + for i in ["upload"]: + set_action_sensitive(i, upload_sens) + + for i in ["cancelq"]: + set_action_sensitive(i, eset is not None and not save_sens) + + for i in ["export", "close", "columns", "irbook", "irfinder", + "move_up", "move_dn", "exchange", "iradioreference", + "cut", "copy", "paste", "delete", "viewdeveloper", + "all", "properties"]: + set_action_sensitive(i, eset is not None) + + def ev_status(self, editorset, msg): + self.sb_radio.pop(0) + self.sb_radio.push(0, msg) + + def ev_usermsg(self, editorset, msg): + self.sb_general.pop(0) + self.sb_general.push(0, msg) + + def ev_editor_selected(self, editorset, editortype): + mappings = { + "memedit": ["view", "edit"], + } + + for _editortype, actions in mappings.items(): + for _action in actions: + action = self.menu_ag.get_action(_action) + action.set_sensitive(editortype.startswith(_editortype)) + + def _connect_editorset(self, eset): + eset.connect("want-close", self.do_close) + eset.connect("status", self.ev_status) + eset.connect("usermsg", self.ev_usermsg) + eset.connect("editor-selected", self.ev_editor_selected) + + def do_diff_radio(self): + if self.tabs.get_n_pages() < 2: + common.show_error("Diff tabs requires at least two open tabs!") + return + + esets = [] + for i in range(0, self.tabs.get_n_pages()): + esets.append(self.tabs.get_nth_page(i)) + + d = gtk.Dialog(title="Diff Radios", + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + parent=self) + + label = gtk.Label("") + label.set_markup("-1 for either Mem # does a full-file hex " + + "dump with diffs highlighted.\n" + + "-2 for first Mem # shows " + + "only the diffs.") + d.vbox.pack_start(label, True, True, 0) + label.show() + + choices = [] + for eset in esets: + choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR, + eset.rthread.radio.MODEL, + eset.filename)) + choice_a = miscwidgets.make_choice(choices, False, choices[0]) + choice_a.show() + chan_a = gtk.SpinButton() + chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0) + chan_a.show() + hbox = gtk.HBox(False, 3) + hbox.pack_start(choice_a, 1, 1, 1) + hbox.pack_start(chan_a, 0, 0, 0) + hbox.show() + d.vbox.pack_start(hbox, 0, 0, 0) + + choice_b = miscwidgets.make_choice(choices, False, choices[1]) + choice_b.show() + chan_b = gtk.SpinButton() + chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0) + chan_b.show() + hbox = gtk.HBox(False, 3) + hbox.pack_start(choice_b, 1, 1, 1) + hbox.pack_start(chan_b, 0, 0, 0) + hbox.show() + d.vbox.pack_start(hbox, 0, 0, 0) + + r = d.run() + sel_a = choice_a.get_active_text() + sel_chan_a = chan_a.get_value() + sel_b = choice_b.get_active_text() + sel_chan_b = chan_b.get_value() + d.destroy() + if r == gtk.RESPONSE_CANCEL: + return + + if sel_a == sel_b: + common.show_error("Can't diff the same tab!") + return + + LOG.debug("Selected %s@%i and %s@%i" % + (sel_a, sel_chan_a, sel_b, sel_chan_b)) + name_a = os.path.basename(sel_a) + name_a = name_a[:name_a.rindex(")")] + name_b = os.path.basename(sel_b) + name_b = name_b[:name_b.rindex(")")] + diffwintitle = "%s@%i diff %s@%i" % ( + name_a, sel_chan_a, name_b, sel_chan_b) + + eset_a = esets[choices.index(sel_a)] + eset_b = esets[choices.index(sel_b)] + + def _show_diff(mem_b, mem_a): + # Step 3: Show the diff + diff = common.simple_diff(mem_a, mem_b) + common.show_diff_blob(diffwintitle, diff) + + def _get_mem_b(mem_a): + # Step 2: Get memory b + job = common.RadioJob(_show_diff, "get_raw_memory", + int(sel_chan_b)) + job.set_cb_args(mem_a) + eset_b.rthread.submit(job) + + if sel_chan_a >= 0 and sel_chan_b >= 0: + # Diff numbered memory + # Step 1: Get memory a + job = common.RadioJob(_get_mem_b, "get_raw_memory", + int(sel_chan_a)) + eset_a.rthread.submit(job) + elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\ + isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio): + # Diff whole (can do this without a job, since both are clone-mode) + try: + addrfmt = CONF.get('hexdump_addrfmt', section='developer', + raw=True) + except: + pass + a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(), + addrfmt=addrfmt) + b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(), + addrfmt=addrfmt) + if sel_chan_a == -2: + diffsonly = True + else: + diffsonly = False + common.show_diff_blob(diffwintitle, + common.simple_diff(a, b, diffsonly)) + else: + common.show_error("Cannot diff whole live-mode radios!") + + def do_new(self): + eset = editorset.EditorSet(_("Untitled") + ".csv", self) + self._connect_editorset(eset) + eset.prime() + eset.show() + + tab = self.tabs.append_page(eset, eset.get_tab_label()) + self.tabs.set_current_page(tab) + + def _do_manual_select(self, filename): + radiolist = {} + for drv, radio in directory.DRV_TO_RADIO.items(): + if not issubclass(radio, chirp_common.CloneModeRadio): + continue + radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv + + lab = gtk.Label("""Unable to detect model! + +If you think that it is valid, you can select a radio model below to +force an open attempt. If selecting the model manually works, please +file a bug on the website and attach your image. If selecting the model +does not work, it is likely that you are trying to open some other type +of file. +""") + + lab.set_justify(gtk.JUSTIFY_FILL) + lab.set_line_wrap(True) + lab.set_use_markup(True) + lab.show() + choice = miscwidgets.make_choice(sorted(radiolist.keys()), False, + sorted(radiolist.keys())[0]) + d = gtk.Dialog(title="Detection Failed", + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + d.vbox.pack_start(lab, 0, 0, 0) + d.vbox.pack_start(choice, 0, 0, 0) + d.vbox.set_spacing(5) + choice.show() + d.set_default_size(400, 200) + # d.set_resizable(False) + r = d.run() + d.destroy() + if r != gtk.RESPONSE_OK: + return + try: + rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]] + return rc(filename) + except: + return + + def do_open(self, fname=None, tempname=None): + if not fname: + types = [(_("All files") + " (*.*)", "*"), + (_("CHIRP Radio Images") + " (*.img)", "*.img"), + (_("CHIRP Files") + " (*.chirp)", "*.chirp"), + (_("CSV Files") + " (*.csv)", "*.csv"), + (_("DAT Files") + " (*.dat)", "*.dat"), + (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), + (_("ICF Files") + " (*.icf)", "*.icf"), + (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), + (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), + (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"), + ] + fname = platform.get_platform().gui_open_file(types=types) + if not fname: + return + + self.record_recent_file(fname) + + if icf.is_icf_file(fname): + a = common.ask_yesno_question( + _("ICF files cannot be edited, only displayed or imported " + "into another file. Open in read-only mode?"), + self) + if not a: + return + read_only = True + else: + read_only = False + + if icf.is_9x_icf(fname): + # We have to actually instantiate the IC9xICFRadio to get its + # sub-devices + radio = ic9x_icf.IC9xICFRadio(fname) + else: + try: + radio = directory.get_radio_by_image(fname) + except errors.ImageMetadataInvalidModel as e: + version = e.metadata.get('chirp_version') + if version: + newer = chirp_common.is_version_newer(version) + LOG.error('Image is from newer CHIRP with a model we ' + 'do not support') + common.show_error( + _('Unable to open this image. It was generated ' + 'with a newer version of CHIRP and thus may ' + 'be for a radio model that is not supported ' + 'by this version. Please update to the latest ' + 'version of CHIRP and try again.')) + else: + LOG.error('Image has metadata but has no chirp_version ' + 'and we do not support the model') + common.show_error( + _('Unable to open this image: unsupported model')) + return + except errors.ImageDetectFailed: + radio = self._do_manual_select(fname) + if not radio: + return + LOG.debug("Manually selected %s" % radio) + except Exception as e: + common.log_exception() + common.show_error(os.path.basename(fname) + ": " + str(e)) + return + + first_tab = False + try: + eset = editorset.EditorSet(radio, self, + filename=fname, + tempname=tempname) + except Exception as e: + common.log_exception() + common.show_error( + _("There was an error opening {fname}: {error}").format( + fname=fname, + error=e)) + return + + eset.set_read_only(read_only) + self._connect_editorset(eset) + eset.show() + self.tabs.append_page(eset, eset.get_tab_label()) + + if hasattr(eset.rthread.radio, "errors") and \ + eset.rthread.radio.errors: + msg = _("{num} errors during open:").format( + num=len(eset.rthread.radio.errors)) + common.show_error_text(msg, + "\r\n".join(eset.rthread.radio.errors)) + self._show_information(radio) + + def do_live_warning(self, radio): + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("" + _("Note:") + "") + msg = _("The {vendor} {model} operates in live mode. " + "This means that any changes you make are immediately sent " + "to the radio. Because of this, you cannot perform the " + "Save or Upload operations. If you wish to " + "edit the contents offline, please Export to a CSV " + "file, using the File menu.") + msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL) + d.format_secondary_markup(msg) + + again = gtk.CheckButton(_("Don't show this again")) + again.show() + d.vbox.pack_start(again, 0, 0, 0) + d.run() + CONF.set_bool("live_mode", again.get_active(), "noconfirm") + d.destroy() + + def do_open_live(self, radio, tempname=None, read_only=False): + eset = editorset.EditorSet(radio, self, tempname=tempname) + eset.connect("want-close", self.do_close) + eset.connect("status", self.ev_status) + eset.set_read_only(read_only) + eset.show() + self.tabs.append_page(eset, eset.get_tab_label()) + + if isinstance(radio, chirp_common.LiveRadio): + reporting.report_model_usage(radio, "live", True) + if not CONF.get_bool("live_mode", "noconfirm"): + self.do_live_warning(radio) + + def do_save(self, eset=None): + if not eset: + eset = self.get_current_editorset() + + # For usability, allow Ctrl-S to short-circuit to Save-As if + # we are working on a yet-to-be-saved image + if not os.path.exists(eset.filename): + return self.do_saveas() + + eset.save() + + def do_saveas(self): + eset = self.get_current_editorset() + + label = _("{vendor} {model} image file").format( + vendor=eset.radio.VENDOR, + model=eset.radio.MODEL) + + defname_format = CONF.get("default_filename", "global") or \ + "{vendor}_{model}_{date}" + defname = defname_format.format( + vendor=eset.radio.VENDOR, + model=eset.radio.MODEL, + date=datetime.now().strftime('%Y%m%d') + ).replace('/', '_') + + types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION, + eset.radio.FILE_EXTENSION)] + + if isinstance(eset.radio, vx7.VX7Radio): + types += [(_("VX7 Commander") + " (*.vx7)", "vx7")] + elif isinstance(eset.radio, vx6.VX6Radio): + types += [(_("VX6 Commander") + " (*.vx6)", "vx6")] + elif isinstance(eset.radio, vx5.VX5Radio): + types += [(_("EVE") + " (*.eve)", "eve")] + types += [(_("VX5 Commander") + " (*.vx5)", "vx5")] + + while True: + fname = platform.get_platform().gui_save_file(default_name=defname, + types=types) + if not fname: + return + + if os.path.exists(fname): + dlg = inputdialog.OverwriteDialog(fname) + owrite = dlg.run() + dlg.destroy() + if owrite == gtk.RESPONSE_OK: + break + else: + break + + try: + eset.save(fname) + except Exception as e: + d = inputdialog.ExceptionDialog(e) + d.run() + d.destroy() + + def cb_clonein(self, radio, emsg=None): + radio.pipe.close() + reporting.report_model_usage(radio, "download", bool(emsg)) + if not emsg: + self.do_open_live(radio, tempname="(" + _("Untitled") + ")") + else: + d = inputdialog.ExceptionDialog(emsg, parent=self) + d.run() + d.destroy() + + def cb_cloneout(self, radio, emsg=None): + radio.pipe.close() + reporting.report_model_usage(radio, "upload", True) + if emsg: + d = inputdialog.ExceptionDialog(emsg) + d.run() + d.destroy() + + def _get_recent_list(self): + recent = [] + for i in range(0, KEEP_RECENT): + fn = CONF.get("recent%i" % i, "state") + if fn: + recent.append(fn) + return recent + + def _set_recent_list(self, recent): + for fn in recent: + CONF.set("recent%i" % recent.index(fn), fn, "state") + + def update_recent_files(self): + i = 0 + for fname in self._get_recent_list(): + action_name = "recent%i" % i + path = "/MenuBar/file/recent" + + old_action = self.menu_ag.get_action(action_name) + if old_action: + self.menu_ag.remove_action(old_action) + + file_basename = os.path.basename(fname).replace("_", "__") + action = gtk.Action( + action_name, "_%i. %s" % (i + 1, file_basename), + _("Open recent file {name}").format(name=fname), "") + action.connect("activate", lambda a, f: self.do_open(f), fname) + mid = self.menu_uim.new_merge_id() + self.menu_uim.add_ui(mid, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + self.menu_ag.add_action(action) + i += 1 + + def record_recent_file(self, filename): + + recent_files = self._get_recent_list() + if filename not in recent_files: + if len(recent_files) == KEEP_RECENT: + del recent_files[-1] + recent_files.insert(0, filename) + self._set_recent_list(recent_files) + + self.update_recent_files() + + def import_stock_config(self, action, config): + eset = self.get_current_editorset() + count = eset.do_import(config) + + def copy_shipped_stock_configs(self, stock_dir): + basepath = platform.get_platform().find_resource("stock_configs") + + files = glob(os.path.join(basepath, "*.csv")) + for fn in files: + if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))): + LOG.info("Skipping existing stock config") + continue + try: + shutil.copy(fn, stock_dir) + LOG.debug("Copying %s -> %s" % (fn, stock_dir)) + except Exception as e: + LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e)) + return False + return True + + def update_stock_configs(self): + stock_dir = platform.get_platform().config_file("stock_configs") + if not os.path.isdir(stock_dir): + try: + os.mkdir(stock_dir) + except Exception as e: + LOG.error("Unable to create directory: %s" % stock_dir) + return + if not self.copy_shipped_stock_configs(stock_dir): + return + + def _do_import_action(config): + name = os.path.splitext(os.path.basename(config))[0] + action_name = "stock-%i" % configs.index(config) + path = "/MenuBar/radio/stock" + action = gtk.Action(action_name, + name, + _("Import stock " + "configuration {name}").format(name=name), + "") + action.connect("activate", self.import_stock_config, config) + mid = self.menu_uim.new_merge_id() + mid = self.menu_uim.add_ui(mid, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + self.menu_ag.add_action(action) + + def _do_open_action(config): + name = os.path.splitext(os.path.basename(config))[0] + action_name = "openstock-%i" % configs.index(config) + path = "/MenuBar/file/openstock" + action = gtk.Action(action_name, + name, + _("Open stock " + "configuration {name}").format(name=name), + "") + action.connect("activate", lambda a, c: self.do_open(c), config) + mid = self.menu_uim.new_merge_id() + mid = self.menu_uim.add_ui(mid, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + self.menu_ag.add_action(action) + + configs = glob(os.path.join(stock_dir, "*.csv")) + for config in configs: + _do_import_action(config) + _do_open_action(config) + + def _confirm_experimental(self, rclass): + sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass) + if CONF.is_defined(sql_key, "state") and \ + not CONF.get_bool(sql_key, "state"): + return True + + title = _("Proceed with experimental driver?") + text = rclass.get_prompts().experimental + msg = _("This radio's driver is experimental. " + "Do you want to proceed?") + resp, squelch = common.show_warning(msg, text, + title=title, + buttons=gtk.BUTTONS_YES_NO, + can_squelch=True) + if resp == gtk.RESPONSE_YES: + CONF.set_bool(sql_key, not squelch, "state") + return resp == gtk.RESPONSE_YES + + def _show_information(self, radio): + message = radio.get_prompts().info + if message is None: + return + + if CONF.get_bool("clone_information", "noconfirm"): + return + + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("" + _("{name} Information").format( + name=radio.get_name()) + "") + msg = _("{information}").format(information=message) + d.format_secondary_markup(msg) + + again = gtk.CheckButton( + _("Don't show information for any radio again")) + again.show() + again.connect("toggled", lambda action: + self.infomenu.set_active(not action.get_active())) + d.vbox.pack_start(again, 0, 0, 0) + h_button_box = d.vbox.get_children()[2] + try: + ok_button = h_button_box.get_children()[0] + ok_button.grab_default() + ok_button.grab_focus() + except AttributeError: + # don't grab focus on GTK+ 2.0 + pass + d.run() + d.destroy() + + def _show_instructions(self, radio, message): + if message is None: + return + + if CONF.get_bool("clone_instructions", "noconfirm"): + return + + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("" + _("{name} Instructions").format( + name=radio.get_name()) + "") + msg = _("{instructions}").format(instructions=message) + d.format_secondary_markup(msg) + + again = gtk.CheckButton( + _("Don't show instructions for any radio again")) + again.show() + again.connect("toggled", lambda action: + self.clonemenu.set_active(not action.get_active())) + d.vbox.pack_start(again, 0, 0, 0) + h_button_box = d.vbox.get_children()[2] + try: + ok_button = h_button_box.get_children()[0] + ok_button.grab_default() + ok_button.grab_focus() + except AttributeError: + # don't grab focus on GTK+ 2.0 + pass + d.run() + d.destroy() + + def do_download(self, port=None, rtype=None): + d = clone.CloneSettingsDialog(parent=self) + settings = d.run() + d.destroy() + if not settings: + return + + rclass = settings.radio_class + if issubclass(rclass, chirp_common.ExperimentalRadio) and \ + not self._confirm_experimental(rclass): + # User does not want to proceed with experimental driver + return + + self._show_instructions(rclass, rclass.get_prompts().pre_download) + + LOG.debug("User selected %s %s on port %s" % + (rclass.VENDOR, rclass.MODEL, settings.port)) + + try: + ser = compat.CompatSerial.get(rclass.NEEDS_COMPAT_SERIAL, + port=settings.port, + baudrate=rclass.BAUD_RATE, + rtscts=rclass.HARDWARE_FLOW, + timeout=0.25) + ser.flushInput() + except serial.SerialException as e: + d = inputdialog.ExceptionDialog(e, parent=self) + d.run() + d.destroy() + return + + radio = settings.radio_class(ser) + + fn = tempfile.mktemp() + if isinstance(radio, chirp_common.CloneModeRadio): + ct = clone.CloneThread(radio, "in", cb=self.cb_clonein, + parent=self) + ct.start() + else: + self.do_open_live(radio) + self._show_information(rclass) # show Info prompt now + + def do_upload(self, port=None, rtype=None): + eset = self.get_current_editorset() + radio = eset.radio + + settings = clone.CloneSettings() + settings.radio_class = radio.__class__ + + d = clone.CloneSettingsDialog(settings, parent=self) + settings = d.run() + d.destroy() + if not settings: + return + prompts = radio.get_prompts() + + if prompts.display_pre_upload_prompt_before_opening_port is True: + LOG.debug("Opening port after pre_upload prompt.") + self._show_instructions(radio, prompts.pre_upload) + + if isinstance(radio, chirp_common.ExperimentalRadio) and \ + not self._confirm_experimental(radio.__class__): + # User does not want to proceed with experimental driver + return + + try: + ser = compat.CompatSerial.get(radio.NEEDS_COMPAT_SERIAL, + port=settings.port, + baudrate=radio.BAUD_RATE, + rtscts=radio.HARDWARE_FLOW, + timeout=0.25) + ser.flushInput() + except serial.SerialException as e: + d = inputdialog.ExceptionDialog(e) + d.run() + d.destroy() + return + + if prompts.display_pre_upload_prompt_before_opening_port is False: + LOG.debug("Opening port before pre_upload prompt.") + self._show_instructions(radio, prompts.pre_upload) + + radio.set_pipe(ser) + + ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self) + ct.start() + + def do_close(self, tab_child=None): + if tab_child: + eset = tab_child + else: + eset = self.get_current_editorset() + + if not eset: + return False + + if eset.is_modified(): + dlg = miscwidgets.YesNoDialog( + title=_("Save Changes?"), parent=self, + buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES, + gtk.STOCK_NO, gtk.RESPONSE_NO, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + dlg.set_text(_("File is modified, save changes before closing?")) + res = dlg.run() + dlg.destroy() + + if res == gtk.RESPONSE_YES: + self.do_save(eset) + elif res != gtk.RESPONSE_NO: + raise ModifiedError() + + eset.rthread.stop() + eset.rthread.join() + + eset.prepare_close() + + if eset.radio.pipe: + eset.radio.pipe.close() + + if isinstance(eset.radio, chirp_common.LiveRadio): + action = self.menu_ag.get_action("openlive") + if action: + action.set_sensitive(True) + + page = self.tabs.page_num(eset) + if page is not None: + self.tabs.remove_page(page) + + return True + + def do_import(self): + types = [(_("All files") + " (*.*)", "*"), + (_("CHIRP Files") + " (*.chirp)", "*.chirp"), + (_("CHIRP Radio Images") + " (*.img)", "*.img"), + (_("CSV Files") + " (*.csv)", "*.csv"), + (_("DAT Files") + " (*.dat)", "*.dat"), + (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), + (_("ICF Files") + " (*.icf)", "*.icf"), + (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"), + (_("Kenwood ITM Files") + " (*.itm)", "*.itm"), + (_("Travel Plus Files") + " (*.tpe)", "*.tpe"), + (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), + (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), + (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")] + filen = platform.get_platform().gui_open_file(types=types) + if not filen: + return + + eset = self.get_current_editorset() + count = eset.do_import(filen) + reporting.report_model_usage(eset.rthread.radio, "import", count > 0) + + def do_dmrmarc_prompt(self): + fields = {"1City": (gtk.Entry(), lambda x: x), + "2State": (gtk.Entry(), lambda x: x), + "3Country": (gtk.Entry(), lambda x: x), + } + + d = inputdialog.FieldDialog(title=_("DMR-MARC Repeater Database Dump"), + parent=self) + for k in sorted(fields.keys()): + d.add_field(k[1:], fields[k][0]) + fields[k][0].set_text(CONF.get(k[1:], "dmrmarc") or "") + + while d.run() == gtk.RESPONSE_OK: + for k in sorted(fields.keys()): + widget, validator = fields[k] + try: + if validator(widget.get_text()): + CONF.set(k[1:], widget.get_text(), "dmrmarc") + continue + except Exception: + pass + + d.destroy() + return True + + d.destroy() + return False + + def do_dmrmarc(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_dmrmarc_prompt(): + self.window.set_cursor(None) + return + + city = CONF.get("city", "dmrmarc") + state = CONF.get("state", "dmrmarc") + country = CONF.get("country", "dmrmarc") + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + if do_import: + eset = self.get_current_editorset() + dmrmarcstr = "dmrmarc://%s/%s/%s" % (city, state, country) + eset.do_import(dmrmarcstr) + else: + try: + from chirp import dmrmarc + radio = dmrmarc.DMRMARCRadio(None) + radio.set_params(city, state, country) + self.do_open_live(radio, read_only=True) + except errors.RadioError as e: + common.show_error(e) + + self.window.set_cursor(None) + + def do_repeaterbook_political_prompt(self): + if not CONF.get_bool("has_seen_credit", "repeaterbook"): + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("RepeaterBook\r\n" + + "North American Repeater Directory") + d.format_secondary_markup("For more information about this " + + "free service, please go to\r\n" + + "http://www.repeaterbook.com") + d.run() + d.destroy() + CONF.set_bool("has_seen_credit", True, "repeaterbook") + + default_state = "Oregon" + default_county = "--All--" + default_band = "--All--" + try: + try: + code = int(CONF.get("state", "repeaterbook")) + except: + code = CONF.get("state", "repeaterbook") + for k, v in fips.FIPS_STATES.items(): + if code == v: + default_state = k + break + + code = CONF.get("county", "repeaterbook") + items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items() + for k, v in items: + if code == v: + default_county = k + break + + code = int(CONF.get("band", "repeaterbook")) + for k, v in RB_BANDS.items(): + if code == v: + default_band = k + break + except: + pass + + state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()), + False, default_state) + county = miscwidgets.make_choice( + sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()), + False, default_county) + band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands), + False, default_band) + + def _changed(box, state, county): + state = fips.FIPS_STATES[state.get_active_text()] + county.get_model().clear() + for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()): + county.append_text(fips_county) + county.value = list(sorted(fips.FIPS_COUNTIES[state].keys()))[0] + + state.widget.connect("changed", _changed, state, county) + + d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self) + d.add_field("State", state.widget) + d.add_field("County", county.widget) + d.add_field("Band", band.widget) + + r = d.run() + chose_state = state.get_active_text() + chose_county = county.get_active_text() + chose_band = band.get_active_text() + d.destroy() + if r != gtk.RESPONSE_OK: + return False + + code = fips.FIPS_STATES[chose_state] + county_id = fips.FIPS_COUNTIES[code][chose_county] + freq = RB_BANDS[chose_band] + CONF.set("state", str(code), "repeaterbook") + CONF.set("county", str(county_id), "repeaterbook") + CONF.set("band", str(freq), "repeaterbook") + + return True + + def do_repeaterbook_political(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_repeaterbook_political_prompt(): + self.window.set_cursor(None) + return + + try: + code = "%02i" % int(CONF.get("state", "repeaterbook")) + except: + try: + code = CONF.get("state", "repeaterbook") + except: + code = '41' # Oregon default + + try: + county = CONF.get("county", "repeaterbook") + except: + county = '%' # --All-- default + + try: + band = int(CONF.get("band", "repeaterbook")) + except: + band = 14 # 2m default + + query = "http://chirp.danplanet.com/query/rb/1.0/chirp" + \ + "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \ + "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%" + query = query % (code, + band and band or "%%", + county and county or "%%") + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this + with compat.py3safe(): + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + fn = tempfile.mktemp(".csv") + try: + chirp_common.urlretrieve(query, fn) + except Exception as e: + LOG.error('Failed to fetch %s: %s' % (query, e)) + if not os.path.exists(fn): + common.show_error(_("RepeaterBook query failed")) + self.window.set_cursor(None) + return + + try: + # Validate CSV + radio = repeaterbook.RBRadio(fn) + if radio.errors: + reporting.report_misc_error("repeaterbook", + ("query=%s\n" % query) + + ("\n") + + ("\n".join(radio.errors))) + except errors.InvalidDataError as e: + common.show_error(str(e)) + self.window.set_cursor(None) + return + except Exception as e: + common.log_exception() + + reporting.report_model_usage(radio, "import", True) + + self.window.set_cursor(None) + if do_import: + eset = self.get_current_editorset() + count = eset.do_import(fn) + else: + self.do_open_live(radio, read_only=True) + + def do_repeaterbook_proximity_prompt(self): + default_band = "--All--" + try: + code = int(CONF.get("band", "repeaterbook")) + for k, v in RB_BANDS.items(): + if code == v: + default_band = k + break + except: + pass + fields = {"1Location": (gtk.Entry(), lambda x: x.get_text()), + "2Distance": (gtk.Entry(), lambda x: x.get_text()), + "3Band": (miscwidgets.make_choice( + sorted(RB_BANDS.keys(), key=key_bands), + False, default_band), + lambda x: RB_BANDS[x.get_active_text()]), + } + + d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), + parent=self) + for k in sorted(fields.keys()): + widget = fields[k][0] + if isinstance(widget, miscwidgets.EditableChoiceBase): + widget = widget.widget + d.add_field(k[1:], widget) + if isinstance(fields[k][0], gtk.Entry): + fields[k][0].set_text( + CONF.get(k[1:].lower(), "repeaterbook") or "") + + while d.run() == gtk.RESPONSE_OK: + valid = True + for k, (widget, fn) in fields.items(): + try: + CONF.set(k[1:].lower(), str(fn(widget)), "repeaterbook") + continue + except: + pass + common.show_error("Invalid value for %s" % k[1:]) + valid = False + break + + if valid: + d.destroy() + return True + + d.destroy() + return False + + def do_repeaterbook_proximity(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_repeaterbook_proximity_prompt(): + self.window.set_cursor(None) + return + + loc = CONF.get("location", "repeaterbook") + + try: + dist = int(CONF.get("distance", "repeaterbook")) + except: + dist = 20 + + try: + band = int(CONF.get("band", "repeaterbook")) or '%' + band = str(band) + except: + band = '%' + + query = "http://chirp.danplanet.com/query/rb/1.0/app_direct" \ + "?loc=%s&band=%s&dist=%s" % (loc, band, dist) + print(query) + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this + with compat.py3safe(): + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + fn = tempfile.mktemp(".csv") + try: + chirp_common.urlretrieve(query, fn) + except Exception as e: + LOG.error('Failed to fetch %s: %s' % (query, e)) + if not os.path.exists(fn): + common.show_error(_("RepeaterBook query failed")) + self.window.set_cursor(None) + return + + try: + # Validate CSV + radio = repeaterbook.RBRadio(fn) + if radio.errors: + reporting.report_misc_error("repeaterbook", + ("query=%s\n" % query) + + ("\n") + + ("\n".join(radio.errors))) + except errors.InvalidDataError as e: + common.show_error(str(e)) + self.window.set_cursor(None) + return + except Exception as e: + common.log_exception() + + reporting.report_model_usage(radio, "import", True) + + self.window.set_cursor(None) + if do_import: + eset = self.get_current_editorset() + count = eset.do_import(fn) + else: + self.do_open_live(radio, read_only=True) + + def do_przemienniki_prompt(self): + d = inputdialog.FieldDialog(title='przemienniki.net query', + parent=self) + fields = { + "Country": + (miscwidgets.make_choice( + ['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi', + 'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se', + 'sk', 'ua', 'uk'], False), + lambda x: str(x.get_active_text())), + "Band": + (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm', + '23cm', '13cm', '3cm'], False, '2m'), + lambda x: str(x.get_active_text())), + "Mode": + (miscwidgets.make_choice(['fm', 'dv'], False), + lambda x: str(x.get_active_text())), + "Only Working": + (miscwidgets.make_choice(['', 'yes'], False), + lambda x: str(x.get_active_text())), + "Latitude": (gtk.Entry(), lambda x: float(x.get_text())), + "Longitude": (gtk.Entry(), lambda x: float(x.get_text())), + "Range": (gtk.Entry(), lambda x: int(x.get_text())), + } + for name in sorted(fields.keys()): + value, fn = fields[name] + d.add_field(name, value) + while d.run() == gtk.RESPONSE_OK: + query = "http://przemienniki.net/export/chirp.csv?" + args = [] + for name, (value, fn) in fields.items(): + if isinstance(value, gtk.Entry): + contents = value.get_text() + else: + contents = value.get_active_text() + if contents: + try: + _value = fn(value) + except ValueError: + common.show_error(_("Invalid value for %s") % name) + query = None + continue + + args.append("=".join((name.replace(" ", "").lower(), + contents))) + query += "&".join(args) + LOG.debug(query) + d.destroy() + return query + + d.destroy() + return query + + def do_przemienniki(self, do_import): + url = self.do_przemienniki_prompt() + if not url: + return + + fn = tempfile.mktemp(".csv") + try: + chirp_common.urlretrieve(url, fn) + except Exception as e: + LOG.error('Failed to fetch %s: %s' % (url, e)) + if not os.path.exists(fn): + common.show_error(_("Query failed")) + return + + class PRRadio(generic_csv.CSVRadio, + chirp_common.NetworkSourceRadio): + VENDOR = "przemienniki.net" + MODEL = "" + + try: + radio = PRRadio(fn) + except Exception as e: + common.show_error(str(e)) + return + + if do_import: + eset = self.get_current_editorset() + count = eset.do_import(fn) + else: + self.do_open_live(radio, read_only=True) + + def do_rfinder_prompt(self): + fields = {"1Email": (gtk.Entry(), lambda x: "@" in x), + "2Password": (gtk.Entry(), lambda x: x), + "3Latitude": (gtk.Entry(), + lambda x: float(x) < 90 and float(x) > -90), + "4Longitude": (gtk.Entry(), + lambda x: float(x) < 180 and float(x) > -180), + "5Range_in_Miles": (gtk.Entry(), + lambda x: int(x) > 0 and int(x) < 5000), + } + + d = inputdialog.FieldDialog(title="RFinder Login", parent=self) + for k in sorted(fields.keys()): + d.add_field(k[1:].replace("_", " "), fields[k][0]) + fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "") + fields[k][0].set_visibility(k != "2Password") + + while d.run() == gtk.RESPONSE_OK: + valid = True + for k in sorted(fields.keys()): + widget, validator = fields[k] + try: + if validator(widget.get_text()): + CONF.set(k[1:], widget.get_text(), "rfinder") + continue + except Exception: + pass + common.show_error("Invalid value for %s" % k[1:]) + valid = False + break + + if valid: + d.destroy() + return True + + d.destroy() + return False + + def do_rfinder(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_rfinder_prompt(): + self.window.set_cursor(None) + return + + lat = CONF.get_float("Latitude", "rfinder") + lon = CONF.get_float("Longitude", "rfinder") + passwd = CONF.get("Password", "rfinder") + email = CONF.get("Email", "rfinder") + miles = CONF.get_int("Range_in_Miles", "rfinder") + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + if do_import: + eset = self.get_current_editorset() + rfstr = "rfinder://%s/%s/%f/%f/%i" % \ + (email, passwd, lat, lon, miles) + count = eset.do_import(rfstr) + else: + from chirp.drivers import rfinder + radio = rfinder.RFinderRadio(None) + radio.set_params((lat, lon), miles, email, passwd) + self.do_open_live(radio, read_only=True) + + self.window.set_cursor(None) + + def do_radioreference_prompt(self): + fields = {"1Username": (gtk.Entry(), lambda x: x), + "2Password": (gtk.Entry(), lambda x: x), + "3Zipcode": (gtk.Entry(), lambda x: x), + } + + d = inputdialog.FieldDialog(title=_("RadioReference.com Query"), + parent=self) + for k in sorted(fields.keys()): + d.add_field(k[1:], fields[k][0]) + fields[k][0].set_text(CONF.get(k[1:], "radioreference") or "") + fields[k][0].set_visibility(k != "2Password") + + while d.run() == gtk.RESPONSE_OK: + valid = True + for k in sorted(fields.keys()): + widget, validator = fields[k] + try: + if validator(widget.get_text()): + CONF.set(k[1:], widget.get_text(), "radioreference") + continue + except Exception: + pass + common.show_error("Invalid value for %s" % k[1:]) + valid = False + break + + if valid: + d.destroy() + return True + + d.destroy() + return False + + def do_radioreference(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_radioreference_prompt(): + self.window.set_cursor(None) + return + + username = CONF.get("Username", "radioreference") + passwd = CONF.get("Password", "radioreference") + zipcode = CONF.get("Zipcode", "radioreference") + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + if do_import: + eset = self.get_current_editorset() + rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd) + count = eset.do_import(rrstr) + else: + try: + from chirp import radioreference + radio = radioreference.RadioReferenceRadio(None) + radio.set_params(zipcode, username, passwd) + self.do_open_live(radio, read_only=True) + except errors.RadioError as e: + common.show_error(e) + + self.window.set_cursor(None) + + def do_export(self): + types = [(_("CSV Files") + " (*.csv)", "csv"), + ] + + eset = self.get_current_editorset() + + if os.path.exists(eset.filename): + base = os.path.basename(eset.filename) + if "." in base: + base = base[:base.rindex(".")] + defname = base + else: + defname = "radio" + + filen = platform.get_platform().gui_save_file(default_name=defname, + types=types) + if not filen: + return + + if os.path.exists(filen): + dlg = inputdialog.OverwriteDialog(filen) + owrite = dlg.run() + dlg.destroy() + if owrite != gtk.RESPONSE_OK: + return + os.remove(filen) + + count = eset.do_export(filen) + reporting.report_model_usage(eset.rthread.radio, "export", count > 0) + + def do_about(self): + d = gtk.AboutDialog() + d.set_transient_for(self) + import sys + verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % ( + ".".join([str(x) for x in gtk.gtk_version]), + ".".join([str(x) for x in gtk.pygtk_version]), + sys.version.split()[0]) + + # Set url hook to handle user activating a URL link in the about dialog + with compat.py3safe(): + gtk.about_dialog_set_url_hook( + lambda dlg, url: webbrowser.open(url)) + + d.set_name("CHIRP") + d.set_version(CHIRP_VERSION) + d.set_copyright("Copyright 2019 CHIRP Software LLC") + d.set_website("http://chirp.danplanet.com") + d.set_translator_credits("Polish: Grzegorz SQ2RBY" + + os.linesep + + "Italian: Fabio IZ2QDH" + + os.linesep + + "Dutch: Michael PD4MT" + + os.linesep + + "German: Benjamin HB9EUK" + + os.linesep + + "Hungarian: Attila HA5JA" + + os.linesep + + "Russian: Dmitry Slukin" + + os.linesep + + "Portuguese (BR): Crezivando PP7CJ") + d.set_comments(verinfo) + + d.run() + d.destroy() + + def do_gethelp(self): + webbrowser.open("http://chirp.danplanet.com") + + def do_columns(self): + eset = self.get_current_editorset() + driver = directory.get_driver(eset.rthread.radio.__class__) + radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR, + eset.rthread.radio.MODEL, + eset.rthread.radio.VARIANT) + d = gtk.Dialog(title=_("Select Columns"), + parent=self, + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + + vbox = gtk.VBox() + vbox.show() + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.add_with_viewport(vbox) + sw.show() + d.vbox.pack_start(sw, 1, 1, 1) + d.set_size_request(-1, 300) + d.set_resizable(False) + + labelstr = _("Visible columns for {radio}").format(radio=radio_name) + label = gtk.Label(labelstr) + label.show() + vbox.pack_start(label) + + fields = [] + memedit = eset.get_current_editor() # .editors["memedit"] + unsupported = memedit.get_unsupported_columns() + for colspec in memedit.cols: + if colspec[0].startswith("_"): + continue + elif colspec[0] in unsupported: + continue + label = colspec[0] + visible = memedit.get_column_visible(memedit.col(label)) + widget = gtk.CheckButton(label) + widget.set_active(visible) + fields.append(widget) + vbox.pack_start(widget, 1, 1, 1) + widget.show() + + res = d.run() + selected_columns = [] + if res == gtk.RESPONSE_OK: + for widget in fields: + colnum = memedit.col(widget.get_label()) + memedit.set_column_visible(colnum, widget.get_active()) + if widget.get_active(): + selected_columns.append(widget.get_label()) + + d.destroy() + + CONF.set(driver, ",".join(selected_columns), "memedit_columns") + + def do_hide_unused(self, action): + eset = self.get_current_editorset() + if eset is None: + conf = config.get("memedit") + conf.set_bool("hide_unused", action.get_active()) + else: + for editortype, editor in eset.editors.iteritems(): + if "memedit" in editortype: + editor.set_hide_unused(action.get_active()) + + def do_clearq(self): + eset = self.get_current_editorset() + eset.rthread.flush() + + def do_copy(self, cut): + eset = self.get_current_editorset() + eset.get_current_editor().copy_selection(cut) + + def do_paste(self): + eset = self.get_current_editorset() + eset.get_current_editor().paste_selection() + + def do_delete(self): + eset = self.get_current_editorset() + eset.get_current_editor().copy_selection(True) + + def do_toggle_report(self, action): + if not action.get_active(): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self) + markup = "" + _("Reporting is disabled") + "" + d.set_markup(markup) + msg = _("The reporting feature of CHIRP is designed to help " + "improve quality by allowing the authors to focus " + "on the radio drivers used most often and errors " + "experienced by the users. The reports contain no " + "identifying information and are used only for " + "statistical purposes by the authors. Your privacy is " + "extremely important, but please consider leaving " + "this feature enabled to help make CHIRP better!\n\n" + "Are you sure you want to disable this feature?") + d.format_secondary_markup(msg.replace("\n", "\r\n")) + r = d.run() + d.destroy() + if r == gtk.RESPONSE_NO: + action.set_active(not action.get_active()) + + conf = config.get() + conf.set_bool("no_report", not action.get_active()) + + def do_toggle_no_smart_tmode(self, action): + CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit") + + def do_toggle_developer(self, action): + conf = config.get() + conf.set_bool("developer", action.get_active(), "state") + + for name in ["viewdeveloper", "loadmod"]: + devaction = self.menu_ag.get_action(name) + devaction.set_visible(action.get_active()) + + def do_toggle_clone_information(self, action): + CONF.set_bool("clone_information", + not action.get_active(), "noconfirm") + + def do_toggle_clone_instructions(self, action): + CONF.set_bool("clone_instructions", + not action.get_active(), "noconfirm") + + def do_change_language(self): + langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German", + "Hungarian", "Russian", "Portuguese (BR)", "French", + "Spanish"] + d = inputdialog.ChoiceDialog(langs, parent=self, + title="Choose Language") + d.label.set_text(_("Choose a language or Auto to use the " + "operating system default. You will need to " + "restart the application before the change " + "will take effect")) + d.label.set_line_wrap(True) + r = d.run() + if r == gtk.RESPONSE_OK: + LOG.debug("Chose language %s" % d.choice.get_active_text()) + conf = config.get() + conf.set("language", d.choice.get_active_text(), "state") + d.destroy() + + def load_module(self, filen=None): + types = [(_("Python Modules") + " *.py", "*.py"), + (_("Modules") + " *.mod", "*.mod")] + + if filen is None: + filen = platform.get_platform().gui_open_file(types=types) + if not filen: + return + + # We're in development mode, so we need to tell the directory to + # allow a loaded module to override an existing driver, against + # its normal better judgement + directory.enable_reregistrations() + + self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color('#ea6262')) + + try: + with file(filen) as module: + code = module.read() + pyc = compile(code, filen, 'exec') + # See this for why: + # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec + exec(pyc, globals(), globals()) + except Exception as e: + common.log_exception() + common.show_error("Unable to load module: %s" % e) + + def mh(self, _action, *args): + action = _action.get_name() + + if action == "quit": + gtk.main_quit() + elif action == "new": + self.do_new() + elif action == "open": + self.do_open() + elif action == "save": + self.do_save() + elif action == "saveas": + self.do_saveas() + elif action.startswith("download"): + self.do_download(*args) + elif action.startswith("upload"): + self.do_upload(*args) + elif action == "close": + self.do_close() + elif action == "import": + self.do_import() + elif action in ["qdmrmarc", "idmrmarc"]: + self.do_dmrmarc(action[0] == "i") + elif action in ["qrfinder", "irfinder"]: + self.do_rfinder(action[0] == "i") + elif action in ["qradioreference", "iradioreference"]: + self.do_radioreference(action[0] == "i") + elif action == "export": + self.do_export() + elif action in ["qrbookpolitical", "irbookpolitical"]: + self.do_repeaterbook_political(action[0] == "i") + elif action in ["qrbookproximity", "irbookproximity"]: + self.do_repeaterbook_proximity(action[0] == "i") + elif action in ["qpr", "ipr"]: + self.do_przemienniki(action[0] == "i") + elif action == "about": + self.do_about() + elif action == "gethelp": + self.do_gethelp() + elif action == "columns": + self.do_columns() + elif action == "hide_unused": + self.do_hide_unused(_action) + elif action == "cancelq": + self.do_clearq() + elif action == "report": + self.do_toggle_report(_action) + elif action == "channel_defaults": + # The memedit thread also has an instance of bandplans. + bp = bandplans.BandPlans(CONF) + bp.select_bandplan(self) + elif action == "no_smart_tmode": + self.do_toggle_no_smart_tmode(_action) + elif action == "developer": + self.do_toggle_developer(_action) + elif action == "clone_information": + self.do_toggle_clone_information(_action) + elif action == "clone_instructions": + self.do_toggle_clone_instructions(_action) + elif action in ["cut", "copy", "paste", "delete", + "move_up", "move_dn", "exchange", "all", + "devshowraw", "devdiffraw", "properties"]: + self.get_current_editorset().get_current_editor().hotkey(_action) + elif action == "devdifftab": + self.do_diff_radio() + elif action == "language": + self.do_change_language() + elif action == "loadmod": + self.load_module() + else: + return + + self.ev_tab_switched() + + def make_menubar(self): + menu_xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + ALT_KEY = "" + CTRL_KEY = "" + if sys.platform == 'darwin': + ALT_KEY = "" + CTRL_KEY = "" + actions = [ + ('file', None, _("_File"), None, None, self.mh), + ('new', gtk.STOCK_NEW, None, None, None, self.mh), + ('open', gtk.STOCK_OPEN, None, None, None, self.mh), + ('openstock', None, _("Open stock config"), None, None, self.mh), + ('recent', None, _("_Recent"), None, None, self.mh), + ('save', gtk.STOCK_SAVE, None, None, None, self.mh), + ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh), + ('loadmod', None, _("Load Module"), None, None, self.mh), + ('close', gtk.STOCK_CLOSE, None, None, None, self.mh), + ('quit', gtk.STOCK_QUIT, None, None, None, self.mh), + ('edit', None, _("_Edit"), None, None, self.mh), + ('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh), + ('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh), + ('paste', None, _("_Paste"), + "%sv" % CTRL_KEY, None, self.mh), + ('delete', None, _("_Delete"), "Delete", None, self.mh), + ('all', None, _("Select _All"), None, None, self.mh), + ('move_up', None, _("Move _Up"), + "%sUp" % CTRL_KEY, None, self.mh), + ('move_dn', None, _("Move Dow_n"), + "%sDown" % CTRL_KEY, None, self.mh), + ('exchange', None, _("E_xchange"), + "%sx" % CTRL_KEY, None, self.mh), + ('properties', None, _("P_roperties"), None, None, self.mh), + ('view', None, _("_View"), None, None, self.mh), + ('columns', None, _("Columns"), None, None, self.mh), + ('viewdeveloper', None, _("Developer"), None, None, self.mh), + ('devshowraw', None, _('Show raw memory'), + "%sr" % CTRL_KEY, None, self.mh), + ('devdiffraw', None, _("Diff raw memories"), + "%sd" % CTRL_KEY, None, self.mh), + ('devdifftab', None, _("Diff tabs"), + "%st" % CTRL_KEY, None, self.mh), + ('language', None, _("Change language"), None, None, self.mh), + ('radio', None, _("_Radio"), None, None, self.mh), + ('download', None, _("Download From Radio"), + "%sd" % ALT_KEY, None, self.mh), + ('upload', None, _("Upload To Radio"), + "%su" % ALT_KEY, None, self.mh), + ('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh), + ('export', None, _("Export"), "%se" % ALT_KEY, None, self.mh), + ('importsrc', None, _("Import from data source"), + None, None, self.mh), + ('idmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh), + ('iradioreference', None, _("RadioReference.com"), + None, None, self.mh), + ('irfinder', None, _("RFinder"), None, None, self.mh), + ('irbook', None, _("RepeaterBook"), None, None, self.mh), + ('irbookpolitical', None, _("RepeaterBook political query"), None, + None, self.mh), + ('irbookproximity', None, _("RepeaterBook proximity query"), None, + None, self.mh), + ('ipr', None, _("przemienniki.net"), None, None, self.mh), + ('querysrc', None, _("Query data source"), None, None, self.mh), + ('qdmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh), + ('qradioreference', None, _("RadioReference.com"), + None, None, self.mh), + ('qrfinder', None, _("RFinder"), None, None, self.mh), + ('qpr', None, _("przemienniki.net"), None, None, self.mh), + ('qrbook', None, _("RepeaterBook"), None, None, self.mh), + ('qrbookpolitical', None, _("RepeaterBook political query"), None, + None, self.mh), + ('qrbookproximity', None, _("RepeaterBook proximity query"), None, + None, self.mh), + ('export_chirp', None, _("CHIRP Native File"), + None, None, self.mh), + ('export_csv', None, _("CSV File"), None, None, self.mh), + ('stock', None, _("Import from stock config"), + None, None, self.mh), + ('channel_defaults', None, _("Channel defaults"), + None, None, self.mh), + ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh), + ('help', None, _('Help'), None, None, self.mh), + ('about', gtk.STOCK_ABOUT, None, None, None, self.mh), + ('gethelp', None, _("Get Help Online..."), None, None, self.mh), + ] + + conf = config.get() + re = not conf.get_bool("no_report") + hu = conf.get_bool("hide_unused", "memedit", default=True) + dv = conf.get_bool("developer", "state") + cf = not conf.get_bool("clone_information", "noconfirm") + ci = not conf.get_bool("clone_instructions", "noconfirm") + st = not conf.get_bool("no_smart_tmode", "memedit") + + toggles = [('report', None, _("Report Statistics"), + None, None, self.mh, re), + ('hide_unused', None, _("Hide Unused Fields"), + None, None, self.mh, hu), + ('no_smart_tmode', None, _("Smart Tone Modes"), + None, None, self.mh, st), + ('clone_information', None, _("Show Information"), + None, None, self.mh, cf), + ('clone_instructions', None, _("Show Instructions"), + None, None, self.mh, ci), + ('developer', None, _("Enable Developer Functions"), + None, None, self.mh, dv), + ] + + self.menu_uim = gtk.UIManager() + self.menu_ag = gtk.ActionGroup("MenuBar") + self.menu_ag.add_actions(actions) + self.menu_ag.add_toggle_actions(toggles) + + self.menu_uim.insert_action_group(self.menu_ag, 0) + self.menu_uim.add_ui_from_string(menu_xml) + + self.add_accel_group(self.menu_uim.get_accel_group()) + + self.infomenu = self.menu_uim.get_widget( + "/MenuBar/help/clone_information") + + self.clonemenu = self.menu_uim.get_widget( + "/MenuBar/help/clone_instructions") + + # Initialize + self.do_toggle_developer(self.menu_ag.get_action("developer")) + + return self.menu_uim.get_widget("/MenuBar") + + def make_tabs(self): + self.tabs = gtk.Notebook() + self.tabs.set_scrollable(True) + + return self.tabs + + def close_out(self): + num = self.tabs.get_n_pages() + while num > 0: + num -= 1 + LOG.debug("Closing %i" % num) + try: + self.do_close(self.tabs.get_nth_page(num)) + except ModifiedError: + return False + + gtk.main_quit() + + return True + + def make_status_bar(self): + box = gtk.HBox(False, 2) + + self.sb_general = gtk.Statusbar() + self.sb_general.show() + box.pack_start(self.sb_general, 1, 1, 1) + + self.sb_radio = gtk.Statusbar() + self.sb_radio.show() + box.pack_start(self.sb_radio, 1, 1, 1) + + with compat.py3safe(quiet=True): + # Gtk2 had resize grips on the status bars, so remove them + # if we can + self.sb_general.set_has_resize_grip(False) + self.sb_radio.set_has_resize_grip(True) + + box.show() + return box + + def ev_delete(self, window, event): + if not self.close_out(): + return True # Don't exit + + def ev_destroy(self, window): + if not self.close_out(): + return True # Don't exit + + def setup_extra_hotkeys(self): + accelg = self.menu_uim.get_accel_group() + + def memedit(a): + self.get_current_editorset().editors["memedit"].hotkey(a) + + actions = [ + # ("action_name", "key", function) + ] + + for name, key, fn in actions: + a = gtk.Action(name, name, name, "") + a.connect("activate", fn) + self.menu_ag.add_action_with_accel(a, key) + a.set_accel_group(accelg) + a.connect_accelerator() + + def _set_icon(self): + this_platform = platform.get_platform() + path = (this_platform.find_resource("chirp.png") or + this_platform.find_resource(os.path.join("pixmaps", + "chirp.png"))) + if os.path.exists(path): + self.set_icon_from_file(path) + else: + LOG.warn("Icon %s not found" % path) + + def _updates(self, version): + if not version: + return + + if version == CHIRP_VERSION: + return + + LOG.info("Server reports version %s is available" % version) + + # Report new updates every three days + intv = 3600 * 24 * 3 + + if CONF.is_defined("last_update_check", "state") and \ + (time.time() - CONF.get_int("last_update_check", "state")) < intv: + return + + CONF.set_int("last_update_check", int(time.time()), "state") + d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=self, + type=gtk.MESSAGE_INFO) + d.label.set_markup( + _('A new version of CHIRP is available: ' + + '{ver}. '.format(ver=version) + + 'It is recommended that you upgrade as soon as possible. ' + 'Please go to: \r\n\r\n' + + 'http://chirp.danplanet.com')) + response = d.run() + d.destroy() + if response == gtk.RESPONSE_OK: + webbrowser.open('http://chirp.danplanet.com/' + 'projects/chirp/wiki/Download') + + def _init_macos(self, menu_bar): + macapp = None + + # for KK7DS runtime <= R10 + try: + import gtk_osxapplication + macapp = gtk_osxapplication.OSXApplication() + except ImportError: + pass + + # for gtk-mac-integration >= 2.0.7 + try: + import gtkosx_application + macapp = gtkosx_application.Application() + except ImportError: + pass + + # for gtk-mac-integration 2.1.3 in brew + try: + from gi.repository import GtkosxApplication + macapp = GtkosxApplication.Application() + except ImportError: + pass + + if macapp is None: + LOG.error("No MacOS support") + return + + this_platform = platform.get_platform() + icon = (this_platform.find_resource("chirp.png") or + this_platform.find_resource(os.path.join("pixmaps", + "chirp.png"))) + if os.path.exists(icon): + icon_pixmap = gtk.gdk.pixbuf_new_from_file(icon) + macapp.set_dock_icon_pixbuf(icon_pixmap) + + menu_bar.hide() + macapp.set_menu_bar(menu_bar) + + quititem = self.menu_uim.get_widget("/MenuBar/file/quit") + quititem.hide() + + aboutitem = self.menu_uim.get_widget("/MenuBar/help/about") + macapp.insert_app_menu_item(aboutitem, 0) + + documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp") + macapp.insert_app_menu_item(documentationitem, 0) + + macapp.set_use_quartz_accelerators(False) + macapp.ready() + + LOG.debug("Initialized MacOS support") + + def __init__(self, *args, **kwargs): + gtk.Window.__init__(self, *args, **kwargs) + + def expose(window, event=None): + allocation = window.get_allocation() + CONF.set_int("window_w", allocation.width, "state") + CONF.set_int("window_h", allocation.height, "state") + + with compat.py3safe(quiet=True): + # GTK3 does not have 'expose_event' and I am not sure which + # is a suitable replacement. We only need this to save window + # size in the config, so don't warn about it. + self.connect("expose_event", expose) + + def state_change(window, event): + CONF.set_bool( + "window_maximized", + event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED, + "state") + self.connect("window-state-event", state_change) + + d = CONF.get("last_dir", "state") + if d and os.path.isdir(d): + platform.get_platform().set_last_dir(d) + + vbox = gtk.VBox(False, 2) + + self._recent = [] + + self.menu_ag = None + mbar = self.make_menubar() + + if os.name != "nt": + self._set_icon() # Windows gets the icon from the exe + if os.uname()[0] == "Darwin": + self._init_macos(mbar) + + vbox.pack_start(mbar, 0, 0, 0) + + self.tabs = None + tabs = self.make_tabs() + tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p)) + tabs.connect("page-removed", lambda *a: self.ev_tab_switched()) + tabs.show() + self.ev_tab_switched() + vbox.pack_start(tabs, 1, 1, 1) + + vbox.pack_start(self.make_status_bar(), 0, 0, 0) + + vbox.show() + + self.add(vbox) + + try: + width = CONF.get_int("window_w", "state") + height = CONF.get_int("window_h", "state") + except Exception: + width = 800 + height = 600 + + self.set_default_size(width, height) + if CONF.get_bool("window_maximized", "state"): + self.maximize() + self.set_title("CHIRP") + + self.connect("delete_event", self.ev_delete) + self.connect("destroy", self.ev_destroy) + + if not CONF.get_bool("warned_about_reporting") and \ + not CONF.get_bool("no_report"): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) + d.set_markup("" + + _("Error reporting is enabled") + + "") + d.format_secondary_markup( + _("If you wish to disable this feature you may do so in " + "the Help menu")) + d.run() + d.destroy() + CONF.set_bool("warned_about_reporting", True) + + self.update_recent_files() + try: + self.update_stock_configs() + except UnicodeDecodeError: + LOG.exception('We hit bug #272 while working with unicode paths. ' + 'Not copying stock configs so we can continue ' + 'startup.') + self.setup_extra_hotkeys() + + def updates_callback(ver): + gobject.idle_add(self._updates, ver) + + if not CONF.get_bool("skip_update_check", "state"): + reporting.check_for_updates(updates_callback) diff --git a/chirp/ui/memdetail.py b/chirp/ui/memdetail.py new file mode 100644 index 0000000..bee6083 --- /dev/null +++ b/chirp/ui/memdetail.py @@ -0,0 +1,421 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import os +import logging + +from chirp import chirp_common, settings +from chirp.ui import miscwidgets, common +from chirp.ui import compat + +LOG = logging.getLogger(__name__) + +POL = ["NN", "NR", "RN", "RR"] + + +class ValueEditor: + """Base class""" + def __init__(self, features, memory, errfn, name, data=None): + self._features = features + self._memory = memory + self._errfn = errfn + self._name = name + self._widget = None + self._init(data) + + def _init(self, data): + """Type-specific initialization""" + + def set_sensitive(self, sensitive): + self._widget.set_sensitive(sensitive) + + def get_widget(self): + """Returns the widget associated with this editor""" + return self._widget + + def _mem_value(self): + """Returns the raw value from the memory associated with this name""" + if self._name.startswith("extra_"): + return self._memory.extra[self._name.split("_", 1)[1]].value + else: + return getattr(self._memory, self._name) + + def _get_value(self): + """Returns the value from the widget that + should be set in the memory""" + + def update(self): + """Updates the memory object with self._getvalue()""" + + try: + newval = self._get_value() + except ValueError as e: + self._errfn(self._name, str(e)) + return str(e) + + if self._name.startswith("extra_"): + try: + self._memory.extra[self._name.split("_", 1)[1]].value = newval + except settings.InternalError as e: + self._errfn(self._name, str(e)) + return str(e) + else: + try: + setattr(self._memory, self._name, newval) + except chirp_common.ImmutableValueError as e: + if getattr(self._memory, self._name) != self._get_value(): + self._errfn(self._name, str(e)) + return str(e) + except ValueError as e: + self._errfn(self._name, str(e)) + return str(e) + + all_msgs = self._features.validate_memory(self._memory) + errs = [] + for msg in all_msgs: + if isinstance(msg, chirp_common.ValidationError): + errs.append(str(msg)) + if errs: + self._errfn(self._name, errs) + else: + self._errfn(self._name, None) + + +class StringEditor(ValueEditor): + def _init(self, data): + try: + self._widget = gtk.Entry(int(data)) + except TypeError: + self._widget = gtk.Entry() + self._widget.set_max_length(int(data)) + self._widget.set_text(str(self._mem_value())) + self._widget.connect("changed", self.changed) + + def _get_value(self): + return self._widget.get_text() + + def changed(self, _widget): + self.update() + + +class ChoiceEditor(ValueEditor): + def _init(self, data): + self._choice = miscwidgets.make_choice([str(x) for x in data], + False, + str(self._mem_value())) + self._widget = self._choice.widget + + self._widget.connect("changed", self.changed) + + def _get_value(self): + return self._choice.value + + def changed(self, _widget): + self.update() + + +class PowerChoiceEditor(ChoiceEditor): + def _init(self, data): + self._choices = data + ChoiceEditor._init(self, data) + + def _get_value(self): + choice = self._widget.get_active_text() + for level in self._choices: + if str(level) == choice: + return level + raise Exception("Internal error: power level went missing") + + +class IntChoiceEditor(ChoiceEditor): + def _get_value(self): + return int(self._widget.get_active_text()) + + +class FloatChoiceEditor(ChoiceEditor): + def _get_value(self): + return float(self._widget.get_active_text()) + + +class FreqEditor(StringEditor): + def _init(self, data): + StringEditor._init(self, 0) + + def _mem_value(self): + return chirp_common.format_freq(StringEditor._mem_value(self)) + + def _get_value(self): + return chirp_common.parse_freq(self._widget.get_text()) + + +class BooleanEditor(ValueEditor): + def _init(self, data): + self._widget = gtk.CheckButton("Enabled") + self._widget.set_active(self._mem_value()) + self._widget.connect("toggled", self.toggled) + + def _get_value(self): + return self._widget.get_active() + + def toggled(self, _widget): + self.update() + + +class OffsetEditor(FreqEditor): + pass + + +class MemoryDetailEditor(gtk.Dialog): + """Detail editor for a memory""" + + def _add(self, tab, row, name, editor, text, colindex=0): + label = gtk.Label(text + ":") + label.set_alignment(0.0, 0.5) + label.show() + tab.attach(label, colindex, colindex + 1, row, row + 1, + xoptions=gtk.FILL, yoptions=0, xpadding=6, ypadding=3) + + widget = editor.get_widget() + widget.show() + tab.attach(widget, colindex + 1, colindex + 2, row, row + 1, + xoptions=gtk.FILL, yoptions=0, xpadding=3, ypadding=3) + + img = gtk.Image() + img.set_size_request(16, -1) + img.show() + tab.attach(img, colindex + 2, colindex + 3, row, row + 1, + xoptions=gtk.FILL, yoptions=0, xpadding=3, ypadding=3) + + self._editors[name] = label, editor, img + return label, editor, img + + def _set_doc(self, name, doc): + label, editor, _img = self._editors[name] + self._tips.set_tip(label, doc) + + def _make_ui(self): + + box = gtk.VBox() + box.show() + + notebook = gtk.Notebook() + notebook.set_show_border(False) + notebook.show() + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.show() + + hbox = gtk.HBox() + hbox.pack_start(sw, 1, 1, 1) + hbox.show() + + tab = notebook.append_page(hbox, gtk.Label(_("General"))) + + table = gtk.Table(len(self._order), 4, False) + table.set_resize_mode(gtk.RESIZE_IMMEDIATE) + table.show() + sw.add_with_viewport(table) + + def _err(name, msg): + try: + _img = self._editors[name][2] + except KeyError: + LOG.error(self._editors.keys()) + if msg is None: + _img.clear() + self._tips.set_tip(_img, "") + else: + _img.set_from_stock(gtk.STOCK_DIALOG_WARNING, + gtk.ICON_SIZE_MENU) + self._tips.set_tip(_img, str(msg)) + self._errors[self._order.index(name)] = msg is not None + self.set_response_sensitive(gtk.RESPONSE_OK, + True not in self._errors) + + row = 0 + for name in self._order: + text, editorcls, data = self._elements[name] + editor = editorcls(self._features, self._memory, + _err, name, data) + + self._add(table, row, name, editor, text) + self._set_doc(name, text) + row += 1 + + if len(self._memory.extra): + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.show() + + hbox = gtk.HBox() + hbox.pack_start(sw, 1, 1, 1) + hbox.show() + + tab = notebook.append_page(hbox, gtk.Label(_("Other"))) + + table = gtk.Table(len(self._memory.extra), 4, False) + table.set_resize_mode(gtk.RESIZE_IMMEDIATE) + table.show() + sw.add_with_viewport(table) + + for setting in self._memory.extra: + name = "extra_%s" % setting.get_name() + if isinstance(setting.value, + settings.RadioSettingValueBoolean): + editor = BooleanEditor(self._features, self._memory, + _err, name) + self._add(table, row, name, editor, + setting.get_shortname()) + self._set_doc(name, setting.__doc__) + elif isinstance(setting.value, + settings.RadioSettingValueList): + editor = ChoiceEditor(self._features, self._memory, _err, + name, setting.value.get_options()) + self._add(table, row, name, editor, + setting.get_shortname()) + self._set_doc(name, setting.__doc__) + row += 1 + self._order.append(name) + + self.vbox.pack_start(notebook, 1, 1, 1) + + def __init__(self, features, memory, parent=None): + self._memory = memory + gtk.Dialog.__init__(self, + title="Memory Properties", + flags=gtk.DIALOG_MODAL, + parent=parent, + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + self.set_size_request(-1, 500) + self._tips = compat.CompatTooltips() + + self._features = features + + self._editors = {} + self._elements = { + "freq": (_("Frequency"), + FreqEditor, None), + "name": (_("Name"), + StringEditor, features.valid_name_length), + "tmode": (_("Tone Mode"), + ChoiceEditor, features.valid_tmodes), + "rtone": (_("Tone"), + FloatChoiceEditor, chirp_common.TONES), + "ctone": (_("ToneSql"), + FloatChoiceEditor, chirp_common.TONES), + "dtcs": (_("DTCS Code"), + IntChoiceEditor, chirp_common.DTCS_CODES), + "rx_dtcs": (_("RX DTCS Code"), + IntChoiceEditor, chirp_common.DTCS_CODES), + "dtcs_polarity": (_("DTCS Pol"), + ChoiceEditor, POL), + "cross_mode": (_("Cross mode"), + ChoiceEditor, features.valid_cross_modes), + "duplex": (_("Duplex"), + ChoiceEditor, features.valid_duplexes), + "offset": (_("Offset"), + OffsetEditor, None), + "mode": (_("Mode"), + ChoiceEditor, features.valid_modes), + "tuning_step": (_("Tune Step"), + FloatChoiceEditor, features.valid_tuning_steps), + "skip": (_("Skip"), + ChoiceEditor, features.valid_skips), + "power": (_("Power"), + PowerChoiceEditor, features.valid_power_levels), + "comment": (_("Comment"), + StringEditor, 256), + } + + self._order = [ + "freq", "name", "tmode", "rtone", "ctone", "cross_mode", + "dtcs", "rx_dtcs", "dtcs_polarity", "duplex", "offset", + "mode", "tuning_step", "skip", "power", "comment" + ] + + hide_rules = [ + ("name", features.has_name), + ("tmode", len(features.valid_tmodes) > 0), + ("ctone", features.has_ctone), + ("dtcs", features.has_dtcs), + ("rx_dtcs", features.has_rx_dtcs), + ("dtcs_polarity", features.has_dtcs_polarity), + ("cross_mode", "Cross" in features.valid_tmodes), + ("duplex", len(features.valid_duplexes) > 0), + ("offset", features.has_offset), + ("mode", len(features.valid_modes) > 0), + ("tuning_step", features.has_tuning_step), + ("skip", len(features.valid_skips) > 0), + ("power", features.valid_power_levels), + ("comment", features.has_comment), + ] + + for name, visible in hide_rules: + if not visible: + del self._elements[name] + self._order.remove(name) + + self._make_ui() + + self._errors = [False] * len(self._order) + + self.connect("response", self._validate) + + def _validate(self, _dialog, response): + if response == gtk.RESPONSE_OK: + all_msgs = self._features.validate_memory(self._memory) + errors = [] + for msg in all_msgs: + if isinstance(msg, chirp_common.ValidationError): + errors.append(msg) + if errors: + common.show_error_text(_("Memory validation failed:"), + os.linesep + + os.linesep.join(errors)) + self.emit_stop_by_name('response') + + def get_memory(self): + self._memory.empty = False + return self._memory + + +class MultiMemoryDetailEditor(MemoryDetailEditor): + + def __init__(self, features, memory, parent=None): + self._selections = dict() + super(MultiMemoryDetailEditor, self).__init__(features, memory, parent) + + def _toggle_selector(self, selector, *widgets): + for widget in widgets: + widget.set_sensitive(selector.get_active()) + + def _add(self, tab, row, name, editor, text): + + label, editor, img = super(MultiMemoryDetailEditor, self)._add( + tab, row, name, editor, text, 1) + + selector = gtk.CheckButton() + tab.attach(selector, 0, 1, row, row + 1, + xoptions=gtk.FILL, yoptions=0, xpadding=0, ypadding=3) + selector.show() + self._toggle_selector(selector, label, editor, img) + selector.connect("toggled", self._toggle_selector, label, editor, img) + self._selections[name] = selector + + def get_fields(self): + return [k for k, v in self._selections.items() if v.get_active()] diff --git a/chirp/ui/memedit.py b/chirp/ui/memedit.py new file mode 100644 index 0000000..a1581f5 --- /dev/null +++ b/chirp/ui/memedit.py @@ -0,0 +1,1744 @@ +# +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import threading + +import base64 +import gtk +import pango +from gobject import TYPE_INT, \ + TYPE_DOUBLE as TYPE_FLOAT, \ + TYPE_STRING, \ + TYPE_BOOLEAN, \ + TYPE_PYOBJECT, \ + TYPE_INT64 +import gobject +import pickle +import os +import logging + +import six + +from chirp.ui import common, shiftdialog, miscwidgets, config, memdetail +from chirp.ui import compat +from chirp.ui import bandplans +from chirp import chirp_common, errors, directory, import_logic + +LOG = logging.getLogger(__name__) + + +if __name__ == "__main__": + import sys + sys.path.insert(0, "..") + + +def handle_toggle(_, path, store, col): + store[path][col] = not store[path][col] + + +def handle_ed(_, iter, new, store, col): + old, = store.get(iter, col) + if old != new: + store.set(iter, col, new) + return True + else: + return False + + +class ValueErrorDialog(gtk.MessageDialog): + def __init__(self, exception, **args): + gtk.MessageDialog.__init__(self, buttons=gtk.BUTTONS_OK, **args) + self.set_property("text", _("Invalid value for this field")) + self.format_secondary_text(str(exception)) + + +def iter_prev(store, iter): + row = store.get_path(iter)[0] + if row == 0: + return None + return store.get_iter((row - 1,)) + + +class MemoryEditor(common.Editor): + cols = [ + (_("Loc"), TYPE_INT, gtk.CellRendererText,), + (_("Frequency"), TYPE_INT64, gtk.CellRendererText,), + (_("Name"), TYPE_STRING, gtk.CellRendererText,), + (_("Tone Mode"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Tone"), TYPE_FLOAT, gtk.CellRendererCombo,), + (_("ToneSql"), TYPE_FLOAT, gtk.CellRendererCombo,), + (_("DTCS Code"), TYPE_INT, gtk.CellRendererCombo,), + (_("DTCS Rx Code"), TYPE_INT, gtk.CellRendererCombo,), + (_("DTCS Pol"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Cross Mode"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Duplex"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Offset"), TYPE_INT64, gtk.CellRendererText,), + (_("Mode"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Power"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Tune Step"), TYPE_FLOAT, gtk.CellRendererCombo,), + (_("Skip"), TYPE_STRING, gtk.CellRendererCombo,), + (_("Comment"), TYPE_STRING, gtk.CellRendererText,), + ("_filled", TYPE_BOOLEAN, None,), + ("_hide_cols", TYPE_PYOBJECT, None,), + ("_extd", TYPE_STRING, None,), + ] + + defaults = { + _("Name"): "", + _("Frequency"): 146010000, + _("Tone"): 88.5, + _("ToneSql"): 88.5, + _("DTCS Code"): 23, + _("DTCS Rx Code"): 23, + _("DTCS Pol"): "NN", + _("Cross Mode"): "Tone->Tone", + _("Duplex"): "", + _("Offset"): 0, + _("Mode"): "FM", + _("Power"): "", + _("Tune Step"): 5.0, + _("Tone Mode"): "", + _("Skip"): "", + _("Comment"): "", + } + + choices = { + _("Tone"): chirp_common.TONES, + _("ToneSql"): chirp_common.TONES, + _("DTCS Code"): chirp_common.ALL_DTCS_CODES, + _("DTCS Rx Code"): chirp_common.ALL_DTCS_CODES, + _("DTCS Pol"): ["NN", "NR", "RN", "RR"], + _("Mode"): chirp_common.MODES, + _("Power"): [], + _("Duplex"): ["", "-", "+", "split", "off"], + _("Tune Step"): chirp_common.TUNING_STEPS, + _("Tone Mode"): ["", "Tone", "TSQL", "DTCS"], + _("Cross Mode"): chirp_common.CROSS_MODES, + } + + def ed_name(self, _, __, new, ___): + return self.rthread.radio.filter_name(new) + + def ed_offset(self, _, path, new, __): + new = chirp_common.parse_freq(new) + return abs(new) + + def ed_freq(self, _foo, path, new, colnum): + iter = self.store.get_iter(path) + was_filled, prev = self.store.get(iter, self.col("_filled"), colnum) + + def set_offset(offset): + if offset > 0: + dup = "+" + elif offset == 0: + dup = "" + else: + dup = "-" + offset *= -1 + + if dup not in self.choices[_("Duplex")]: + LOG.warn("Duplex %s not supported by this radio" % dup) + return + + if offset: + self.store.set(iter, self.col(_("Offset")), offset) + + self.store.set(iter, self.col(_("Duplex")), dup) + + def set_ts(ts): + if ts in self.choices[_("Tune Step")]: + self.store.set(iter, self.col(_("Tune Step")), ts) + else: + LOG.warn("Tune step %s not supported by this radio" % ts) + + def get_ts(path): + return self.store.get(iter, self.col(_("Tune Step")))[0] + + def set_mode(mode): + if mode in self.choices[_("Mode")]: + self.store.set(iter, self.col(_("Mode")), mode) + else: + LOG.warn("Mode %s not supported by this radio (%s)" % + (mode, self.choices[_("Mode")])) + + def set_tone(tone): + if tone in self.choices[_("Tone")]: + self.store.set(iter, self.col(_("Tone")), tone) + else: + LOG.warn("Tone %s not supported by this radio" % tone) + + try: + new = chirp_common.parse_freq(new) + except ValueError as e: + LOG.error("chirp_common.parse_freq error: %s", e) + new = None + + if not self._features.has_nostep_tuning: + set_ts(chirp_common.required_step(new)) + + is_changed = new != prev if was_filled else True + if new is not None and is_changed: + defaults = self.bandplans.get_defaults_for_frequency(new) + set_offset(defaults.offset or 0) + if defaults.step_khz: + set_ts(defaults.step_khz) + if defaults.mode: + set_mode(defaults.mode) + if defaults.tones: + set_tone(defaults.tones[0]) + + return new + + def ed_loc(self, _, path, new, __): + iter = self.store.get_iter(path) + curloc, = self.store.get(iter, self.col(_("Loc"))) + + job = common.RadioJob(None, "erase_memory", curloc) + job.set_desc(_("Erasing memory {loc}").format(loc=curloc)) + self.rthread.submit(job) + + self.need_refresh = True + + return new + + def ed_duplex(self, _foo1, path, new, _foo2): + if new == "": + return # Fast path outta here + + iter = self.store.get_iter(path) + freq, = self.store.get(iter, self.col(_("Frequency"))) + if new == "split": + # If we're going to split mode, use the current + # RX frequency as the default TX frequency + self.store.set(iter, self.col("Offset"), freq) + else: + defaults = self.bandplans.get_defaults_for_frequency(freq) + offset = defaults.offset or 0 + self.store.set(iter, self.col(_("Offset")), abs(offset)) + + return new + + def ed_tone_field(self, _foo, path, new, col): + if self._config.get_bool("no_smart_tmode"): + return new + + iter = self.store.get_iter(path) + + # Python scoping hurts us here, so store this as a list + # that we can modify, instead of helpful variables :( + modes = list(self.store.get(iter, + self.col(_("Tone Mode")), + self.col(_("Cross Mode")))) + + def _tm(*tmodes): + if modes[0] not in tmodes: + modes[0] = tmodes[0] + self.store.set(iter, self.col(_("Tone Mode")), modes[0]) + + def _cm(*cmodes): + if modes[0] == "Cross" and modes[1] not in cmodes: + modes[1] = cmodes[0] + self.store.set(iter, self.col(_("Cross Mode")), modes[1]) + + if col == self.col(_("DTCS Code")): + _tm("DTCS", "Cross") + _cm(*tuple([x for x in chirp_common.CROSS_MODES + if x.startswith("DTCS->")])) + elif col == self.col(_("DTCS Rx Code")): + _tm("Cross") + _cm(*tuple([x for x in chirp_common.CROSS_MODES + if x.endswith("->DTCS")])) + elif col == self.col(_("DTCS Pol")): + _tm("DTCS", "Cross") + _cm(*tuple([x for x in chirp_common.CROSS_MODES + if "DTCS" in x])) + elif col == self.col(_("Tone")): + _tm("Tone", "Cross") + _cm(*tuple([x for x in chirp_common.CROSS_MODES + if x.startswith("Tone->")])) + elif col == self.col(_("ToneSql")): + _tm("TSQL", "Cross") + _cm(*tuple([x for x in chirp_common.CROSS_MODES + if x.endswith("->Tone")])) + elif col == self.col(_("Cross Mode")): + _tm("Cross") + + return new + + def _get_cols_to_hide(self, iter): + tmode, duplex, cmode = self.store.get(iter, + self.col(_("Tone Mode")), + self.col(_("Duplex")), + self.col(_("Cross Mode"))) + + hide = [] + txmode, rxmode = cmode.split("->") + + if tmode == "Tone": + hide += [self.col(_("ToneSql")), + self.col(_("DTCS Code")), + self.col(_("DTCS Rx Code")), + self.col(_("DTCS Pol")), + self.col(_("Cross Mode"))] + elif tmode == "TSQL" or tmode == "TSQL-R": + if self._features.has_ctone: + hide += [self.col(_("Tone"))] + + hide += [self.col(_("DTCS Code")), + self.col(_("DTCS Rx Code")), + self.col(_("DTCS Pol")), + self.col(_("Cross Mode"))] + elif tmode == "DTCS" or tmode == "DTCS-R": + hide += [self.col(_("Tone")), + self.col(_("ToneSql")), + self.col(_("Cross Mode")), + self.col(_("DTCS Rx Code"))] + elif tmode == "" or tmode == "(None)": + hide += [self.col(_("Tone")), + self.col(_("ToneSql")), + self.col(_("DTCS Code")), + self.col(_("DTCS Rx Code")), + self.col(_("DTCS Pol")), + self.col(_("Cross Mode"))] + elif tmode == "Cross": + if txmode != "Tone": + hide += [self.col(_("Tone"))] + if txmode != "DTCS": + hide += [self.col(_("DTCS Code"))] + if rxmode != "Tone": + hide += [self.col(_("ToneSql"))] + if rxmode != "DTCS": + hide += [self.col(_("DTCS Rx Code"))] + if "DTCS" not in cmode: + hide += [self.col(_("DTCS Pol"))] + + if duplex == "" or duplex == "(None)" or duplex == "off": + hide += [self.col(_("Offset"))] + + return hide + + def maybe_hide_cols(self, iter): + hide_cols = self._get_cols_to_hide(iter) + self.store.set(iter, self.col("_hide_cols"), hide_cols) + + def edited(self, rend, path, new, cap): + if self.read_only: + common.show_error(_("Unable to make changes to this model")) + return + + iter = self.store.get_iter(path) + if not self.store.get(iter, self.col("_filled"))[0] and \ + self.store.get(iter, self.col(_("Frequency")))[0] == 0: + LOG.error(_("Editing new item, taking defaults")) + self.insert_new(iter) + + colnum = self.col(cap) + funcs = { + _("Loc"): self.ed_loc, + _("Name"): self.ed_name, + _("Frequency"): self.ed_freq, + _("Duplex"): self.ed_duplex, + _("Offset"): self.ed_offset, + _("Tone"): self.ed_tone_field, + _("ToneSql"): self.ed_tone_field, + _("DTCS Code"): self.ed_tone_field, + _("DTCS Rx Code"): self.ed_tone_field, + _("DTCS Pol"): self.ed_tone_field, + _("Cross Mode"): self.ed_tone_field, + } + + if cap in funcs: + new = funcs[cap](rend, path, new, colnum) + + if new is None: + LOG.error(_("Bad value for {col}: {val}").format(col=cap, val=new)) + return + + if self.store.get_column_type(colnum) == TYPE_INT: + new = int(new) + elif self.store.get_column_type(colnum) == TYPE_FLOAT: + new = float(new) + elif self.store.get_column_type(colnum) == TYPE_BOOLEAN: + new = bool(new) + elif self.store.get_column_type(colnum) == TYPE_STRING: + if new == "(None)": + new = "" + + if not handle_ed(rend, iter, new, self.store, self.col(cap)) and \ + cap != _("Frequency"): + # No change was made + # For frequency, we make an exception, since the handler might + # have altered the duplex. That needs to be fixed. + return + + mem = self._get_memory(iter) + + msgs = self.rthread.radio.validate_memory(mem) + if msgs: + common.show_error(_("Error setting memory") + ": " + + "\r\n\r\n".join(msgs)) + self.prefill() + return + + mem.empty = False + + job = common.RadioJob(self._set_memory_cb, "set_memory", mem) + job.set_desc(_("Writing memory {number}").format(number=mem.number)) + self.rthread.submit(job) + + self.store.set(iter, self.col("_filled"), True) + + self.maybe_hide_cols(iter) + + persist_defaults = [_("Power"), _("Frequency"), _("Mode")] + if cap in persist_defaults: + self.defaults[cap] = new + + def _render(self, colnum, val, iter=None, hide=[]): + if colnum in hide and self.hide_unused: + return "" + + if colnum == self.col(_("Frequency")): + val = chirp_common.format_freq(val) + elif colnum in [self.col(_("DTCS Code")), self.col(_("DTCS Rx Code"))]: + val = "%03i" % int(val) + elif colnum == self.col(_("Offset")): + val = chirp_common.format_freq(val) + elif colnum in [self.col(_("Tone")), self.col(_("ToneSql"))]: + val = "%.1f" % val + elif colnum in [self.col(_("Tone Mode")), self.col(_("Duplex"))]: + if not val: + val = "(None)" + elif colnum == self.col(_("Loc")) and iter is not None: + extd, = self.store.get(iter, self.col("_extd")) + if extd: + val = extd + + return str(val) + + def render(self, _, rend, model, iter, colnum): + val, hide, filled = model.get(iter, colnum, self.col("_hide_cols"), + self.col('_filled')) + val = self._render(colnum, val, iter, hide or []) + rend.set_property("text", "%s" % val) + rend.set_sensitive(filled) + + def insert_new(self, iter, loc=None): + line = [] + for key, val in self.defaults.items(): + line.append(self.col(key)) + line.append(val) + + if not loc: + loc, = self.store.get(iter, self.col(_("Loc"))) + + self.store.set(iter, + 0, loc, + *tuple(line)) + + return self._get_memory(iter) + + def insert_easy(self, store, _iter, delta): + if delta < 0: + iter = store.insert_before(_iter) + else: + iter = store.insert_after(_iter) + + newpos, = store.get(_iter, self.col(_("Loc"))) + newpos += delta + + LOG.debug("Insert easy: %i" % delta) + + mem = self.insert_new(iter, newpos) + job = common.RadioJob(None, "set_memory", mem) + job.set_desc(_("Writing memory {number}").format(number=mem.number)) + self.rthread.submit(job) + + def insert_hard(self, store, _iter, delta, warn=True): + if isinstance(self.rthread.radio, chirp_common.LiveRadio) and warn: + txt = _("This operation requires moving all subsequent channels " + "by one spot until an empty location is reached. This " + "can take a LONG time. Are you sure you want to do this?") + if not common.ask_yesno_question(txt): + return False # No change + + if delta <= 0: + iter = _iter + else: + iter = store.iter_next(_iter) + + pos, = store.get(iter, self.col(_("Loc"))) + + sd = shiftdialog.ShiftDialog(self.rthread) + + if delta == 0: + sd.delete(pos) + sd.destroy() + self.prefill() + else: + sd.insert(pos) + sd.destroy() + job = common.RadioJob( + lambda x: self.prefill(), "erase_memory", pos) + job.set_desc(_("Adding memory {number}").format(number=pos)) + self.rthread.submit(job) + + return True # We changed memories + + def _delete_rows(self, paths): + to_remove = [] + for path in paths: + iter = self.store.get_iter(path) + cur_pos, = self.store.get(iter, self.col(_("Loc"))) + to_remove.append(cur_pos) + self.store.set(iter, self.col("_filled"), False) + job = common.RadioJob(None, "erase_memory", cur_pos) + job.set_desc(_("Erasing memory {number}").format(number=cur_pos)) + self.rthread.submit(job) + + def handler(mem): + if not isinstance(mem, Exception): + if not mem.empty or self.show_empty: + gobject.idle_add(self.set_memory, mem) + + job = common.RadioJob(handler, "get_memory", cur_pos) + job.set_desc(_("Getting memory {number}").format(number=cur_pos)) + self.rthread.submit(job) + + if not self.show_empty: + # We need to actually remove the rows from the store + # now, but carefully! Get a list of deleted locations + # in order and proceed from the first path in the list + # until we run out of rows or we've removed all the + # desired ones. + to_remove.sort() + to_remove.reverse() + iter = self.store.get_iter(paths[0]) + while to_remove and iter: + pos, = self.store.get(iter, self.col(_("Loc"))) + if pos in to_remove: + to_remove.remove(pos) + if not self.store.remove(iter): + break # This was the last row + else: + iter = self.store.iter_next(iter) + + return True # We changed memories + + def _delete_rows_and_shift(self, paths, all=False): + iter = self.store.get_iter(paths[0]) + starting_loc, = self.store.get(iter, self.col(_("Loc"))) + for i in range(0, len(paths)): + sd = shiftdialog.ShiftDialog(self.rthread) + sd.delete(starting_loc, quiet=True, all=all) + sd.destroy() + + self.prefill() + return True # We changed memories + + def _move_up_down(self, paths, action): + if action.endswith("up"): + delta = -1 + donor_path = paths[-1] + victim_path = paths[0] + else: + delta = 1 + donor_path = paths[0] + victim_path = paths[-1] + + try: + victim_path = (victim_path[0] + delta,) + if victim_path[0] < 0: + raise ValueError() + donor_loc = self.store.get(self.store.get_iter(donor_path), + self.col(_("Loc")))[0] + victim_loc = self.store.get(self.store.get_iter(victim_path), + self.col(_("Loc")))[0] + except ValueError: + self.emit("usermsg", "No room to %s" % (action.replace("_", " "))) + return False # No change + + class Context: + pass + ctx = Context() + + ctx.victim_mem = None + ctx.donor_loc = donor_loc + ctx.done_count = 0 + ctx.path_count = len(paths) + + # Steps: + # 1. Grab the victim (the one that will need to be saved and moved + # from the front to the back or back to the front) and save it + # 2. Grab each memory along the way, storing it in the +delta + # destination location after we get it + # 3. If we're the final move, then schedule storing the victim + # in the hole we created + + def update_selection(): + sel = self.view.get_selection() + sel.unselect_all() + for path in paths: + gobject.idle_add(sel.select_path, (path[0]+delta,)) + + def save_victim(mem, ctx): + ctx.victim_mem = mem + + def store_victim(mem, dest): + old = mem.number + mem.number = dest + job = common.RadioJob(None, "set_memory", mem) + job.set_desc( + _("Moving memory from {old} to {new}").format(old=old, + new=dest)) + self.rthread.submit(job) + self._set_memory(self.store.get_iter(donor_path), mem) + update_selection() + + def move_mem(mem, delta, ctx, iter): + old = mem.number + mem.number += delta + job = common.RadioJob(None, "set_memory", mem) + job.set_desc( + _("Moving memory from {old} to {new}").format(old=old, + new=old+delta)) + self.rthread.submit(job) + self._set_memory(iter, mem) + ctx.done_count += 1 + if ctx.done_count == ctx.path_count: + store_victim(ctx.victim_mem, ctx.donor_loc) + + job = common.RadioJob(lambda m: save_victim(m, ctx), + "get_memory", victim_loc) + job.set_desc(_("Getting memory {number}").format(number=victim_loc)) + self.rthread.submit(job) + + for i in range(len(paths)): + path = paths[i] + if delta > 0: + dest = i+1 + else: + dest = i-1 + + if dest < 0 or dest >= len(paths): + dest = victim_path + else: + dest = paths[dest] + + iter = self.store.get_iter(path) + loc, = self.store.get(iter, self.col(_("Loc"))) + job = common.RadioJob(move_mem, "get_memory", loc) + job.set_cb_args(delta, ctx, self.store.get_iter(dest)) + job.set_desc("Getting memory %i" % loc) + self.rthread.submit(job) + + return True # We (scheduled some) change to the memories + + def _exchange_memories(self, paths): + if len(paths) != 2: + self.emit("usermsg", "Select two memories first") + return False + + loc_a, = self.store.get(self.store.get_iter(paths[0]), + self.col(_("Loc"))) + loc_b, = self.store.get(self.store.get_iter(paths[1]), + self.col(_("Loc"))) + + def store_mem(mem, dst): + src = mem.number + mem.number = dst + job = common.RadioJob(None, "set_memory", mem) + job.set_desc( + _("Moving memory from {old} to {new}").format( + old=src, new=dst)) + self.rthread.submit(job) + if dst == loc_a: + self.prefill() + + job = common.RadioJob(lambda m: store_mem(m, loc_b), + "get_memory", loc_a) + job.set_desc(_("Getting memory {number}").format(number=loc_a)) + self.rthread.submit(job) + + job = common.RadioJob(lambda m: store_mem(m, loc_a), + "get_memory", loc_b) + job.set_desc(_("Getting memory {number}").format(number=loc_b)) + self.rthread.submit(job) + + # We (scheduled some) change to the memories + return True + + def _show_raw(self, cur_pos): + def idle_show_raw(result): + gobject.idle_add(common.show_diff_blob, + _("Raw memory {number}").format( + number=cur_pos), result) + + job = common.RadioJob(idle_show_raw, "get_raw_memory", cur_pos) + job.set_desc(_("Getting raw memory {number}").format(number=cur_pos)) + self.rthread.submit(job) + + def _diff_raw(self, paths): + if len(paths) != 2: + common.show_error(_("You can only diff two memories!")) + return + + loc_a = self.store.get(self.store.get_iter(paths[0]), + self.col(_("Loc")))[0] + loc_b = self.store.get(self.store.get_iter(paths[1]), + self.col(_("Loc")))[0] + + raw = {} + + def diff_raw(which, result): + raw[which] = _("Memory {number}").format(number=which) + \ + os.linesep + result + + if len(raw.keys()) == 2: + diff = common.simple_diff(raw[loc_a], raw[loc_b]) + gobject.idle_add(common.show_diff_blob, + _("Diff of {a} and {b}").format(a=loc_a, + b=loc_b), + diff) + + job = common.RadioJob(lambda r: diff_raw(loc_a, r), + "get_raw_memory", loc_a) + job.set_desc(_("Getting raw memory {number}").format(number=loc_a)) + self.rthread.submit(job) + + job = common.RadioJob(lambda r: diff_raw(loc_b, r), + "get_raw_memory", loc_b) + job.set_desc(_("Getting raw memory {number}").format(number=loc_b)) + self.rthread.submit(job) + + def _copy_field(self, src_memory, dst_memory, field): + if field.startswith("extra_"): + field = field.split("_", 1)[1] + value = src_memory.extra[field].value.get_value() + dst_memory.extra[field].value = value + else: + setattr(dst_memory, field, getattr(src_memory, field)) + + def _apply_multiple(self, src_memory, fields, locations): + for location in locations: + def apply_and_set(memory): + for field in fields: + self._copy_field(src_memory, memory, field) + cb = (memory.number == locations[-1] and + self._set_memory_cb or None) + job = common.RadioJob(cb, "set_memory", memory) + job.set_desc(_("Writing memory {number}").format( + number=memory.number)) + self.rthread.submit(job) + job = common.RadioJob(apply_and_set, "get_memory", location) + job.set_desc(_("Getting original memory {number}").format( + number=location)) + self.rthread.submit(job) + + def edit_memory(self, memory, locations): + if len(locations) > 1: + dlg = memdetail.MultiMemoryDetailEditor(self._features, memory) + else: + dlg = memdetail.MemoryDetailEditor(self._features, memory) + r = dlg.run() + if r == gtk.RESPONSE_OK: + self.need_refresh = True + mem = dlg.get_memory() + if len(locations) > 1: + self._apply_multiple(memory, dlg.get_fields(), locations) + else: + if "name" not in mem.immutable: + mem.name = self.rthread.radio.filter_name(mem.name) + job = common.RadioJob(self._set_memory_cb, "set_memory", mem) + job.set_desc(_("Writing memory {number}").format( + number=mem.number)) + self.rthread.submit(job) + dlg.destroy() + + def mh(self, _action, store, paths): + action = _action.get_name() + selected = [] + for path in paths: + iter = store.get_iter(path) + loc, = store.get(iter, self.col(_("Loc"))) + selected.append(loc) + cur_pos = selected[0] + + require_contiguous = ["delete_s", "move_up", "move_dn"] + if action in require_contiguous: + last = paths[0][0] + for path in paths[1:]: + if path[0] != last+1: + self.emit("usermsg", _("Memories must be contiguous")) + return + last = path[0] + + changed = False + + if action == "insert_next": + changed = self.insert_hard(store, iter, 1) + elif action == "insert_prev": + changed = self.insert_hard(store, iter, -1) + elif action == "delete": + changed = self._delete_rows(paths) + elif action == "delete_s": + changed = self._delete_rows_and_shift(paths) + elif action == "delete_sall": + changed = self._delete_rows_and_shift(paths, all=True) + elif action in ["move_up", "move_dn"]: + changed = self._move_up_down(paths, action) + elif action == "exchange": + changed = self._exchange_memories(paths) + elif action in ["cut", "copy"]: + changed = self.copy_selection(action == "cut") + elif action == "paste": + changed = self.paste_selection() + elif action == "all": + changed = self.select_all() + elif action == "devshowraw": + self._show_raw(cur_pos) + elif action == "devdiffraw": + self._diff_raw(paths) + elif action == "properties": + job = common.RadioJob(self.edit_memory, "get_memory", cur_pos) + job.set_cb_args(selected) + self.rthread.submit(job) + + if changed: + self.emit("changed") + + def hotkey(self, action): + if self._in_editing: + # Don't forward potentially-dangerous hotkeys to the menu + # handler if we're editing a cell right now + return + + self.emit("usermsg", "") + (store, paths) = self.view.get_selection().get_selected_rows() + if len(paths) == 0: + return + self.mh(action, store, paths) + + def make_context_menu(self): + if self._config.get_bool("developer", "state"): + devmenu = """ + + + +""" + else: + devmenu = "" + + menu_xml = """ + + + + + + + + + + + + + + + + + + + + + %s + + +""" % devmenu + + (store, paths) = self.view.get_selection().get_selected_rows() + issingle = len(paths) == 1 + istwo = len(paths) == 2 + + actions = [ + ("cut", _("Cut")), + ("copy", _("Copy")), + ("paste", _("Paste")), + ("all", _("Select All")), + ("insert_prev", _("Insert row above")), + ("insert_next", _("Insert row below")), + ("deletes", _("Delete")), + ("delete", issingle and _("this memory") or _("these memories")), + ("delete_s", _("...and shift block up")), + ("delete_sall", _("...and shift all memories up")), + ("move_up", _("Move up")), + ("move_dn", _("Move down")), + ("exchange", _("Exchange memories")), + ("properties", _("P_roperties")), + ("devshowraw", _("Show Raw Memory")), + ("devdiffraw", _("Diff Raw Memories")), + ] + + no_multiple = ["insert_prev", "insert_next", "paste", "devshowraw"] + only_two = ["devdiffraw", "exchange"] + + ag = gtk.ActionGroup("Menu") + + for name, label in actions: + a = gtk.Action(name, label, "", 0) + a.connect("activate", self.mh, store, paths) + if name in no_multiple: + a.set_sensitive(issingle) + if name in only_two: + a.set_sensitive(istwo) + ag.add_action(a) + + if issingle: + iter = store.get_iter(paths[0]) + cur_pos, = store.get(iter, self.col(_("Loc"))) + if cur_pos == self._features.memory_bounds[1]: + ag.get_action("delete_s").set_sensitive(False) + + uim = gtk.UIManager() + uim.insert_action_group(ag, 0) + uim.add_ui_from_string(menu_xml) + + return uim.get_widget("/Menu") + + def double_click_empty_memory(self, treeiter, path, col): + loc = self.store.get(treeiter, self.col(_('Loc'))) + # Make this filled=True, thus sensitive=True + self.store.set(treeiter, self.col(_('_filled')), True) + # Set the cursor and stat editing + self.view.set_cursor(path, col, True) + + def click_cb(self, view, event): + self.emit("usermsg", "") + + pathinfo = view.get_path_at_pos(int(event.x), int(event.y)) + if pathinfo is None: + return + path, col, x, y = pathinfo + treeiter = self.store.get_iter(path) + + if hasattr(gtk.gdk.EventType, '_2BUTTON_PRESS'): + double_click = event.type == gtk.gdk.EventType._2BUTTON_PRESS + else: + double_click = False + + if event.button == 1 and double_click: + filled, = self.store.get(treeiter, self.col(_('_filled'))) + if not filled: + self.double_click_empty_memory(treeiter, path, col) + return True + elif event.button == 3: + view.set_cursor(path, col) + menu = self.make_context_menu() + try: + menu.popup(None, None, None, event.button, event.time) + except TypeError: + # GTK3 + menu.popup(None, None, None, None, event.button, + event.time) + return True + + def get_column_visible(self, col): + column = self.view.get_column(col) + return column.get_visible() + + def set_column_visible(self, col, visible): + column = self.view.get_column(col) + column.set_visible(visible) + + def cell_editing_started(self, rend, event, path): + self._in_editing = True + self._edit_path = self.view.get_cursor() + + def cell_editing_stopped(self, *args): + self._in_editing = False + self.view.grab_focus() + self.view.set_cursor(*self._edit_path) + + def make_editor(self): + types = tuple([x[1] for x in self.cols]) + self.store = gtk.ListStore(*types) + + self.view = gtk.TreeView(self.store) + self.view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + self.view.set_rules_hint(True) + + hbox = gtk.HBox() + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add(self.view) + + filled = self.col("_filled") + + default_col_order = [x for x, y, z in self.cols if z] + try: + config_setting = self._config.get("column_order_%s" % + self.__class__.__name__) + if config_setting is None: + col_order = default_col_order + else: + col_order = config_setting.split(",") + if len(col_order) != len(default_col_order): + raise Exception() + for i in col_order: + if i not in default_col_order: + raise Exception() + except Exception as e: + LOG.error("column order setting: %s", e) + col_order = default_col_order + + non_editable = [_("Loc")] + + unsupported_cols = self.get_unsupported_columns() + visible_cols = self.get_columns_visible() + + self._renderers = {} + cols = {} + i = 0 + for _cap, _type, _rend in self.cols: + if not _rend: + continue + rend = _rend() + self._renderers[_cap] = rend + rend.connect('editing-started', self.cell_editing_started) + rend.connect('editing-canceled', self.cell_editing_stopped) + rend.connect('edited', self.cell_editing_stopped) + + if _type == TYPE_BOOLEAN: + # rend.set_property("activatable", True) + # rend.connect("toggled", handle_toggle, self.store, i) + col = gtk.TreeViewColumn(_cap, rend, active=i, + sensitive=filled) + elif _rend == gtk.CellRendererCombo: + if isinstance(self.choices[_cap], gtk.ListStore): + choices = self.choices[_cap] + else: + choices = gtk.ListStore(TYPE_STRING, TYPE_STRING) + for choice in self.choices[_cap]: + choices.append([str(choice), self._render(i, choice)]) + rend.set_property("model", choices) + rend.set_property("text-column", 1) + rend.set_property("editable", True) + rend.set_property("has-entry", False) + rend.connect("edited", self.edited, _cap) + col = gtk.TreeViewColumn(_cap, rend, text=i) + col.set_cell_data_func(rend, self.render, i) + else: + rend.set_property("editable", _cap not in non_editable) + rend.connect("edited", self.edited, _cap) + col = gtk.TreeViewColumn(_cap, rend, text=i) + col.set_cell_data_func(rend, self.render, i) + + col.set_reorderable(True) + col.set_sort_column_id(i) + col.set_resizable(True) + col.set_min_width(1) + col.set_visible(not _cap.startswith("_") and + _cap in visible_cols and + _cap not in unsupported_cols) + cols[_cap] = col + i += 1 + + for cap in col_order: + self.view.append_column(cols[cap]) + + self.store.set_sort_column_id(self.col(_("Loc")), gtk.SORT_ASCENDING) + + self.view.show() + sw.show() + hbox.pack_start(sw, 1, 1, 1) + + self.view.connect("button_press_event", self.click_cb) + + hbox.show() + + return hbox + + def col(self, caption): + try: + return self._cached_cols[caption] + except KeyError: + raise Exception( + _("Internal Error: Column {name} not found").format( + name=caption)) + + def rend(self, caption): + try: + return self._renderers[caption] + except KeyError: + print(self._renderers) + raise Exception( + _('Internal Error: Renderer for column %s not found') % ( + caption)) + + def prefill(self): + self.store.clear() + self._rows_in_store = 0 + + lo = int(self.lo_limit_adj.get_value()) + hi = int(self.hi_limit_adj.get_value()) + + def handler(mem, number): + if not isinstance(mem, Exception): + if not mem.empty or self.show_empty: + gobject.idle_add(self.set_memory, mem) + else: + mem = chirp_common.Memory() + mem.number = number + mem.name = "ERROR" + mem.empty = True + gobject.idle_add(self.set_memory, mem) + + for i in range(lo, hi+1): + job = common.RadioJob(handler, "get_memory", i) + job.set_desc(_("Getting memory {number}").format(number=i)) + job.set_cb_args(i) + self.rthread.submit(job, 2) + + if self.show_special: + for i in self._features.valid_special_chans: + job = common.RadioJob(handler, "get_memory", i) + job.set_desc(_("Getting channel {chan}").format(chan=i)) + job.set_cb_args(i) + self.rthread.submit(job, 2) + + def _set_memory(self, iter, memory): + self.store.set(iter, + self.col("_filled"), not memory.empty, + self.col(_("Loc")), memory.number, + self.col("_extd"), memory.extd_number, + self.col(_("Name")), memory.name, + self.col(_("Frequency")), memory.freq, + self.col(_("Tone Mode")), memory.tmode, + self.col(_("Tone")), memory.rtone, + self.col(_("ToneSql")), memory.ctone, + self.col(_("DTCS Code")), memory.dtcs, + self.col(_("DTCS Rx Code")), memory.rx_dtcs, + self.col(_("DTCS Pol")), memory.dtcs_polarity, + self.col(_("Cross Mode")), memory.cross_mode, + self.col(_("Duplex")), memory.duplex, + self.col(_("Offset")), memory.offset, + self.col(_("Mode")), memory.mode, + self.col(_("Power")), (memory.power and + str(memory.power) or + ""), + self.col(_("Tune Step")), memory.tuning_step, + self.col(_("Skip")), memory.skip, + self.col(_("Comment")), memory.comment) + + hide = self._get_cols_to_hide(iter) + self.store.set(iter, self.col("_hide_cols"), hide) + + def set_memory(self, memory): + iter = self.store.get_iter_first() + + while iter is not None: + loc, = self.store.get(iter, self.col(_("Loc"))) + if loc == memory.number: + return self._set_memory(iter, memory) + + iter = self.store.iter_next(iter) + + iter = self.store.append() + self._rows_in_store += 1 + self._set_memory(iter, memory) + + def clear_memory(self, number): + iter = self.store.get_iter_first() + while iter: + loc, = self.store.get(iter, self.col(_("Loc"))) + if loc == number: + LOG.debug("Deleting %i" % number) + # FIXME: Make the actual remove happen on callback + self.store.remove(iter) + job = common.RadioJob(None, "erase_memory", number) + job.set_desc( + _("Erasing memory {number}").format(number=number)) + self.rthread.submit() + break + iter = self.store.iter_next(iter) + + def _set_mem_vals(self, mem, vals, iter): + power_levels = {"": None} + for i in self._features.valid_power_levels: + power_levels[str(i)] = i + + mem.freq = vals[self.col(_("Frequency"))] + mem.number = vals[self.col(_("Loc"))] + mem.extd_number = vals[self.col("_extd")] + mem.name = vals[self.col(_("Name"))] + mem.vfo = 0 + mem.rtone = vals[self.col(_("Tone"))] + mem.ctone = vals[self.col(_("ToneSql"))] + mem.dtcs = vals[self.col(_("DTCS Code"))] + mem.rx_dtcs = vals[self.col(_("DTCS Rx Code"))] + mem.tmode = vals[self.col(_("Tone Mode"))] + mem.cross_mode = vals[self.col(_("Cross Mode"))] + mem.dtcs_polarity = vals[self.col(_("DTCS Pol"))] + mem.duplex = vals[self.col(_("Duplex"))] + mem.offset = vals[self.col(_("Offset"))] + mem.mode = vals[self.col(_("Mode"))] + mem.power = power_levels[vals[self.col(_("Power"))]] + mem.tuning_step = vals[self.col(_("Tune Step"))] + mem.skip = vals[self.col(_("Skip"))] + mem.comment = vals[self.col(_("Comment"))] + mem.empty = not vals[self.col("_filled")] + + def _get_memory(self, iter): + vals = self.store.get(iter, *range(0, len(self.cols))) + mem = chirp_common.Memory() + self._set_mem_vals(mem, vals, iter) + + return mem + + def _limit_key(self, which): + if which not in ["lo", "hi"]: + raise Exception(_("Internal Error: Invalid limit {number}").format( + number=which)) + return "%s_%s" % \ + (directory.radio_class_id(self.rthread.radio.__class__), which) + + def _store_limit(self, sb, which): + self._config.set_int(self._limit_key(which), int(sb.get_value())) + + def make_controls(self, min, max): + hbox = gtk.HBox(False, 2) + + lab = gtk.Label(_("Memory Range:")) + lab.show() + hbox.pack_start(lab, 0, 0, 0) + + lokey = self._limit_key("lo") + hikey = self._limit_key("hi") + lostart = self._config.is_defined(lokey) and \ + self._config.get_int(lokey) or min + histart = self._config.is_defined(hikey) and \ + self._config.get_int(hikey) or 999 + + self.lo_limit_adj = gtk.Adjustment(lostart, min, max-1, 1, 10) + lo = compat.SpinButton(self.lo_limit_adj) + lo.set_value(lostart) + lo.connect("value-changed", self._store_limit, "lo") + lo.show() + hbox.pack_start(lo, 0, 0, 0) + + lab = gtk.Label(" - ") + lab.show() + hbox.pack_start(lab, 0, 0, 0) + + self.hi_limit_adj = gtk.Adjustment(histart, min+1, max, 1, 10) + hi = compat.SpinButton(self.hi_limit_adj) + hi.set_value(histart) + hi.connect("value-changed", self._store_limit, "hi") + hi.show() + hbox.pack_start(hi, 0, 0, 0) + + refresh = gtk.Button(_("Refresh")) + refresh.set_relief(gtk.RELIEF_NONE) + refresh.connect("clicked", lambda x: self.prefill()) + refresh.show() + hbox.pack_start(refresh, 0, 0, 0) + + def activate_go(widget): + refresh.clicked() + + def set_hi(widget, event): + loval = self.lo_limit_adj.get_value() + hival = self.hi_limit_adj.get_value() + if loval >= hival: + self.hi_limit_adj.set_value(loval + 25) + + lo.connect_after("focus-out-event", set_hi) + lo.connect_after("activate", activate_go) + hi.connect_after("activate", activate_go) + + sep = gtk.VSeparator() + sep.show() + hbox.pack_start(sep, 0, 0, 2) + + showspecial = gtk.ToggleButton(_("Special Channels")) + showspecial.set_relief(gtk.RELIEF_NONE) + showspecial.set_active(self.show_special) + showspecial.connect("toggled", + lambda x: self.set_show_special(x.get_active())) + showspecial.show() + hbox.pack_start(showspecial, 0, 0, 0) + + showempty = gtk.ToggleButton(_("Show Empty")) + showempty.set_relief(gtk.RELIEF_NONE) + showempty.set_active(self.show_empty) + showempty.connect("toggled", + lambda x: self.set_show_empty(x.get_active())) + showempty.show() + hbox.pack_start(showempty, 0, 0, 0) + + sep = gtk.VSeparator() + sep.show() + hbox.pack_start(sep, 0, 0, 2) + + props = gtk.Button(_("Properties")) + props.set_relief(gtk.RELIEF_NONE) + props.connect("clicked", + lambda x: self.hotkey( + gtk.Action("properties", "", "", 0))) + props.show() + hbox.pack_start(props, 0, 0, 0) + + hbox.show() + + return hbox + + def set_show_special(self, show): + self.show_special = show + self.prefill() + self._config.set_bool("show_special", show) + + def set_show_empty(self, show): + self.show_empty = show + self.prefill() + self._config.set_bool("hide_empty", not show) + + def set_hide_unused(self, hide_unused): + self.hide_unused = hide_unused + self.prefill() + self._config.set_bool("hide_unused", hide_unused) + + def __cache_columns(self): + # We call self.col() a lot. Caching the name->column# lookup + # makes a significant performance improvement + self._cached_cols = {} + i = 0 + for x in self.cols: + self._cached_cols[x[0]] = i + i += 1 + + def get_unsupported_columns(self): + maybe_hide = [ + ("has_dtcs", _("DTCS Code")), + ("has_rx_dtcs", _("DTCS Rx Code")), + ("has_dtcs_polarity", _("DTCS Pol")), + ("has_mode", _("Mode")), + ("has_offset", _("Offset")), + ("has_name", _("Name")), + ("has_tuning_step", _("Tune Step")), + ("has_name", _("Name")), + ("has_ctone", _("ToneSql")), + ("has_cross", _("Cross Mode")), + ("has_comment", _("Comment")), + ("valid_tmodes", _("Tone Mode")), + ("valid_tmodes", _("Tone")), + ("valid_duplexes", _("Duplex")), + ("valid_skips", _("Skip")), + ("valid_power_levels", _("Power")), + ] + + unsupported = [] + for feature, colname in maybe_hide: + if feature.startswith("has_"): + supported = self._features[feature] + LOG.info("%s supported: %s" % (colname, supported)) + elif feature.startswith("valid_"): + supported = len(self._features[feature]) != 0 + + if not supported: + unsupported.append(colname) + + return unsupported + + def get_columns_visible(self): + unsupported = self.get_unsupported_columns() + driver = directory.radio_class_id(self.rthread.radio.__class__) + user_visible = self._config.get(driver, "memedit_columns") + if user_visible: + user_visible = user_visible.split(",") + else: + # No setting for this radio, so assume all + user_visible = [x[0] for x in self.cols if x not in unsupported] + return user_visible + + def __init__(self, rthread): + super(MemoryEditor, self).__init__(rthread) + + self.defaults = dict(self.defaults) + + self._config = config.get("memedit") + + self.bandplans = bandplans.BandPlans(config.get()) + + self.allowed_bands = [144, 440] + self.count = 100 + self.show_special = self._config.get_bool("show_special") + self.show_empty = not self._config.get_bool("hide_empty") + self.hide_unused = self._config.get_bool("hide_unused", default=True) + self.read_only = False + + self.need_refresh = False + self._in_editing = False + + self.lo_limit_adj = self.hi_limit_adj = None + self.store = self.view = None + + self.__cache_columns() + + self._features = self.rthread.radio.get_features() + + (min, max) = self._features.memory_bounds + + self.choices[_("Mode")] = self._features["valid_modes"] + self.choices[_("Tone Mode")] = self._features["valid_tmodes"] + self.choices[_("Cross Mode")] = self._features["valid_cross_modes"] + self.choices[_("Skip")] = self._features["valid_skips"] + self.choices[_("Power")] = [str(x) for x in + self._features["valid_power_levels"]] + self.choices[_("DTCS Pol")] = self._features["valid_dtcs_pols"] + self.choices[_("DTCS Code")] = self._features["valid_dtcs_codes"] + self.choices[_("DTCS Rx Code")] = self._features["valid_dtcs_codes"] + + if self._features["valid_power_levels"]: + self.defaults[_("Power")] = self._features["valid_power_levels"][0] + + self.choices[_("Tune Step")] = self._features["valid_tuning_steps"] + + self.choices[_("Duplex")] = list(self._features.valid_duplexes) + + if self.defaults[_("Mode")] not in self._features.valid_modes: + self.defaults[_("Mode")] = self._features.valid_modes[0] + + vbox = gtk.VBox(False, 2) + vbox.pack_start(self.make_controls(min, max), 0, 0, 0) + vbox.pack_start(self.make_editor(), 1, 1, 1) + vbox.show() + + self.prefill() + + self.root = vbox + + # Run low priority jobs to get the rest of the memories + hi = int(self.hi_limit_adj.get_value()) + for i in range(hi, max+1): + job = common.RadioJob(None, "get_memory", i) + job.set_desc(_("Getting memory {number}").format(number=i)) + self.rthread.submit(job, 10) + + def _set_memory_cb(self, result): + if isinstance(result, Exception): + # FIXME: This can't be in the thread + dlg = ValueErrorDialog(result) + dlg.run() + dlg.destroy() + self.prefill() + elif self.need_refresh: + self.prefill() + self.need_refresh = False + + self.emit('changed') + + def copy_selection(self, cut=False): + (store, paths) = self.view.get_selection().get_selected_rows() + + maybe_cut = [] + selection = [] + + for path in paths: + iter = store.get_iter(path) + mem = self._get_memory(iter) + selection.append(mem.dupe()) + maybe_cut.append((iter, mem)) + + if cut: + for iter, mem in maybe_cut: + mem.empty = True + job = common.RadioJob(self._set_memory_cb, + "erase_memory", mem.number) + job.set_desc( + _("Cutting memory {number}").format(number=mem.number)) + self.rthread.submit(job) + + self._set_memory(iter, mem) + + result = base64.b64encode(pickle.dumps((self._features, + selection))).decode() + if hasattr(gtk.Clipboard, 'get'): + # GTK3 + clipboard = gtk.Clipboard.get(gtk.gdk.SELECTION_CLIPBOARD) + clipboard.set_text(result, len(result)) + else: + # GTK2 + clipboard = gtk.Clipboard(selection="CLIPBOARD") + clipboard.set_text(result) + clipboard.store() + + return cut # Only changed if we did a cut + + def _paste_selection(self, clipboard, text, data): + if not text: + return + + (store, paths) = self.view.get_selection().get_selected_rows() + if len(paths) > 1: + common.show_error("To paste, select only the starting location") + return + + iter = store.get_iter(paths[0]) + + always = False + + try: + src_features, mem_list = pickle.loads(base64.b64decode(text)) + except Exception: + LOG.error("Paste failed to unpickle") + return + + if (paths[0][0] + len(mem_list)) > self._rows_in_store: + common.show_error(_("Unable to paste {src} memories into " + "{dst} rows. Increase the memory bounds " + "or show empty memories.").format( + src=len(mem_list), + dst=(self._rows_in_store - paths[0][0]))) + return + + for mem in mem_list: + if mem.empty: + iter = self.store.iter_next(iter) + continue + loc, filled = store.get(iter, + self.col(_("Loc")), self.col("_filled")) + if filled and not always: + d = miscwidgets.YesNoDialog(title=_("Overwrite?"), + buttons=(gtk.STOCK_YES, 1, + gtk.STOCK_NO, 2, + gtk.STOCK_CANCEL, 3, + "All", 4)) + d.set_text( + _("Overwrite location {number}?").format(number=loc)) + r = d.run() + d.destroy() + if r == 4: + always = True + elif r == 3: + break + elif r == 2: + iter = store.iter_next(iter) + continue + + mem.name = self.rthread.radio.filter_name(mem.name) + + src_number = mem.number + mem.number = loc + + try: + mem = import_logic.import_mem(self.rthread.radio, + src_features, + mem) + except import_logic.DestNotCompatible: + msgs = self.rthread.radio.validate_memory(mem) + errs = [x for x in msgs + if isinstance(x, chirp_common.ValidationError)] + if errs: + d = miscwidgets.YesNoDialog(title=_("Incompatible Memory"), + buttons=(gtk.STOCK_OK, 1, + gtk.STOCK_CANCEL, 2)) + d.set_text( + _("Pasted memory {number} is not compatible with " + "this radio because:").format(number=src_number) + + os.linesep + os.linesep.join(msgs)) + r = d.run() + d.destroy() + if r == 2: + break + else: + iter = store.iter_next(iter) + continue + + self._set_memory(iter, mem) + iter = store.iter_next(iter) + + job = common.RadioJob(self._set_memory_cb, "set_memory", mem) + job.set_desc( + _("Writing memory {number}").format(number=mem.number)) + self.rthread.submit(job) + + def paste_selection(self): + if hasattr(gtk.Clipboard, 'get'): + # GTK3 + clipboard = gtk.Clipboard.get(gtk.gdk.SELECTION_CLIPBOARD) + text = clipboard.wait_for_text() + self._paste_selection(clipboard, text, None) + else: + # GTK2 + clipboard = gtk.Clipboard(selection="CLIPBOARD") + clipboard.request_text(self._paste_selection) + + def select_all(self): + self.view.get_selection().select_all() + + def prepare_close(self): + cols = self.view.get_columns() + self._config.set("column_order_%s" % self.__class__.__name__, + ",".join([x.get_title() for x in cols])) + + def other_editor_changed(self, target_editor): + self.need_refresh = True + + +class DstarMemoryEditor(MemoryEditor): + def _get_cols_to_hide(self, iter): + hide = MemoryEditor._get_cols_to_hide(self, iter) + + mode, = self.store.get(iter, self.col(_("Mode"))) + if mode != "DV": + hide += [self.col("URCALL"), + self.col("RPT1CALL"), + self.col("RPT2CALL")] + + return hide + + def render(self, null, rend, model, iter, colnum): + MemoryEditor.render(self, null, rend, model, iter, colnum) + + vals = model.get(iter, *tuple(range(0, len(self.cols)))) + val = vals[colnum] + + def _enabled(sensitive): + rend.set_property("sensitive", sensitive) + + def d_unless_mode(mode): + _enabled(vals[self.col(_("Mode"))] == mode) + + _dv_columns = [_("URCALL"), _("RPT1CALL"), _("RPT2CALL"), + _("Digital Code")] + dv_columns = [self.col(x) for x in _dv_columns] + if colnum in dv_columns: + d_unless_mode("DV") + + def _get_memory(self, iter): + vals = self.store.get(iter, *range(0, len(self.cols))) + if vals[self.col(_("Mode"))] != "DV": + return MemoryEditor._get_memory(self, iter) + + mem = chirp_common.DVMemory() + + MemoryEditor._set_mem_vals(self, mem, vals, iter) + + mem.dv_urcall = vals[self.col(_("URCALL"))] + mem.dv_rpt1call = vals[self.col(_("RPT1CALL"))] + mem.dv_rpt2call = vals[self.col(_("RPT2CALL"))] + mem.dv_code = vals[self.col(_("Digital Code"))] + + return mem + + def __init__(self, rthread): + # I think self.cols is "static" or "unbound" or something else + # like that and += modifies the type, not self (how bizarre) + self.cols = list(self.cols) + new_cols = [("URCALL", TYPE_STRING, gtk.CellRendererCombo), + ("RPT1CALL", TYPE_STRING, gtk.CellRendererCombo), + ("RPT2CALL", TYPE_STRING, gtk.CellRendererCombo), + ("Digital Code", TYPE_INT, gtk.CellRendererText), + ] + for col in new_cols: + index = self.cols.index(("_filled", TYPE_BOOLEAN, None)) + self.cols.insert(index, col) + + self.choices = dict(self.choices) + self.defaults = dict(self.defaults) + + self.choices["URCALL"] = gtk.ListStore(TYPE_STRING, TYPE_STRING) + self.choices["RPT1CALL"] = gtk.ListStore(TYPE_STRING, TYPE_STRING) + self.choices["RPT2CALL"] = gtk.ListStore(TYPE_STRING, TYPE_STRING) + + self.defaults["URCALL"] = "" + self.defaults["RPT1CALL"] = "" + self.defaults["RPT2CALL"] = "" + self.defaults["Digital Code"] = 0 + + MemoryEditor.__init__(self, rthread) + + def ucall_cb(calls): + self.defaults["URCALL"] = calls[0] + for call in calls: + self.choices["URCALL"].append((call, call)) + + if self._features.requires_call_lists: + ujob = common.RadioJob(ucall_cb, "get_urcall_list") + ujob.set_desc(_("Downloading URCALL list")) + rthread.submit(ujob) + + def rcall_cb(calls): + self.defaults["RPT1CALL"] = calls[0] + self.defaults["RPT2CALL"] = calls[0] + for call in calls: + self.choices["RPT1CALL"].append((call, call)) + self.choices["RPT2CALL"].append((call, call)) + + if self._features.requires_call_lists: + rjob = common.RadioJob(rcall_cb, "get_repeater_call_list") + rjob.set_desc(_("Downloading RPTCALL list")) + rthread.submit(rjob) + + _dv_columns = ["URCALL", "RPT1CALL", "RPT2CALL", "Digital Code"] + + if not self._features.requires_call_lists: + for i in _dv_columns: + if i not in self.choices: + continue + rend = self.rend(i) + rend.set_property("has-entry", True) + + for i in _dv_columns: + rend = self.rend(i) + rend.set_property("family", "Monospace") + + def set_urcall_list(self, urcalls): + store = self.choices["URCALL"] + + store.clear() + for call in urcalls: + store.append((call, call)) + + def set_repeater_list(self, repeaters): + for listname in ["RPT1CALL", "RPT2CALL"]: + store = self.choices[listname] + + store.clear() + for call in repeaters: + store.append((call, call)) + + def _set_memory(self, iter, memory): + MemoryEditor._set_memory(self, iter, memory) + + if isinstance(memory, chirp_common.DVMemory): + self.store.set(iter, + self.col("URCALL"), memory.dv_urcall, + self.col("RPT1CALL"), memory.dv_rpt1call, + self.col("RPT2CALL"), memory.dv_rpt2call, + self.col("Digital Code"), memory.dv_code, + ) + else: + self.store.set(iter, + self.col("URCALL"), "", + self.col("RPT1CALL"), "", + self.col("RPT2CALL"), "", + self.col("Digital Code"), 0, + ) + + +class ID800MemoryEditor(DstarMemoryEditor): + pass diff --git a/chirp/ui/miscwidgets.py b/chirp/ui/miscwidgets.py new file mode 100644 index 0000000..e68ae2a --- /dev/null +++ b/chirp/ui/miscwidgets.py @@ -0,0 +1,879 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import abc +import gtk +import gobject +import pango + +import os +import logging + +import six + +from chirp import platform +from chirp.ui import compat +from chirp.ui import common + +LOG = logging.getLogger(__name__) + + +class KeyedListWidget(gtk.HBox): + __gsignals__ = { + "item-selected": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + "item-toggled": (gobject.SIGNAL_ACTION, + gobject.TYPE_BOOLEAN, + (gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)), + "item-set": (gobject.SIGNAL_ACTION, + gobject.TYPE_BOOLEAN, + (gobject.TYPE_STRING,)), + } + + def _toggle(self, rend, path, colnum): + if self.__toggle_connected: + self.__store[path][colnum] = not self.__store[path][colnum] + iter = self.__store.get_iter(path) + id, = self.__store.get(iter, 0) + self.emit("item-toggled", id, self.__store[path][colnum]) + + def _edited(self, rend, path, new, colnum): + iter = self.__store.get_iter(path) + key, oldval = self.__store.get(iter, 0, colnum) + self.__store.set(iter, colnum, new) + if not self.emit("item-set", key): + self.__store.set(iter, colnum, oldval) + + def _mouse(self, view, event): + x, y = event.get_coords() + path = self.__view.get_path_at_pos(int(x), int(y)) + if path: + self.__view.set_cursor_on_cell(path[0]) + + sel = self.get_selected() + if sel: + self.emit("item-selected", sel) + + def _make_view(self): + colnum = -1 + + self._renderers = [] + + for typ, cap in self.columns: + colnum += 1 + if colnum == 0: + continue # Key column + + if typ in [gobject.TYPE_STRING, gobject.TYPE_INT, + gobject.TYPE_FLOAT]: + rend = gtk.CellRendererText() + rend.set_property("ellipsize", pango.ELLIPSIZE_END) + column = gtk.TreeViewColumn(cap, rend, text=colnum) + elif typ in [gobject.TYPE_BOOLEAN]: + rend = gtk.CellRendererToggle() + rend.connect("toggled", self._toggle, colnum) + column = gtk.TreeViewColumn(cap, rend, active=colnum) + else: + raise Exception("Unsupported type %s" % typ) + + self._renderers.append(rend) + + column.set_sort_column_id(colnum) + self.__view.append_column(column) + + self.__view.connect("button_press_event", self._mouse) + + def set_item(self, key, *values): + iter = self.__store.get_iter_first() + while iter: + id, = self.__store.get(iter, 0) + if id == key: + self.__store.insert_after(iter, row=(id,)+values) + self.__store.remove(iter) + return + iter = self.__store.iter_next(iter) + + self.__store.append(row=(key,) + values) + + self.emit("item-set", key) + + def get_item(self, key): + iter = self.__store.get_iter_first() + while iter: + vals = self.__store.get(iter, *tuple(range(len(self.columns)))) + if vals[0] == key: + return vals + iter = self.__store.iter_next(iter) + + return None + + def del_item(self, key): + iter = self.__store.get_iter_first() + while iter: + id, = self.__store.get(iter, 0) + if id == key: + self.__store.remove(iter) + return True + + iter = self.__store.iter_next(iter) + + return False + + def has_item(self, key): + return self.get_item(key) is not None + + def get_selected(self): + try: + (store, iter) = self.__view.get_selection().get_selected() + return store.get(iter, 0)[0] + except Exception as e: + LOG.error("Unable to find selected: %s" % e) + return None + + def select_item(self, key): + if key is None: + sel = self.__view.get_selection() + sel.unselect_all() + return True + + iter = self.__store.get_iter_first() + while iter: + if self.__store.get(iter, 0)[0] == key: + selection = self.__view.get_selection() + path = self.__store.get_path(iter) + selection.select_path(path) + return True + iter = self.__store.iter_next(iter) + + return False + + def get_keys(self): + keys = [] + iter = self.__store.get_iter_first() + while iter: + key, = self.__store.get(iter, 0) + keys.append(key) + iter = self.__store.iter_next(iter) + + return keys + + def __init__(self, columns): + gtk.HBox.__init__(self, True, 0) + + self.columns = columns + + types = tuple([x for x, y in columns]) + + self.__store = gtk.ListStore(*types) + self.__view = gtk.TreeView(self.__store) + + self.pack_start(self.__view, 1, 1, 1) + + self.__toggle_connected = False + + self._make_view() + self.__view.show() + + def connect(self, signame, *args): + if signame == "item-toggled": + self.__toggle_connected = True + + gtk.HBox.connect(self, signame, *args) + + def set_editable(self, column, is_editable): + rend = self.get_renderer(column) + rend.set_property("editable", True) + rend.connect("edited", self._edited, column + 1) + + def set_sort_column(self, column, value=None): + if not value: + value = column + col = self.__view.get_column(column) + col.set_sort_column_id(value) + + def get_renderer(self, colnum): + return self._renderers[colnum] + + +class ListWidget(gtk.HBox): + __gsignals__ = { + "click-on-list": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gtk.TreeView, gtk.gdk.Event)), + "item-toggled": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + } + + store_type = gtk.ListStore + + def mouse_cb(self, view, event): + self.emit("click-on-list", view, event) + + # pylint: disable-msg=W0613 + def _toggle(self, render, path, column): + self._store[path][column] = not self._store[path][column] + iter = self._store.get_iter(path) + vals = tuple(self._store.get(iter, *tuple(range(self._ncols)))) + for cb in self.toggle_cb: + cb(*vals) + self.emit("item-toggled", vals) + + def make_view(self, columns): + self._view = gtk.TreeView(self._store) + + for _type, _col in columns: + if _col.startswith("__"): + continue + + index = columns.index((_type, _col)) + if _type == gobject.TYPE_STRING or \ + _type == gobject.TYPE_INT or \ + _type == gobject.TYPE_FLOAT: + rend = gtk.CellRendererText() + column = gtk.TreeViewColumn(_col, rend, text=index) + column.set_resizable(True) + rend.set_property("ellipsize", pango.ELLIPSIZE_END) + elif _type == gobject.TYPE_BOOLEAN: + rend = gtk.CellRendererToggle() + rend.connect("toggled", self._toggle, index) + column = gtk.TreeViewColumn(_col, rend, active=index) + else: + raise Exception("Unknown column type (%i)" % index) + + column.set_sort_column_id(index) + self._view.append_column(column) + + self._view.connect("button_press_event", self.mouse_cb) + + def __init__(self, columns, parent=True): + gtk.HBox.__init__(self) + + # pylint: disable-msg=W0612 + col_types = tuple([x for x, y in columns]) + self._ncols = len(col_types) + + self._store = self.store_type(*col_types) + self._view = None + self.make_view(columns) + + self._view.show() + if parent: + self.pack_start(self._view, 1, 1, 1) + + self.toggle_cb = [] + + def packable(self): + return self._view + + def add_item(self, *vals): + if len(vals) != self._ncols: + raise Exception("Need %i columns" % self._ncols) + + args = [] + i = 0 + for val in vals: + args.append(i) + args.append(val) + i += 1 + + args = tuple(args) + + iter = self._store.append() + self._store.set(iter, *args) + + def _remove_item(self, model, path, iter, match): + vals = model.get(iter, *tuple(range(0, self._ncols))) + if vals == match: + model.remove(iter) + + def remove_item(self, *vals): + if len(vals) != self._ncols: + raise Exception("Need %i columns" % self._ncols) + + def remove_selected(self): + try: + (lst, iter) = self._view.get_selection().get_selected() + lst.remove(iter) + except Exception as e: + LOG.error("Unable to remove selected: %s" % e) + + def get_selected(self, take_default=False): + (lst, iter) = self._view.get_selection().get_selected() + if not iter and take_default: + iter = lst.get_iter_first() + + return lst.get(iter, *tuple(range(self._ncols))) + + def move_selected(self, delta): + (lst, iter) = self._view.get_selection().get_selected() + + pos = int(lst.get_path(iter)[0]) + + try: + target = None + + if delta > 0 and pos > 0: + target = lst.get_iter(pos-1) + elif delta < 0: + target = lst.get_iter(pos+1) + except Exception as e: + return False + + if target: + return lst.swap(iter, target) + + def _get_value(self, model, path, iter, lst): + lst.append(model.get(iter, *tuple(range(0, self._ncols)))) + + def get_values(self): + lst = [] + + self._store.foreach(self._get_value, lst) + + return lst + + def set_values(self, lst): + self._store.clear() + + for i in lst: + self.add_item(*i) + + +class TreeWidget(ListWidget): + store_type = gtk.TreeStore + + # pylint: disable-msg=W0613 + def _toggle(self, render, path, column): + self._store[path][column] = not self._store[path][column] + iter = self._store.get_iter(path) + vals = tuple(self._store.get(iter, *tuple(range(self._ncols)))) + + piter = self._store.iter_parent(iter) + if piter: + parent = self._store.get(piter, self._key)[0] + else: + parent = None + + for cb in self.toggle_cb: + cb(parent, *vals) + + def __init__(self, columns, key, parent=True): + ListWidget.__init__(self, columns, parent) + + self._key = key + + def _add_item(self, piter, *vals): + args = [] + i = 0 + for val in vals: + args.append(i) + args.append(val) + i += 1 + + args = tuple(args) + + iter = self._store.append(piter) + self._store.set(iter, *args) + + def _iter_of(self, key, iter=None): + if not iter: + iter = self._store.get_iter_first() + + while iter is not None: + _id = self._store.get(iter, self._key)[0] + if _id == key: + return iter + + iter = self._store.iter_next(iter) + + return None + + def add_item(self, parent, *vals): + if len(vals) != self._ncols: + raise Exception("Need %i columns" % self._ncols) + + if not parent: + self._add_item(None, *vals) + else: + iter = self._iter_of(parent) + if iter: + self._add_item(iter, *vals) + else: + raise Exception("Parent not found: %s", parent) + + def _set_values(self, parent, vals): + if isinstance(vals, dict): + for key, val in vals.items(): + iter = self._store.append(parent) + self._store.set(iter, self._key, key) + self._set_values(iter, val) + elif isinstance(vals, list): + for i in vals: + self._set_values(parent, i) + elif isinstance(vals, tuple): + self._add_item(parent, *vals) + else: + LOG.error("Unknown type: %s" % vals) + + def set_values(self, vals): + self._store.clear() + self._set_values(self._store.get_iter_first(), vals) + + def del_item(self, parent, key): + iter = self._iter_of(key, + self._store.iter_children(self._iter_of(parent))) + if iter: + self._store.remove(iter) + else: + raise Exception("Item not found") + + def get_item(self, parent, key): + iter = self._iter_of(key, + self._store.iter_children(self._iter_of(parent))) + + if iter: + return self._store.get(iter, *(tuple(range(0, self._ncols)))) + else: + raise Exception("Item not found") + + def set_item(self, parent, *vals): + iter = self._iter_of(vals[self._key], + self._store.iter_children(self._iter_of(parent))) + + if iter: + args = [] + i = 0 + + for val in vals: + args.append(i) + args.append(val) + i += 1 + + self._store.set(iter, *(tuple(args))) + else: + raise Exception("Item not found") + + +class ProgressDialog(gtk.Window): + def __init__(self, title, parent=None): + gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) + self.set_modal(True) + self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.set_title(title) + if parent: + self.set_transient_for(parent) + + self.set_resizable(False) + + vbox = gtk.VBox(False, 2) + + self.label = gtk.Label("") + self.label.set_size_request(100, 50) + self.label.show() + + self.pbar = gtk.ProgressBar() + self.pbar.show() + + vbox.pack_start(self.label, 0, 0, 0) + vbox.pack_start(self.pbar, 0, 0, 0) + + vbox.show() + + self.add(vbox) + + def set_text(self, text): + self.label.set_text(text) + self.queue_draw() + + while gtk.events_pending(): + gtk.main_iteration_do(False) + + def set_fraction(self, frac): + self.pbar.set_fraction(frac) + self.queue_draw() + + while gtk.events_pending(): + gtk.main_iteration_do(False) + + +class LatLonEntry(gtk.Entry): + def __init__(self, *args): + gtk.Entry.__init__(self, *args) + + self.connect("changed", self.format) + + def format(self, entry): + string = entry.get_text() + if string is None: + return + + deg = u"\u00b0" + + while " " in string: + if "." in string: + break + elif deg not in string: + string = string.replace(" ", deg) + elif "'" not in string: + string = string.replace(" ", "'") + elif '"' not in string: + string = string.replace(" ", '"') + else: + string = string.replace(" ", "") + + entry.set_text(string) + + def parse_dd(self, string): + return float(string) + + def parse_dm(self, string): + string = string.strip() + string = string.replace(' ', ' ') + + (_degrees, _minutes) = string.split(' ', 2) + + degrees = int(_degrees) + minutes = float(_minutes) + + return degrees + (minutes / 60.0) + + def parse_dms(self, string): + string = string.replace(u"\u00b0", " ") + string = string.replace('"', ' ') + string = string.replace("'", ' ') + string = string.replace(' ', ' ') + string = string.strip() + + items = string.split(' ') + + if len(items) > 3: + raise Exception("Invalid format") + elif len(items) == 3: + deg = items[0] + mns = items[1] + sec = items[2] + elif len(items) == 2: + deg = items[0] + mns = items[1] + sec = 0 + elif len(items) == 1: + deg = items[0] + mns = 0 + sec = 0 + else: + deg = 0 + mns = 0 + sec = 0 + + degrees = int(deg) + minutes = int(mns) + seconds = float(sec) + + return degrees + (minutes / 60.0) + (seconds / 3600.0) + + def value(self): + string = self.get_text() + + try: + return self.parse_dd(string) + except: + try: + return self.parse_dm(string) + except: + try: + return self.parse_dms(string) + except Exception as e: + LOG.error("DMS: %s" % e) + + raise Exception("Invalid format") + + def validate(self): + try: + self.value() + return True + except: + return False + + +class YesNoDialog(gtk.Dialog): + def __init__(self, title="", parent=None, buttons=None): + gtk.Dialog.__init__(self, title=title, parent=parent, buttons=buttons) + + self._label = gtk.Label("") + self._label.show() + + # pylint: disable-msg=E1101 + self.vbox.pack_start(self._label, 1, 1, 1) + + def set_text(self, text): + self._label.set_text(text) + + +@six.add_metaclass(abc.ABCMeta) +class EditableChoiceBase(object): + def __init__(self, options, editable, default): + pass + + @property + @abc.abstractmethod + def widget(self): + raise NotImplementedError() + + @property + @abc.abstractmethod + def value(self): + pass + + @value.setter + @abc.abstractmethod + def value(self): + pass + + @abc.abstractmethod + def get_model(self): + pass + + @abc.abstractmethod + def append_text(self, text): + """Add a choice option""" + pass + + def set_active(self, text): + """Legacy compat""" + self.value = text + + def get_active_text(self): + """Legacy compat""" + return self.value + + def set_sensitive(self, sensitive): + self.widget.set_sensitive(sensitive) + + def connect(self, signal, fn, data): + self.widget.connect(signal, lambda *a: fn(self, data)) + + +class EditableChoiceGTK3(EditableChoiceBase): + def __init__(self, options, editable, default): + self.store = gtk.ListStore(str) + for i, option in enumerate(options): + self.store.append([option]) + if editable: + self.sel = gtk.ComboBox.new_with_model_and_entry(self.store) + self.sel.set_entry_text_column(0) + else: + self.sel = gtk.ComboBox.new_with_model(self.store) + renderer_text = gtk.CellRendererText() + self.sel.pack_start(renderer_text, True) + self.sel.add_attribute(renderer_text, "text", 0) + if default: + self.value = default + + @property + def widget(self): + return self.sel + + @property + def value(self): + modeliter = self.sel.get_active_iter() + if modeliter: + return self.store[modeliter][0] + else: + LOG.warning('No active iter') + + @value.setter + def value(self, val): + modeliter = self.store.get_iter_first() + while modeliter is not None: + row = self.store[modeliter] + if row[0] == val: + self.sel.set_active_iter(modeliter) + break + modeliter = self.store.iter_next(modeliter) + + def get_model(self): + return self.store + + def append_text(self, text): + self.store.append([text]) + + +class EditableChoiceGTK2(EditableChoiceBase): + def __init__(self, options, editable, default): + if editable: + self.sel = gtk.combo_box_entry_new_text() + else: + self.sel = gtk.combo_box_new_text() + + for opt in options: + self.sel.append_text(opt) + + if default: + try: + idx = options.index(default) + self.sel.set_active(idx) + except Exception as e: + pass + + @property + def widget(self): + return self.sel + + @property + def value(self): + store = self.sel.get_model() + modeliter = self.sel.get_active_iter() + return store[modeliter][0] + + @value.setter + def value(self, val): + common.combo_select(self.sel, val) + + def get_model(self): + return self.sel.get_model() + + def append_text(self, text): + self.sel.append_text(text) + + +def make_choice(options, editable=True, default=None): + if hasattr(gtk.ComboBox, 'new_with_entry'): + return EditableChoiceGTK3(options, editable, default) + else: + return EditableChoiceGTK2(options, editable, default) + + +class FilenameBox(gtk.HBox): + __gsignals__ = { + "filename-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def do_browse(self, _, dir): + if self.filename.get_text(): + start = os.path.dirname(self.filename.get_text()) + else: + start = None + + if dir: + fn = platform.get_platform().gui_select_dir(start) + else: + fn = platform.get_platform().gui_save_file(start, types=self.types) + if fn: + self.filename.set_text(fn) + + def do_changed(self, _): + self.emit("filename_changed") + + def __init__(self, find_dir=False, types=[]): + gtk.HBox.__init__(self, False, 0) + + self.types = types + + self.filename = gtk.Entry() + self.filename.show() + self.pack_start(self.filename, 1, 1, 1) + + browse = gtk.Button("...") + browse.show() + self.pack_start(browse, 0, 0, 0) + + self.filename.connect("changed", self.do_changed) + browse.connect("clicked", self.do_browse, find_dir) + + def set_filename(self, fn): + self.filename.set_text(fn) + + def get_filename(self): + return self.filename.get_text() + + +def make_pixbuf_choice(options, default=None): + store = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) + box = gtk.ComboBox(store) + + cell = gtk.CellRendererPixbuf() + box.pack_start(cell, True) + box.add_attribute(cell, "pixbuf", 0) + + cell = gtk.CellRendererText() + box.pack_start(cell, True) + box.add_attribute(cell, "text", 1) + + _default = None + for pic, value in options: + iter = store.append() + store.set(iter, 0, pic, 1, value) + if default == value: + _default = options.index((pic, value)) + + if _default: + box.set_active(_default) + + return box + + +def test(): + win = gtk.Window(gtk.WINDOW_TOPLEVEL) + lst = ListWidget([(gobject.TYPE_STRING, "Foo"), + (gobject.TYPE_BOOLEAN, "Bar")]) + + lst.add_item("Test1", True) + lst.set_values([("Test2", True), ("Test3", False)]) + + lst.show() + win.add(lst) + win.show() + + win1 = ProgressDialog("foo") + win1.show() + + win2 = gtk.Window(gtk.WINDOW_TOPLEVEL) + lle = LatLonEntry() + lle.show() + win2.add(lle) + win2.show() + + win3 = gtk.Window(gtk.WINDOW_TOPLEVEL) + lst = TreeWidget([(gobject.TYPE_STRING, "Id"), + (gobject.TYPE_STRING, "Value")], + 1) + lst.set_values({"Fruit": [("Apple", "Red"), ("Orange", "Orange")], + "Pizza": [("Cheese", "Simple"), ("Pepperoni", "Yummy")]}) + lst.add_item("Fruit", "Bananna", "Yellow") + lst.show() + win3.add(lst) + win3.show() + + def print_val(entry): + if entry.validate(): + print("Valid: %s" % entry.value()) + else: + print("Invalid") + lle.connect("activate", print_val) + + lle.set_text("45 13 12") + + try: + gtk.main() + except KeyboardInterrupt: + pass + + print(lst.get_values()) + + +if __name__ == "__main__": + test() diff --git a/chirp/ui/radiobrowser.py b/chirp/ui/radiobrowser.py new file mode 100644 index 0000000..90b2981 --- /dev/null +++ b/chirp/ui/radiobrowser.py @@ -0,0 +1,350 @@ +import gtk +import gobject +import pango +import re +import os +import logging + +from chirp import bitwise +from chirp.ui import common, config + +LOG = logging.getLogger(__name__) + +CONF = config.get() + + +def do_insert_line_with_tags(b, line): + def i(text, *tags): + b.insert_with_tags_by_name(b.get_end_iter(), text, *tags) + + def ident(name): + if "unknown" in name: + i(name, 'grey', 'bold') + else: + i(name, 'bold') + + def nonzero(value): + i(value, 'red', 'bold') + + def foo(value): + i(value, 'blue', 'bold') + + m = re.match("^( *)([A-z0-9_]+: )(0x[A-F0-9]+) \((.*)\)$", line) + if m: + i(m.group(1)) + ident(m.group(2)) + if m.group(3) == '0x00': + i(m.group(3)) + else: + nonzero(m.group(3)) + i(' (') + for char in m.group(4): + if char == '1': + nonzero(char) + else: + i(char) + i(')') + return + + m = re.match("^( *)([A-z0-9_]+: )(.*)$", line) + if m: + i(m.group(1)) + ident(m.group(2)) + i(m.group(3)) + return + + m = re.match("^(.*} )([A-z0-9_]+)( \()([0-9]+)( bytes at )(0x[A-F0-9]+)", + line) + if m: + i(m.group(1)) + ident(m.group(2)) + i(m.group(3)) + foo(m.group(4)) + i(m.group(5)) + foo(m.group(6)) + i(")") + return + + i(line) + + +def do_insert_with_tags(buf, text): + buf.set_text('') + lines = text.split(os.linesep) + for line in lines: + do_insert_line_with_tags(buf, line) + buf.insert_with_tags_by_name(buf.get_end_iter(), os.linesep) + + +def classname(obj): + return str(obj.__class__).split('.')[-1] + + +def bitwise_type(classname): + return classname.split("DataElement")[0] + + +class FixedEntry(gtk.Entry): + def __init__(self, *args, **kwargs): + super(FixedEntry, self).__init__(*args, **kwargs) + + try: + fontsize = CONF.get_int("browser_fontsize", "developer") + except Exception: + fontsize = 10 + if fontsize < 4 or fontsize > 144: + LOG.warn("Unsupported browser_fontsize %i. Using 10." % fontsize) + fontsize = 11 + + fontdesc = pango.FontDescription("Courier bold %i" % fontsize) + self.modify_font(fontdesc) + + +class IntegerEntry(FixedEntry): + def _colorize(self, _self): + value = self.get_text() + if value.startswith("0x"): + value = value[2:] + value = value.replace("0", "") + if not value: + self.modify_text(gtk.STATE_NORMAL, None) + else: + self.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('red')) + + def __init__(self, *args, **kwargs): + super(IntegerEntry, self).__init__(*args, **kwargs) + self.connect("changed", self._colorize) + + +class BitwiseEditor(gtk.HBox): + def __init__(self, element): + super(BitwiseEditor, self).__init__(False, 3) + self._element = element + self._build_ui() + + +class IntegerEditor(BitwiseEditor): + def _changed(self, entry, base): + if not self._update: + return + value = entry.get_text() + if value.startswith("0x"): + value = value[2:] + self._element.set_value(int(value, base)) + self._update_entries(skip=entry) + + def _update_entries(self, skip=None): + self._update = False + for ent, format_spec in self._entries: + if ent != skip: + ent.set_text(format_spec.format(int(self._element))) + self._update = True + + def _build_ui(self): + self._entries = [] + self._update = True + + hexdigits = ((self._element.size() / 4) + + (self._element.size() % 4 and 1 or 0)) + formats = [('Hex', 16, '0x{:0%iX}' % hexdigits), + ('Dec', 10, '{:d}'), + ('Bin', 2, '{:0%ib}' % self._element.size())] + for name, base, format_spec in formats: + lab = gtk.Label(name) + self.pack_start(lab, 0, 0, 0) + lab.show() + int(self._element) + ent = IntegerEntry() + self._entries.append((ent, format_spec)) + ent.connect('changed', self._changed, base) + self.pack_start(ent, 0, 0, 0) + ent.show() + self._update_entries() + + +class BCDArrayEditor(BitwiseEditor): + def _changed(self, entry, hexent): + self._element.set_value(int(entry.get_text())) + self._format_hexent(hexent) + + def _format_hexent(self, hexent): + value = "" + for i in self._element: + a, b = i.get_value() + value += "%i%i" % (a, b) + hexent.set_text(value) + + def _build_ui(self): + lab = gtk.Label("Dec") + lab.show() + self.pack_start(lab, 0, 0, 0) + ent = FixedEntry() + ent.set_text(str(int(self._element))) + ent.show() + self.pack_start(ent, 1, 1, 1) + + lab = gtk.Label("Hex") + lab.show() + self.pack_start(lab, 0, 0, 0) + + hexent = FixedEntry() + hexent.show() + self.pack_start(hexent, 1, 1, 1) + hexent.set_editable(False) + + ent.connect('changed', self._changed, hexent) + self._format_hexent(hexent) + + +class CharArrayEditor(BitwiseEditor): + def _changed(self, entry): + self._element.set_value(entry.get_text().ljust(len(self._element))) + + def _build_ui(self): + ent = FixedEntry(len(self._element)) + ent.set_text(str(self._element).rstrip("\x00")) + ent.connect('changed', self._changed) + ent.show() + self.pack_start(ent, 1, 1, 1) + + +class OtherEditor(BitwiseEditor): + def _build_ui(self): + name = classname(self._element) + name = bitwise_type(name) + if isinstance(self._element, bitwise.arrayDataElement): + name += " %s[%i]" % ( + bitwise_type(classname(self._element[0])), + len(self._element)) + + l = gtk.Label(name) + l.show() + self.pack_start(l, 1, 1, 1) + + +class RadioBrowser(common.Editor): + def _build_ui(self): + self._display = gtk.Table(20, 2) + + self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT) + self._tree = gtk.TreeView(self._store) + + rend = gtk.CellRendererText() + tvc = gtk.TreeViewColumn('Element', rend, text=0) + self._tree.append_column(tvc) + self._tree.connect('button_press_event', self._tree_click) + + self.root = gtk.HPaned() + self.root.set_position(200) + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add(self._tree) + sw.show() + self.root.add1(sw) + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.add_with_viewport(self._display) + sw.show() + self.root.add2(sw) + self._tree.show() + self._display.show() + self.root.show() + + def _fill(self, name, obj, parent=None): + iter = self._store.append(parent, (name, obj)) + + if isinstance(obj, bitwise.structDataElement): + for name, item in obj.items(): + if isinstance(item, bitwise.structDataElement): + self._fill(name, item, iter) + elif isinstance(item, bitwise.arrayDataElement): + self._fill("%s[%i]" % (name, len(item)), item, iter) + elif isinstance(obj, bitwise.arrayDataElement): + i = 0 + for item in obj: + if isinstance(obj[0], bitwise.structDataElement): + self._fill("%s[%i]" % (name, i), item, iter) + i += 1 + + def _tree_click(self, view, event): + if event.button != 1: + return + + index = [0] + + def pack(widget, pos): + self._display.attach(widget, pos, pos + 1, index[0], index[0] + 1, + xoptions=gtk.FILL, yoptions=0) + + def next_row(): + index[0] += 1 + + def abandon(child): + self._display.remove(child) + + pathinfo = view.get_path_at_pos(int(event.x), int(event.y)) + path = pathinfo[0] + iter = self._store.get_iter(path) + name, obj = self._store.get(iter, 0, 1) + + self._display.foreach(abandon) + + for name, item in obj.items(): + if item.size() % 8 == 0: + name = '%s (%s %i bytes)' % ( + name, bitwise_type(classname(item)), item.size() / 8) + else: + name = '%s (%s %i bits)' % ( + name, bitwise_type(classname(item)), item.size()) + l = gtk.Label(name + " ") + l.set_use_markup(True) + l.show() + pack(l, 0) + + if (isinstance(item, bitwise.intDataElement) or + isinstance(item, bitwise.bcdDataElement)): + e = IntegerEditor(item) + elif (isinstance(item, bitwise.arrayDataElement) and + isinstance(item[0], bitwise.bcdDataElement)): + e = BCDArrayEditor(item) + elif (isinstance(item, bitwise.arrayDataElement) and + isinstance(item[0], bitwise.charDataElement)): + e = CharArrayEditor(item) + else: + e = OtherEditor(item) + e.show() + pack(e, 1) + next_row() + + def __init__(self, rthread): + super(RadioBrowser, self).__init__(rthread) + self._radio = rthread.radio + self._focused = False + self._build_ui() + self._fill('root', self._radio._memobj) + + def focus(self): + self._focused = True + + def unfocus(self): + if self._focused: + self.emit("changed") + self._focused = False + + +if __name__ == "__main__": + from chirp.drivers import * + from chirp import directory + import sys + + r = directory.get_radio_by_image(sys.argv[1]) + + class Foo: + radio = r + + w = gtk.Window() + b = RadioBrowser(Foo) + w.set_default_size(1024, 768) + w.add(b.root) + w.show() + gtk.main() diff --git a/chirp/ui/reporting.py b/chirp/ui/reporting.py new file mode 100644 index 0000000..ad52be9 --- /dev/null +++ b/chirp/ui/reporting.py @@ -0,0 +1,195 @@ +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# README: +# +# I know that collecting data is not very popular. I don't like it +# either. However, it's hard to tell what drivers people are using +# and I think it would be helpful if I had that information. This is +# completely optional, so you can turn it off if you want. It doesn't +# report anything other than version and model usage information. The +# code below is very conservative, and will disable itself if reporting +# fails even once or takes too long to perform. It's run in a thread +# so that the user shouldn't even notice it's happening. +# + +import threading +import os +import time +import logging + +from chirp import CHIRP_VERSION, platform + +REPORT_URL = "http://chirp.danplanet.com/report/report.php?do_report" +ENABLED = True +THREAD_SEM = threading.Semaphore(10) # Maximum number of outstanding threads +LAST = 0 +LAST_TYPE = None + +LOG = logging.getLogger(__name__) + +try: + # Don't let failure to import any of these modules cause trouble + from chirp.ui import config + import xmlrpclib +except: + ENABLED = False + + +def should_report(): + if not ENABLED: + LOG.info("Not reporting due to recent failure") + return False + + conf = config.get() + if conf.get_bool("no_report"): + LOG.info("Reporting disabled") + return False + + return True + + +def _report_model_usage(model, direction, success): + global ENABLED + if direction not in ["live", "download", "upload", + "import", "export", "importsrc"]: + LOG.warn("Invalid direction `%s'" % direction) + return True # This is a bug, but not fatal + + model = "%s_%s" % (model.VENDOR, model.MODEL) + data = "%s,%s,%s" % (model, direction, success) + + LOG.debug("Reporting model usage: %s" % data) + + proxy = xmlrpclib.ServerProxy(REPORT_URL) + id = proxy.report_stats(CHIRP_VERSION, + platform.get_platform().os_version_string(), + "model_use", + data) + + # If the server returns zero, it wants us to shut up + return id != 0 + + +def _report_exception(stack): + global ENABLED + + LOG.debug("Reporting exception") + + proxy = xmlrpclib.ServerProxy(REPORT_URL) + id = proxy.report_exception(CHIRP_VERSION, + platform.get_platform().os_version_string(), + "exception", + stack) + + # If the server returns zero, it wants us to shut up + return id != 0 + + +def _report_misc_error(module, data): + global ENABLED + + LOG.debug("Reporting misc error with %s" % module) + + proxy = xmlrpclib.ServerProxy(REPORT_URL) + id = proxy.report_misc_error(CHIRP_VERSION, + platform.get_platform().os_version_string(), + module, data) + + # If the server returns zero, it wants us to shut up + return id != 0 + + +def _check_for_updates(callback): + LOG.debug("Checking for updates") + proxy = xmlrpclib.ServerProxy(REPORT_URL) + ver = proxy.check_for_updates(CHIRP_VERSION, + platform.get_platform().os_version_string()) + + LOG.debug("Server reports version %s is latest" % ver) + callback(ver) + return True + + +class ReportThread(threading.Thread): + def __init__(self, func, *args): + threading.Thread.__init__(self) + self.__func = func + self.__args = args + + def _run(self): + try: + return self.__func(*self.__args) + except Exception as e: + LOG.debug("Failed to report: %s" % e) + return False + + def run(self): + start = time.time() + result = self._run() + if not result: + # Reporting failed + ENABLED = False + elif (time.time() - start) > 15: + # Reporting took too long + LOG.debug("Time to report was %.2f sec -- Disabling" % + (time.time()-start)) + ENABLED = False + + THREAD_SEM.release() + + +def dispatch_thread(func, *args): + global LAST + global LAST_TYPE + + # If reporting is disabled or failing, bail + if not should_report(): + LOG.debug("Reporting is disabled") + return + + # If the time between now and the last report is less than 5 seconds, bail + delta = time.time() - LAST + if delta < 5 and func == LAST_TYPE: + LOG.debug("Throttling...") + return + + LAST = time.time() + LAST_TYPE = func + + # If there are already too many threads running, bail + if not THREAD_SEM.acquire(False): + LOG.debug("Too many threads already running") + return + + t = ReportThread(func, *args) + t.start() + + +def report_model_usage(model, direction, success): + dispatch_thread(_report_model_usage, model, direction, success) + + +def report_exception(stack): + dispatch_thread(_report_exception, stack) + + +def report_misc_error(module, data): + dispatch_thread(_report_misc_error, module, data) + + +# Calls callback with the latest version +def check_for_updates(callback): + dispatch_thread(_check_for_updates, callback) diff --git a/chirp/ui/settingsedit.py b/chirp/ui/settingsedit.py new file mode 100644 index 0000000..60272b3 --- /dev/null +++ b/chirp/ui/settingsedit.py @@ -0,0 +1,232 @@ +# Copyright 2012 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +import logging + +from chirp import chirp_common +from chirp import settings +from chirp.ui import common, miscwidgets + +LOG = logging.getLogger(__name__) + + +class RadioSettingProxy(settings.RadioSetting): + + def __init__(self, setting, editor): + self._setting = setting + self._editor = editor + + +class SettingsEditor(common.Editor): + + def __init__(self, rthread): + super(SettingsEditor, self).__init__(rthread) + + # The main box + self.root = gtk.HBox(False, 0) + + # The pane + paned = gtk.HPaned() + paned.show() + self.root.pack_start(paned, 1, 1, 0) + + # The selection tree + self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_INT) + self._view = gtk.TreeView(self._store) + self._view.get_selection().connect("changed", self._view_changed_cb) + self._view.append_column( + gtk.TreeViewColumn("", gtk.CellRendererText(), text=0)) + self._view.show() + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled_window.add_with_viewport(self._view) + scrolled_window.set_size_request(200, -1) + scrolled_window.show() + paned.pack1(scrolled_window) + + # The settings notebook + self._notebook = gtk.Notebook() + self._notebook.set_show_tabs(False) + self._notebook.set_show_border(False) + self._notebook.show() + paned.pack2(self._notebook) + + self._changed = False + self._settings = None + + job = common.RadioJob(self._get_settings_cb, "get_settings") + job.set_desc("Getting radio settings") + self.rthread.submit(job) + + def _save_settings(self): + if self._settings is None: + return + + def setting_cb(result): + if isinstance(result, Exception): + common.show_error(_("Error in setting value: %s") % result) + elif self._changed: + self.emit("changed") + self._changed = False + + job = common.RadioJob(setting_cb, "set_settings", + self._settings) + job.set_desc("Setting radio settings") + self.rthread.submit(job) + + def _do_save_setting(self, widget, value): + if isinstance(value, settings.RadioSettingValueInteger): + value.set_value(widget.get_adjustment().get_value()) + elif isinstance(value, settings.RadioSettingValueFloat): + value.set_value(widget.get_text()) + elif isinstance(value, settings.RadioSettingValueBoolean): + value.set_value(widget.get_active()) + elif isinstance(value, settings.RadioSettingValueList): + value.set_value(widget.value) + elif isinstance(value, settings.RadioSettingValueString): + value.set_value(widget.get_text()) + else: + LOG.error("Unsupported widget type %s for %s" % + (element.value.__class__, element.get_name())) + + self._changed = True + self._save_settings() + + def _save_setting(self, widget, value): + try: + self._do_save_setting(widget, value) + except settings.InvalidValueError as e: + common.show_error(_("Invalid setting value: %s") % e) + + def _build_ui_tab(self, group): + + # The scrolled window + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.show() + + # Notebook tab + tab = self._notebook.append_page(sw, gtk.Label(_(group.get_name()))) + + # Settings table + table = gtk.Table(len(group), 2, False) + table.set_resize_mode(gtk.RESIZE_IMMEDIATE) + table.show() + sw.add_with_viewport(table) + + row = 0 + for element in group: + if not isinstance(element, settings.RadioSetting): + continue + + # Label + label = gtk.Label(element.get_shortname() + ":") + label.set_alignment(0.0, 0.5) + label.show() + + table.attach(label, 0, 1, row, row + 1, + xoptions=gtk.FILL, yoptions=0, + xpadding=6, ypadding=3) + + if isinstance(element.value, list) and \ + isinstance(element.value[0], + settings.RadioSettingValueInteger): + box = gtk.HBox(True) + else: + box = gtk.VBox(True) + + # Widget container + box.show() + table.attach(box, 1, 2, row, row + 1, + xoptions=gtk.FILL, yoptions=0, + xpadding=12, ypadding=3) + + for i in list(element.keys()): + value = element[i] + if isinstance(value, settings.RadioSettingValueInteger): + widget = gtk.SpinButton() + adj = widget.get_adjustment() + adj.configure(value.get_value(), + value.get_min(), value.get_max(), + value.get_step(), 1, 0) + widget.connect("value-changed", self._save_setting, value) + elif isinstance(value, settings.RadioSettingValueFloat): + widget = gtk.Entry() + widget.set_width_chars(16) + widget.set_text(value.format()) + widget.connect("focus-out-event", lambda w, e, v: + self._save_setting(w, v), value) + elif isinstance(value, settings.RadioSettingValueBoolean): + widget = gtk.CheckButton(_("Enabled")) + widget.set_active(value.get_value()) + widget.connect("toggled", self._save_setting, value) + elif isinstance(value, settings.RadioSettingValueList): + choice = miscwidgets.make_choice([], editable=False) + model = choice.get_model() + model.clear() + for option in value.get_options(): + choice.append_text(option) + choice.value = value.get_value() + widget = choice.widget + choice.connect("changed", self._save_setting, value) + elif isinstance(value, settings.RadioSettingValueString): + widget = gtk.Entry() + widget.set_width_chars(32) + widget.set_text(str(value).rstrip()) + widget.connect("focus-out-event", lambda w, e, v: + self._save_setting(w, v), value) + else: + LOG.error("Unsupported widget type: %s" % value.__class__) + + widget.set_sensitive(value.get_mutable()) + label.set_mnemonic_widget(widget) + widget.get_accessible().set_name(element.get_shortname()) + widget.show() + + box.pack_start(widget, 1, 1, 1) + + row += 1 + + return tab + + def _build_ui_group(self, group, parent): + tab = self._build_ui_tab(group) + + iter = self._store.append(parent) + self._store.set(iter, 0, group.get_shortname(), 1, tab) + + for element in group: + if not isinstance(element, settings.RadioSetting): + self._build_ui_group(element, iter) + + def _build_ui(self, settings): + if not isinstance(settings, list): + raise Exception("Invalid Radio Settings") + return + + self._settings = settings + for group in settings: + self._build_ui_group(group, None) + self._view.expand_all() + + def _get_settings_cb(self, settings): + gobject.idle_add(self._build_ui, settings) + + def _view_changed_cb(self, selection): + (lst, iter) = selection.get_selected() + tab, = self._store.get(iter, 1) + self._notebook.set_current_page(tab) diff --git a/chirp/ui/shiftdialog.py b/chirp/ui/shiftdialog.py new file mode 100644 index 0000000..98fdca1 --- /dev/null +++ b/chirp/ui/shiftdialog.py @@ -0,0 +1,161 @@ +# +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import gtk +import gobject +import threading +import logging + +from chirp import errors, chirp_common + +LOG = logging.getLogger(__name__) + + +class ShiftDialog(gtk.Dialog): + def __init__(self, rthread, parent=None): + gtk.Dialog.__init__(self, + title=_("Shift"), + buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)) + + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + + self.rthread = rthread + + self.__prog = gtk.ProgressBar() + self.__prog.show() + + self.__labl = gtk.Label("") + self.__labl.show() + + self.vbox.pack_start(self.__prog, 1, 1, 1) + self.vbox.pack_start(self.__labl, 0, 0, 0) + + self.quiet = False + + self.thread = None + + def _status(self, msg, prog): + self.__labl.set_text(msg) + self.__prog.set_fraction(prog) + + def status(self, msg, prog): + gobject.idle_add(self._status, msg, prog) + + def _shift_memories(self, delta, memories): + count = 0.0 + for i in memories: + src = i.number + dst = src + delta + + LOG.info("Moving %i to %i" % (src, dst)) + self.status(_("Moving {src} to {dst}").format(src=src, + dst=dst), + count / len(memories)) + + i.number = dst + if i.empty: + self.rthread.radio.erase_memory(i.number) + else: + self.rthread.radio.set_memory(i) + count += 1.0 + + return int(count) + + def _get_mems_until_hole(self, start, endokay=False, all=False): + mems = [] + + llimit, ulimit = self.rthread.radio.get_features().memory_bounds + + pos = start + while pos <= ulimit: + self.status(_("Looking for a free spot ({number})").format( + number=pos), 0) + try: + mem = self.rthread.radio.get_memory(pos) + if mem.empty and not all: + break + except errors.InvalidMemoryLocation: + break + + mems.append(mem) + pos += 1 + + if pos > ulimit and not endokay: + raise errors.InvalidMemoryLocation(_("No space to insert a row")) + + LOG.debug("Found a hole: %i" % pos) + + return mems + + def _insert_hole(self, start): + mems = self._get_mems_until_hole(start) + mems.reverse() + if mems: + ret = self._shift_memories(1, mems) + if ret: + # Clear the hole we made + self.rthread.radio.erase_memory(start) + return ret + else: + LOG.warn("No memory list?") + return 0 + + def _delete_hole(self, start, all=False): + mems = self._get_mems_until_hole(start+1, endokay=True, all=all) + if mems: + count = self._shift_memories(-1, mems) + self.rthread.radio.erase_memory(count+start) + return count + else: + LOG.warn("No memory list?") + return 0 + + def finished(self): + if self.quiet: + gobject.idle_add(self.response, gtk.RESPONSE_OK) + else: + gobject.idle_add(self.set_response_sensitive, + gtk.RESPONSE_OK, True) + + def threadfn(self, newhole, func, *args): + self.status("Waiting for radio to become available", 0) + self.rthread.lock() + + try: + count = func(newhole, *args) + except errors.InvalidMemoryLocation as e: + self.status(str(e), 0) + self.finished() + return + + self.rthread.unlock() + self.status(_("Moved {count} memories").format(count=count), 1) + + self.finished() + + def insert(self, newhole, quiet=False): + self.quiet = quiet + self.thread = threading.Thread(target=self.threadfn, + args=(newhole, self._insert_hole)) + self.thread.start() + gtk.Dialog.run(self) + + def delete(self, newhole, quiet=False, all=False): + self.quiet = quiet + self.thread = threading.Thread(target=self.threadfn, + args=(newhole, self._delete_hole, all)) + self.thread.start() + gtk.Dialog.run(self) diff --git a/chirp/util.py b/chirp/util.py new file mode 100644 index 0000000..001f749 --- /dev/null +++ b/chirp/util.py @@ -0,0 +1,146 @@ +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import six +import struct + + +def hexprint(data, addrfmt=None): + """Return a hexdump-like encoding of @data""" + if addrfmt is None: + addrfmt = '%(addr)03i' + + block_size = 8 + + lines = len(data) // block_size + + if six.PY3 and isinstance(data, bytes): + pad = bytes([0]) + else: + pad = '\x00' + + if (len(data) % block_size) != 0: + lines += 1 + data += pad * ((lines * block_size) - len(data)) + + out = "" + + if six.PY3 and isinstance(data, bytes): + byte_to_int = lambda b: b + else: + byte_to_int = ord + + for block in range(0, (len(data) // block_size)): + addr = block * block_size + try: + out += addrfmt % locals() + except (OverflowError, ValueError, TypeError, KeyError): + out += "%03i" % addr + out += ': ' + + left = len(data) - (block * block_size) + if left < block_size: + limit = left + else: + limit = block_size + + for j in range(0, limit): + byte = data[(block * block_size) + j] + if isinstance(byte, str): + byte = ord(byte) + out += "%02x " % byte + + out += " " + + for j in range(0, limit): + byte = data[(block * block_size) + j] + if isinstance(byte, str): + byte = ord(byte) + if byte > 0x20 and byte < 0x7E: + out += "%s" % chr(byte) + else: + out += "." + + out += "\n" + + return out + + +def bcd_encode(val, bigendian=True, width=None): + """This is really old and shouldn't be used anymore""" + digits = [] + while val != 0: + digits.append(val % 10) + val /= 10 + + result = "" + + if len(digits) % 2 != 0: + digits.append(0) + + while width and width > len(digits): + digits.append(0) + + for i in range(0, len(digits), 2): + newval = struct.pack("B", (digits[i + 1] << 4) | digits[i]) + if bigendian: + result = newval + result + else: + result = result + newval + + return result + + +def get_dict_rev(thedict, value): + """Return the first matching key for a given @value in @dict""" + _dict = {} + for k, v in thedict.items(): + _dict[v] = k + return _dict[value] + + +def safe_charset_string(indexes, charset, safechar=" "): + """Return a string from an array of charset indexes, + replaces out of charset values with safechar""" + assert safechar in charset + _string = "" + for i in indexes: + try: + _string += charset[i] + except IndexError: + _string += safechar + return _string + + +class StringStruct(object): + """String-compatible struct module.""" + @staticmethod + def pack(*args): + from chirp import bitwise + fmt = args[0] + # Encode any string arguments to bytes + newargs = (bitwise.string_straight_encode(x) if isinstance(x, str) + else x + for x in args[1:]) + return bitwise.string_straight_decode(struct.pack(fmt, *newargs)) + + @staticmethod + def unpack(fmt, data): + from chirp import bitwise + result = struct.unpack(fmt, bitwise.string_straight_encode(data)) + # Decode any string results + return tuple(bitwise.string_straight_decode(x) if isinstance(x, bytes) + else x + for x in result) diff --git a/chirp/wxui/__init__.py b/chirp/wxui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chirp/wxui/clone.py b/chirp/wxui/clone.py new file mode 100644 index 0000000..e2a2a51 --- /dev/null +++ b/chirp/wxui/clone.py @@ -0,0 +1,276 @@ +import collections +import logging +import threading + +import serial +import wx + +from chirp import chirp_common +from chirp import directory +from chirp import platform +from chirp.ui import config +from chirp.wxui import common + +LOG = logging.getLogger(__name__) +CONF = config.get() + + +class CloneThread(threading.Thread): + def __init__(self, radio, dialog, fn): + super(CloneThread, self).__init__() + self._radio = radio + self._dialog = dialog + self._fn = getattr(self._radio, fn) + self._radio.status_fn = self._status + + def _status(self, status): + self._dialog._status(status) + + def run(self): + try: + self._fn() + except Exception as e: + LOG.error('Failed to clone: %s' % e) + self._dialog.fail(str(e)) + else: + self._dialog.complete() + + +class SettingsThread(threading.Thread): + def __init__(self, radio, progdialog, settings=None): + super(SettingsThread, self).__init__() + self._radio = radio + self._dialog = progdialog + self.settings = settings + self._dialog.SetRange(100) + self.error = None + + def run(self): + self._radio.pipe.open() + try: + self._run() + except Exception as e: + LOG.exception('Failed during setting operation') + wx.CallAfter(self._dialog.Update, 100) + self.error = str(e) + finally: + self._radio.pipe.close() + + def _run(self): + if self.settings: + msg = _('Applying settings') + else: + msg = _('Loading settings') + wx.CallAfter(self._dialog.Update, 10, newmsg=msg) + if self.settings: + self._radio.set_settings(self.settings) + else: + self.settings = self._radio.get_settings() + wx.CallAfter(self._dialog.Update, 100, newmsg=_('Complete')) + + +class ChirpCloneDialog(wx.Dialog): + def __init__(self, *a, **k): + super(ChirpCloneDialog, self).__init__( + *a, title='Communicate with radio', **k) + + panel = wx.Panel(self) + + try: + grid = wx.FlexGridSizer(3, 2) + except TypeError: + grid = wx.FlexGridSizer(2, 5, 0) + + def _add_grid(label, control): + grid.Add(wx.StaticText(panel, label=label), + border=20, flag=wx.ALIGN_CENTER|wx.RIGHT|wx.LEFT) + grid.Add(control, 1, flag=wx.EXPAND) + + ports = platform.get_platform().list_serial_ports() + last_port = CONF.get('last_port', 'state') + if last_port and last_port not in ports: + ports.insert(0, last_port) + elif not last_port: + last_port = ports[0] + self._port = wx.ComboBox(panel, choices=ports) + self._port.SetValue(last_port) + self.Bind(wx.EVT_COMBOBOX, self._selected_port, self._port) + _add_grid('Port', self._port) + + self._vendor = wx.Choice(panel, choices=['Icom', 'Yaesu']) + _add_grid('Vendor', self._vendor) + self.Bind(wx.EVT_CHOICE, self._selected_vendor, self._vendor) + + self._model = wx.Choice(panel, choices=[]) + _add_grid('Model', self._model) + self.Bind(wx.EVT_CHOICE, self._selected_model, self._model) + + self.gauge = wx.Gauge(panel) + + hbox2 = wx.BoxSizer(wx.HORIZONTAL) + + action = self._make_action(panel) + + hbox2.Add(action) + self._action_button = action + + cancel = wx.Button(panel, wx.ID_CANCEL) + hbox2.Add(cancel, flag=wx.RIGHT) + + vbox = wx.BoxSizer(wx.VERTICAL) + vbox.Add(grid, proportion=0, flag=wx.ALIGN_CENTER|wx.TOP|wx.BOTTOM, + border=20) + vbox.Add(self.gauge, flag=wx.EXPAND, border=10, proportion=1) + vbox.Add(wx.StaticLine(panel), flag=wx.EXPAND|wx.TOP, border=10) + vbox.Add(hbox2, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=10) + panel.SetSizer(vbox) + + self._vendors = collections.defaultdict(list) + for rclass in directory.DRV_TO_RADIO.values(): + if (not issubclass(rclass, chirp_common.CloneModeRadio) and + not issubclass(rclass, chirp_common.LiveRadio)): + continue + self._vendors[rclass.VENDOR].append(rclass) + for alias in rclass.ALIASES: + self._vendors[alias.VENDOR].append(alias) + + self._vendor.Set(sorted(self._vendors.keys())) + try: + self.select_vendor_model(CONF.get('last_vendor', 'state'), + CONF.get('last_model', 'state')) + except ValueError as e: + LOG.warning('Last vendor/model not found') + + def disable_model_select(self): + self._vendor.Disable() + self._model.Disable() + + def disable_running(self): + self._port.Disable() + self._action_button.Disable() + + def _persist_choices(self): + CONF.set('last_vendor', self._vendor.GetStringSelection(), 'state') + CONF.set('last_model', self._model.GetStringSelection(), 'state') + CONF.set('last_port', self._port.GetValue(), 'state') + + def _selected_port(self, event): + self._persist_choices() + + def _select_vendor(self, vendor): + models = [x.MODEL for x in self._vendors[vendor]] + self._model.Set(models) + self._model.SetSelection(0) + + def _selected_vendor(self, event): + self._select_vendor(event.GetString()) + self._persist_choices() + + def _selected_model(self, event): + self._persist_choices() + + def select_vendor_model(self, vendor, model): + self._vendor.SetSelection(self._vendor.GetItems().index(vendor)) + self._select_vendor(vendor) + self._model.SetSelection(self._model.GetItems().index(model)) + + def _status(self, status): + def _safe_status(): + self.gauge.SetRange(status.max) + self.gauge.SetValue(status.cur) + + wx.CallAfter(_safe_status) + + def complete(self): + self._radio.pipe.close() + wx.CallAfter(self.EndModal, wx.ID_OK) + + def fail(self, message): + def safe_fail(): + wx.MessageBox(message, + 'Error communicating with radio', + wx.ICON_ERROR) + self.EndModal(wx.ID_CANCEL) + + wx.CallAfter(safe_fail) + + def get_selected_rclass(self): + vendor = self._vendor.GetStringSelection() + model = self._model.GetSelection() + return self._vendors[vendor][model] + + +class ChirpDownloadDialog(ChirpCloneDialog): + def _make_action(self, panel): + start = wx.Button(panel, label='Download') + start.Bind(wx.EVT_BUTTON, self._download) + return start + + def _selected_model(self, event): + super(ChirpDownloadDialog, self)._selected_model(event) + rclass = self.get_selected_rclass() + prompts = rclass.get_prompts() + if prompts.pre_download: + prompt = prompts.pre_download + else: + prompt = '' + + # FIXME: Handle download prompt here + + def _download(self, event): + self.disable_model_select() + self.disable_running() + + port = self._port.GetValue() + rclass = self.get_selected_rclass() + + pipe = serial.Serial(port=port, baudrate=rclass.BAUD_RATE, + rtscts=rclass.HARDWARE_FLOW, timeout=0.25) + try: + self._radio = rclass(pipe) + except Exception as e: + self.fail(str(e)) + return + + if isinstance(self._radio, chirp_common.LiveRadio): + self._radio = common.LiveAdapter(self._radio) + + self._radio.status_fn = self._status + + self._clone_thread = CloneThread(self._radio, self, 'sync_in') + self._clone_thread.start() + + +class ChirpUploadDialog(ChirpCloneDialog): + def __init__(self, radio, *a, **k): + super(ChirpUploadDialog, self).__init__(*a, **k) + self._radio = radio + + self.select_vendor_model(self._radio.VENDOR, + self._radio.MODEL) + + if isinstance(self._radio, chirp_common.LiveRadio): + self._radio = common.LiveAdapter(self._radio) + + def _make_action(self, panel): + start = wx.Button(panel, label='Upload') + start.Bind(wx.EVT_BUTTON, self._upload) + self.disable_model_select() + return start + + def _upload(self, event): + self.disable_running() + + port = self._port.GetValue() + + baud = self._radio.BAUD_RATE + if self._radio.pipe: + baud = self._radio.pipe.baudrate + + pipe = serial.Serial(port=port, baudrate=baud, + rtscts=self._radio.HARDWARE_FLOW, timeout=0.25) + self._radio.set_pipe(pipe) + self._radio._status_fn = self._status + + self._clone_thread = CloneThread(self._radio, self, 'sync_out') + self._clone_thread.start() diff --git a/chirp/wxui/common.py b/chirp/wxui/common.py new file mode 100644 index 0000000..34dcf3e --- /dev/null +++ b/chirp/wxui/common.py @@ -0,0 +1,272 @@ +import functools +import logging + +import wx + +from chirp import chirp_common +from chirp.drivers import generic_csv +from chirp import errors +from chirp import settings + +LOG = logging.getLogger(__name__) + +CHIRP_DATA_MEMORY = wx.DataFormat('x-chirp/memory-channel') +EditorChanged, EVT_EDITOR_CHANGED = wx.lib.newevent.NewCommandEvent() + + +class LiveAdapter(generic_csv.CSVRadio): + FILE_EXTENSION = 'img' + + def __init__(self, liveradio): + # Python2 old-style class compatibility + generic_csv.CSVRadio.__init__(self, None) + self._liveradio = liveradio + self.VENDOR = liveradio.VENDOR + self.MODEL = liveradio.MODEL + self.VARIANT = liveradio.VARIANT + self.BAUD_RATE = liveradio.BAUD_RATE + self.HARDWARE_FLOW = liveradio.HARDWARE_FLOW + self.pipe = liveradio.pipe + self._features = self._liveradio.get_features() + + def get_features(self): + return self._features + + def set_pipe(self, pipe): + self.pipe = pipe + self._liveradio.pipe = pipe + + def sync_in(self): + for i in range(*self._features.memory_bounds): + mem = self._liveradio.get_memory(i) + self.set_memory(mem) + status = chirp_common.Status() + status.max = self._features.memory_bounds[1] + status.cur = i + status.msg = 'Cloning' + self.status_fn(status) + + def sync_out(self): + # FIXME: Handle errors + for i in range(*self._features.memory_bounds): + mem = self.get_memory(i) + if mem.freq == 0: + # Convert the CSV notion of emptiness + try: + self._liveradio.erase_memory(i) + except errors.RadioError as e: + LOG.error(e) + else: + try: + self._liveradio.set_memory(mem) + except errors.RadioError as e: + LOG.error(e) + status = chirp_common.Status() + status.max = self._features.memory_bounds[1] + status.cur = i + status.msg = 'Cloning' + self.status_fn(status) + + def get_settings(self): + return self._liveradio.get_settings() + + def set_settings(self, settings): + return self._liveradio.set_settings(settings) + + +class ChirpEditor(wx.Panel): + def cb_copy(self, cut=False): + pass + + def cb_paste(self, data): + pass + + def select_all(self): + pass + + def saved(self): + pass + + def selected(self): + pass + + +class ChirpSettingGrid(wx.Panel): + def __init__(self, settinggroup, *a, **k): + super(ChirpSettingGrid, self).__init__(*a, **k) + self._group = settinggroup + + self.pg = wx.propgrid.PropertyGrid( + self, + style=wx.propgrid.PG_SPLITTER_AUTO_CENTER | + wx.propgrid.PG_BOLD_MODIFIED) + + self.pg.Bind(wx.propgrid.EVT_PG_CHANGED, self._pg_changed) + + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + sizer.Add(self.pg, 1, wx.EXPAND) + + self._choices = {} + + for name, element in self._group.items(): + if not isinstance(element, settings.RadioSetting): + LOG.debug('Skipping nested group %s' % element) + continue + for i in element.keys(): + value = element[i] + if isinstance(value, settings.RadioSettingValueInteger): + editor = self._get_editor_int(element, value) + elif isinstance(value, settings.RadioSettingValueFloat): + editor = self._get_editor_float(element, value) + elif isinstance(value, settings.RadioSettingValueList): + editor = self._get_editor_choice(element, value) + elif isinstance(value, settings.RadioSettingValueBoolean): + editor = self._get_editor_bool(element, value) + elif isinstance(value, settings.RadioSettingValueString): + editor = self._get_editor_str(element, value) + else: + LOG.warning('Unsupported setting type %r' % value) + editor = None + if editor: + self.pg.Append(editor) + + def _pg_changed(self, event): + wx.PostEvent(self, EditorChanged(self.GetId())) + + def _get_editor_int(self, setting, value): + class ChirpIntProperty(wx.propgrid.IntProperty): + def ValidateValue(self, val, info): + if not (val > value.get_min() and val < value.get_max()): + info.SetFailureMessage( + _('Value must be between %i and %i') % ( + value.get_min(), value.get_max())) + return False + return True + return ChirpIntProperty(setting.get_shortname(), + setting.get_name(), + value=int(value)) + + def _get_editor_float(self, setting, value): + class ChirpFloatProperty(wx.propgrid.IntProperty): + def ValidateValue(self, val, info): + if not (val > value.get_min() and val < value.get_max()): + info.SetFailureMessage( + _('Value must be between %.4f and %.4f') % ( + value.get_min(), value.get_max())) + return ChirpFloatProperty(setting.get_shortname(), + setting.get_name(), + value=int(value)) + + def _get_editor_choice(self, setting, value): + choices = value.get_options() + self._choices[setting.get_name()] = choices + current = choices.index(str(value)) + return wx.propgrid.EnumProperty(setting.get_shortname(), + setting.get_name(), + choices, range(len(choices)), + current) + + def _get_editor_bool(self, setting, value): + prop = wx.propgrid.BoolProperty(setting.get_shortname(), + setting.get_name(), + bool(value)) + prop.SetAttribute(wx.propgrid.PG_BOOL_USE_CHECKBOX, True) + return prop + + def _get_editor_str(self, setting, value): + class ChirpStrProperty(wx.propgrid.StringProperty): + def ValidateValue(self, text, info): + try: + value.set_value(text) + except Exception as e: + info.SetFailureMessage(str(e)) + return False + return True + + return ChirpStrProperty(setting.get_shortname(), + setting.get_name(), + value=str(value)) + + def get_values(self): + values = {} + for prop in self.pg._Items(): + if isinstance(prop, wx.propgrid.EnumProperty): + value = self._choices[prop.GetName()][prop.GetValue()] + else: + value = prop.GetValue() + values[prop.GetName()] = value + return values + + def saved(self): + for prop in self.pg._Items(): + prop.SetModifiedStatus(False) + + +def _error_proof(*expected_errors): + """Decorate a method and display an error if it raises. + + If the method raises something in expected_errors, then + log an error, otherwise log exception. + """ + + def show_error(msg): + d = wx.MessageDialog(None, str(msg), 'An error has occurred', + style=wx.OK | wx.ICON_ERROR) + d.ShowModal() + + def wrap(fn): + @functools.wraps(fn) + def inner(*args, **kwargs): + try: + return fn(*args, **kwargs) + except expected_errors as e: + LOG.error('%s: %s' % (fn, e)) + show_error(e) + except Exception as e: + LOG.exception('%s raised unexpected exception' % fn) + show_error(e) + + return inner + return wrap + + +class error_proof(object): + def __init__(self, *expected_exceptions): + self._expected = expected_exceptions + + @staticmethod + def show_error(msg): + d = wx.MessageDialog(None, str(msg), 'An error has occurred', + style=wx.OK | wx.ICON_ERROR) + d.ShowModal() + + def run_safe(self, fn, args, kwargs): + try: + return fn(*args, **kwargs) + except self._expected as e: + LOG.error('%s: %s' % (fn, e)) + self.show_error(e) + except Exception as e: + LOG.exception('%s raised unexpected exception' % fn) + self.show_error(e) + + def __call__(self, fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + return self.run_safe(fn, args, kwargs) + return wrapper + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, traceback): + if exc_type: + if exc_type in self._expected: + LOG.error('%s: %s: %s' % (fn, exc_type, exc_val)) + self.show_error(exc_val) + return True + else: + LOG.exception('Context raised unexpected_exception', + exc_info=(exc_type, exc_val, traceback)) + self.show_error(exc_val) diff --git a/chirp/wxui/developer.py b/chirp/wxui/developer.py new file mode 100644 index 0000000..a4e5c70 --- /dev/null +++ b/chirp/wxui/developer.py @@ -0,0 +1,364 @@ +import functools +import logging +import os + +import wx +import wx.richtext +import wx.lib.scrolledpanel + +from chirp import bitwise +from chirp.wxui import common + +LOG = logging.getLogger(__name__) + + +def simple_diff(a, b, diffsonly=False): + lines_a = a.split(os.linesep) + lines_b = b.split(os.linesep) + blankprinted = True + + diff = "" + for i in range(0, len(lines_a)): + if lines_a[i] != lines_b[i]: + diff += "-%s%s" % (lines_a[i], os.linesep) + diff += "+%s%s" % (lines_b[i], os.linesep) + blankprinted = False + elif diffsonly is True: + if blankprinted: + continue + diff += os.linesep + blankprinted = True + else: + diff += " %s%s" % (lines_a[i], os.linesep) + return diff + + +class MemoryDialog(wx.Dialog): + def __init__(self, mem, *a, **k): + + super(MemoryDialog, self).__init__(*a, **k) + + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + + self.text = wx.richtext.RichTextCtrl( + self, style=wx.VSCROLL|wx.HSCROLL|wx.NO_BORDER) + sizer.Add(self.text, 1, wx.EXPAND) + + sizer.Add(wx.StaticLine(self), 0) + + btn = wx.Button(self, wx.OK, 'OK') + btn.Bind(wx.EVT_BUTTON, lambda e: self.EndModal(wx.OK)) + sizer.Add(btn, 0) + + font = wx.Font(12, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL, + wx.FONTWEIGHT_NORMAL) + + self.text.BeginFont(font) + + if isinstance(mem, tuple): + mem_a, mem_b = mem + self._diff_memories(mem_a, mem_b) + else: + self._raw_memory(mem) + + self.Centre() + + def _raw_memory(self, mem): + self.text.WriteText(mem) + + def _diff_memories(self, mem_a, mem_b): + diff = simple_diff(mem_a, mem_b) + + for line in diff.split(os.linesep): + color = None + if line.startswith('+'): + self.text.BeginTextColour((255, 0, 0)) + color = True + elif line.startswith('-'): + self.text.BeginTextColour((0, 0, 255)) + color = True + self.text.WriteText(line) + self.text.Newline() + if color: + self.text.EndTextColour() + + +class ChirpEditor(wx.Panel): + def __init__(self, parent, obj): + super(ChirpEditor, self).__init__(parent) + self._obj = obj + self._fixed_font = wx.Font(pointSize=10, + family=wx.FONTFAMILY_TELETYPE, + style=wx.FONTSTYLE_NORMAL, + weight=wx.FONTWEIGHT_NORMAL) + self._changed_color = wx.Colour(0, 255, 0) + self._error_color = wx.Colour(255, 0, 0) + + def set_up(self): + pass + + def _mark_changed(self, thing): + thing.SetBackgroundColour(self._changed_color) + tt = thing.GetToolTip() + if not tt: + tt = wx.ToolTip('') + thing.SetToolTip(tt) + tt.SetTip('Press enter to set this in memory') + + def _mark_unchanged(self, thing): + thing.SetBackgroundColour(wx.NullColour) + thing.UnsetToolTip() + + def _mark_error(self, thing, reason): + tt = thing.GetToolTip() + if not tt: + tt = wx.ToolTip('') + thing.SetToolTip(tt) + tt.SetTip(reason) + thing.SetBackgroundColour(self._error_color) + + def __repr__(self): + addr = '0x%02x' % int(self._obj._offset) + + def typestr(c): + return c.__class__.__name__.lower().replace('dataelement', '') + + if isinstance(self._obj, bitwise.arrayDataElement): + innertype = list(self._obj.items())[0][1] + return '%s[%i] (%i bytes each) @ %s' % (typestr(innertype), + len(self._obj), + innertype.size() / 8, + addr) + elif self._obj.size() % 8 == 0: + return '%s (%i bytes) @ %s' % (typestr(self._obj), + self._obj.size() / 8, + addr) + else: + return '%s bits @ %s' % (self._obj.size(), + addr) + + +class ChirpStringEditor(ChirpEditor): + def set_up(self): + sizer = wx.BoxSizer(wx.HORIZONTAL) + entry = wx.TextCtrl(self, value=str(self._obj), + style=wx.TE_PROCESS_ENTER) + entry.SetMaxLength(len(self._obj)) + self.SetSizer(sizer) + sizer.Add(entry, 1, wx.EXPAND) + + entry.Bind(wx.EVT_TEXT, self._edited) + entry.Bind(wx.EVT_TEXT_ENTER, self._changed) + + def _edited(self, event): + entry = event.GetEventObject() + value = entry.GetValue() + if len(value) == len(self._obj): + self._mark_changed(entry) + else: + self._mark_error(entry, 'Length must be %i' % len(self._obj)) + + @common.error_proof() + def _changed(self, event): + entry = event.GetEventObject() + value = entry.GetValue() + self._obj.set_value(value) + self._mark_unchanged(entry) + LOG.debug('Set value: %r' % value) + + +class ChirpIntegerEditor(ChirpEditor): + def set_up(self): + sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(sizer) + + hexdigits = (self._obj.size() / 4) + (self._obj.size() % 4 and 1 or 0) + bindigits = self._obj.size() + + self._editors = {'Hex': (16, '{:0%iX}' % hexdigits), + 'Dec': (10, '{:d}'), + 'Bin': (2, '{:0%ib}' % bindigits)} + self._entries = {} + for name, (base, fmt) in self._editors.items(): + label = wx.StaticText(self, label=name) + entry = wx.TextCtrl(self, value=fmt.format(int(self._obj)), + style=wx.TE_PROCESS_ENTER) + entry.SetFont(self._fixed_font) + sizer.Add(label, 0, wx.ALIGN_CENTER) + sizer.Add(entry, 1, flag=wx.EXPAND) + self._entries[name] = entry + + entry.Bind(wx.EVT_TEXT, functools.partial(self._edited, + base=base)) + entry.Bind(wx.EVT_TEXT_ENTER, functools.partial(self._changed, + base=base)) + + def _edited(self, event, base=10): + entry = event.GetEventObject() + others = {n: e for n, e in self._entries.items() + if e != entry} + + try: + val = int(entry.GetValue(), base) + assert val >= 0, 'Value must be zero or greater' + assert val < pow(2, self._obj.size()), \ + 'Value does not fit in %i bits' % self._obj.size() + except (ValueError, AssertionError) as e: + self._mark_error(entry, str(e)) + return + else: + self._mark_changed(entry) + + for name, entry in others.items(): + base, fmt = self._editors[name] + entry.ChangeValue(fmt.format(val)) + + @common.error_proof() + def _changed(self, event, base=10): + entry = event.GetEventObject() + val = int(entry.GetValue(), base) + self._obj.set_value(val) + self._mark_unchanged(entry) + LOG.debug('Set value: %r' % val) + + +class ChirpBCDEditor(ChirpEditor): + def set_up(self): + sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(sizer) + entry = wx.TextCtrl(self, value=str(int(self._obj)), + style=wx.TE_PROCESS_ENTER) + entry.SetFont(self._fixed_font) + sizer.Add(entry, 1, wx.EXPAND) + entry.Bind(wx.EVT_TEXT, self._edited) + entry.Bind(wx.EVT_TEXT_ENTER, self._changed) + + def _edited(self, event): + entry = event.GetEventObject() + try: + val = int(entry.GetValue()) + digits = self._obj.size() // 4 + assert val >= 0, 'Value must be zero or greater' + assert len(entry.GetValue()) == digits, \ + 'Value must be exactly %i decimal digits' % digits + except (ValueError, AssertionError) as e: + self._mark_error(entry, str(e)) + else: + self._mark_changed(entry) + + @common.error_proof() + def _changed(self, event): + entry = event.GetEventObject() + val = int(entry.GetValue()) + self._obj.set_value(val) + self._mark_unchanged(entry) + LOG.debug('Set Value: %r' % val) + + +class ChirpBrowserPanel(wx.lib.scrolledpanel.ScrolledPanel): + def __init__(self, parent): + super(ChirpBrowserPanel, self).__init__(parent) + self._sizer = wx.FlexGridSizer(2) + self._sizer.AddGrowableCol(1) + self.SetSizer(self._sizer) + self.SetupScrolling() + self._editors = {} + + def add_editor(self, name, editor): + self._editors[name] = editor + + def selected(self): + for name, editor in self._editors.items(): + editor.set_up() + label = wx.StaticText(self, label='%s: ' % name) + tt = wx.ToolTip(repr(editor)) + label.SetToolTip(tt) + + self._sizer.Add(label, 0, wx.ALIGN_CENTER) + self._sizer.Add(editor, 1, flag=wx.EXPAND) + + self._editors = {} + self._sizer.Layout() + + +class ChirpRadioBrowser(common.ChirpEditor): + def __init__(self, radio, *a, **k): + super(ChirpRadioBrowser, self).__init__(*a, **k) + self._loaded = False + self._radio = radio + self._features = radio.get_features() + + self._treebook = wx.Treebook(self) + + try: + view = self._treebook.GetTreeCtrl() + except AttributeError: + # https://github.com/wxWidgets/Phoenix/issues/918 + view = self._treebook.GetChildren()[0] + + view.SetMinSize((250,0)) + + self._treebook.Bind(wx.EVT_TREEBOOK_PAGE_CHANGED, + self.page_selected) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._treebook, 1, wx.EXPAND) + self.SetSizer(sizer) + + def selected(self): + if self._loaded: + return + + pd = wx.ProgressDialog('Loading', 'Building Radio Browser') + self._loaded = True + self._load_from_radio('%s %s' % (self._radio.VENDOR, + self._radio.MODEL), + self._radio._memobj, pd) + pd.Destroy() + self._treebook.ExpandNode(0) + + def page_selected(self, event): + page = self._treebook.GetPage(event.GetSelection()) + page.selected() + + def _load_from_radio(self, name, memobj, pd, parent=None): + editor = None + + def sub_panel(name, memobj, pd, parent): + page = ChirpBrowserPanel(self) + if parent: + pos = self._treebook.FindPage(parent) + self._treebook.InsertSubPage(pos, page, name) + # Stop updating the progress dialog once we get past the + # first generation in the tree because it slows us down + # a lot. + pd = None + else: + self._treebook.AddPage(page, name) + + for subname, item in memobj.items(): + self._load_from_radio(subname, item, pd, parent=page) + + + if isinstance(memobj, bitwise.structDataElement): + if pd: + pd.Pulse('Loading %s' % name) + sub_panel(name, memobj, pd, parent) + elif isinstance(memobj, bitwise.arrayDataElement): + if isinstance(memobj[0], bitwise.charDataElement): + editor = ChirpStringEditor(parent, memobj) + elif isinstance(memobj[0], bitwise.bcdDataElement): + editor = ChirpBCDEditor(parent, memobj) + else: + if pd: + pd.Pulse('Loading %s' % name) + sub_panel('%s[%i]' % (name, len(memobj)), memobj, pd, parent) + elif isinstance(memobj, bitwise.intDataElement): + editor = ChirpIntegerEditor(parent, memobj) + else: + print('Unsupported editor type for %s (%s)' % ( + name, memobj.__class__)) + + if editor: + parent.add_editor(name, editor) diff --git a/chirp/wxui/main.py b/chirp/wxui/main.py new file mode 100644 index 0000000..0c179dc --- /dev/null +++ b/chirp/wxui/main.py @@ -0,0 +1,522 @@ +import datetime +import functools +import logging +import os +import sys + +import wx +import wx.aui +import wx.lib.newevent + +from chirp import directory +from chirp.ui import config +from chirp.wxui import common +from chirp.wxui import clone +from chirp.wxui import developer +from chirp.wxui import memedit +from chirp.wxui import settingsedit +from chirp import CHIRP_VERSION + +EditorSetChanged, EVT_EDITORSET_CHANGED = wx.lib.newevent.NewCommandEvent() +CONF = config.get() +LOG = logging.getLogger(__name__) + + +class ChirpEditorSet(wx.Panel): + def __init__(self, radio, filename, *a, **k): + super(ChirpEditorSet, self).__init__(*a, **k) + self._radio = radio + if filename is None: + filename = '%s.%s' % (self.default_filename, + radio.FILE_EXTENSION) + self._filename = filename + self._modified = not os.path.exists(filename) + + self._editors = wx.Notebook(self, style=wx.NB_LEFT) + + self._editors.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, + self._editor_selected) + + sizer = wx.BoxSizer() + sizer.Add(self._editors, 1, wx.EXPAND) + self.SetSizer(sizer) + + features = radio.get_features() + + if features.has_sub_devices: + radios = radio.get_sub_devices() + format = 'Memories (%(variant)s)' + else: + radios = [radio] + format = 'Memories' + + for radio in radios: + edit = memedit.ChirpMemEdit(radio, self._editors) + edit.refresh() + self.Bind(common.EVT_EDITOR_CHANGED, self._editor_changed) + self._editors.AddPage(edit, format % {'variant': radio.VARIANT}) + + if features.has_settings: + if isinstance(radio, common.LiveAdapter): + settings = settingsedit.ChirpLiveSettingsEdit(radio, + self._editors) + else: + settings = settingsedit.ChirpCloneSettingsEdit(radio, + self._editors) + self._editors.AddPage(settings, 'Settings') + + if CONF.get_bool('developer', 'state'): + browser = developer.ChirpRadioBrowser(radio, self._editors) + self._editors.AddPage(browser, 'Browser') + + def _editor_changed(self, event): + self._modified = True + wx.PostEvent(self, EditorSetChanged(self.GetId(), editorset=self)) + + def _editor_selected(self, event): + page_index = event.GetSelection() + page = self._editors.GetPage(page_index) + page.selected() + + def save(self, filename=None): + if filename is None: + filename = self.filename + LOG.debug('Saving to %s' % filename) + self._radio.save(filename) + self._filename = filename + self._modified = False + # FIXME + self._editors.GetPage(1).saved() + + @property + def filename(self): + return self._filename + + @property + def modified(self): + return self._modified + + @property + def radio(self): + return self._radio + + @property + def default_filename(self): + defname_format = CONF.get("default_filename", "global") or \ + "{vendor}_{model}_{date}" + defname = defname_format.format( + vendor=self._radio.VENDOR, + model=self._radio.MODEL, + date=datetime.datetime.now().strftime('%Y%m%d') + ).replace('/', '_') + return defname + + @property + def current_editor(self): + return self._editors.GetCurrentPage() + + @property + def current_editor_index(self): + return self._editors.GetSelection() + + def select_editor(self, index): + self._editors.SetSelection(index) + + def cb_copy(self, cut=False): + return self.current_editor.cb_copy(cut=cut) + + def cb_paste(self, data): + return self.current_editor.cb_paste(data) + + def select_all(self): + return self.current_editor.select_all() + + +class ChirpMain(wx.Frame): + def __init__(self, *args, **kwargs): + super(ChirpMain, self).__init__(*args, **kwargs) + + self.SetSize(int(CONF.get('window_w', 'state') or 1024), + int(CONF.get('window_h', 'state') or 768)) + + self.SetMenuBar(self.make_menubar()) + + # Stock items look good on linux, terrible on others, + # so punt on this for the moment. + # self.make_toolbar() + + self._editors = wx.aui.AuiNotebook( + self, style=wx.aui.AUI_NB_CLOSE_ON_ALL_TABS|wx.aui.AUI_NB_TAB_MOVE) + self.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._editor_close) + self.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED, + self._editor_page_changed) + self.Bind(wx.EVT_CLOSE, self._window_close) + + self._update_window_for_editor() + + @property + def current_editorset(self): + return self._editors.GetCurrentPage() + + def open_file(self, filename, exists=True, select=True): + + if exists: + radio = directory.get_radio_by_image(filename) + else: + CSVRadio = directory.get_radio('Generic_CSV') + radio = CSVRadio(None) + + editorset = ChirpEditorSet(radio, filename, self._editors) + self.add_editorset(editorset, select=select) + + def add_editorset(self, editorset, select=True): + self._editors.AddPage(editorset, + os.path.basename(editorset.filename), + select=select) + self.Bind(EVT_EDITORSET_CHANGED, self._editor_changed, editorset) + self._update_editorset_title(editorset) + + def make_menubar(self): + file_menu = wx.Menu() + + new_item = file_menu.Append(wx.ID_NEW) + self.Bind(wx.EVT_MENU, self._menu_new, new_item) + + open_item = file_menu.Append(wx.ID_OPEN) + self.Bind(wx.EVT_MENU, self._menu_open, open_item) + + save_item = file_menu.Append(wx.ID_SAVE) + self.Bind(wx.EVT_MENU, self._menu_save, save_item) + + save_item = file_menu.Append(wx.ID_SAVEAS) + self.Bind(wx.EVT_MENU, self._menu_save_as, save_item) + + file_menu.Append(wx.MenuItem(file_menu, wx.ID_SEPARATOR)) + + close_item = file_menu.Append(wx.ID_CLOSE) + self.Bind(wx.EVT_MENU, self._menu_close, close_item) + + exit_item = file_menu.Append(wx.ID_EXIT) + self.Bind(wx.EVT_MENU, self._menu_exit, exit_item) + + edit_menu = wx.Menu() + + cut_item = edit_menu.Append(wx.ID_CUT) + self.Bind(wx.EVT_MENU, functools.partial(self._menu_copy, cut=True), + cut_item) + + copy_item = edit_menu.Append(wx.ID_COPY) + self.Bind(wx.EVT_MENU, self._menu_copy, copy_item) + + paste_item = edit_menu.Append(wx.ID_PASTE) + self.Bind(wx.EVT_MENU, self._menu_paste, paste_item) + + selall_item = edit_menu.Append(wx.ID_SELECTALL) + self.Bind(wx.EVT_MENU, self._menu_selall, selall_item) + + radio_menu = wx.Menu() + + self._download_menu_item = wx.NewId() + download_item = wx.MenuItem(radio_menu, self._download_menu_item, + 'Download') + download_item.SetAccel(wx.AcceleratorEntry(wx.ACCEL_ALT, ord('D'))) + self.Bind(wx.EVT_MENU, self._menu_download, download_item) + radio_menu.Append(download_item) + + self._upload_menu_item = wx.NewId() + upload_item = wx.MenuItem(radio_menu, self._upload_menu_item, 'Upload') + upload_item.SetAccel(wx.AcceleratorEntry(wx.ACCEL_ALT, ord('U'))) + self.Bind(wx.EVT_MENU, self._menu_upload, upload_item) + radio_menu.Append(upload_item) + + if CONF.get_bool('developer', 'state'): + radio_menu.Append(wx.MenuItem(file_menu, wx.ID_SEPARATOR)) + + self._reload_driver_item = wx.NewId() + reload_drv_item = wx.MenuItem(radio_menu, + self._reload_driver_item, + 'Reload Driver') + reload_drv_item.SetAccel( + wx.AcceleratorEntry(wx.ACCEL_ALT | wx.ACCEL_CTRL, + ord('R'))) + self.Bind(wx.EVT_MENU, self._menu_reload_driver, reload_drv_item) + radio_menu.Append(reload_drv_item) + + self._reload_both_item = wx.NewId() + reload_both_item = wx.MenuItem(radio_menu, + self._reload_both_item, + 'Reload Driver and File') + reload_both_item.SetAccel( + wx.AcceleratorEntry( + wx.ACCEL_ALT | wx.ACCEL_CTRL | wx.ACCEL_SHIFT, + ord('R'))) + self.Bind( + wx.EVT_MENU, + functools.partial(self._menu_reload_driver, andfile=True), + reload_both_item) + radio_menu.Append(reload_both_item) + + self._interact_driver_item = wx.NewId() + interact_drv_item = wx.MenuItem(radio_menu, + self._interact_driver_item, + 'Interact with driver') + self.Bind(wx.EVT_MENU, self._menu_interact_driver, + interact_drv_item) + radio_menu.Append(interact_drv_item) + + help_menu = wx.Menu() + + about_item = wx.MenuItem(help_menu, wx.NewId(), 'About') + self.Bind(wx.EVT_MENU, self._menu_about, about_item) + help_menu.Append(about_item) + + menu_bar = wx.MenuBar() + menu_bar.Append(file_menu, '&File') + menu_bar.Append(edit_menu, '&Edit') + menu_bar.Append(radio_menu, '&Radio') + menu_bar.Append(help_menu, '&Help') + + return menu_bar + + def make_toolbar(self): + tb = self.CreateToolBar() + + def bm(stock): + return wx.ArtProvider.GetBitmap(stock, + wx.ART_TOOLBAR, (32, 32)) + + tbopen = tb.AddTool(wx.NewId(), 'Open', bm(wx.ART_FILE_OPEN), + 'Open a file') + tbsave = tb.AddTool(wx.NewId(), 'Save', bm(wx.ART_FILE_SAVE), + 'Save file') + tbclose = tb.AddTool(wx.NewId(), 'Close', bm(wx.ART_CLOSE), + 'Close file') + tbdl = tb.AddTool(wx.NewId(), 'Download', bm(wx.ART_GO_DOWN), + 'Download from radio') + tbl = tb.AddTool(wx.NewId(), 'Upload', bm(wx.ART_GO_UP), + 'Upload to radio') + + self.Bind(wx.EVT_MENU, self._menu_open, tbopen) + tb.Realize() + + def _editor_page_changed(self, event): + self._editors.GetPage(event.GetSelection()) + self._update_window_for_editor() + + def _editor_changed(self, event): + self._update_editorset_title(event.editorset) + self._update_window_for_editor() + + def _editor_close(self, event): + eset = self._editors.GetPage(event.GetSelection()) + if eset.modified: + if wx.MessageBox( + '%s has not been saved. Close anyway?' % eset.filename, + 'Close without saving?', + wx.YES_NO|wx.NO_DEFAULT|wx.ICON_WARNING) != wx.YES: + event.Veto() + + wx.CallAfter(self._update_window_for_editor) + + def _update_window_for_editor(self): + eset = self.current_editorset + can_close = False + can_save = False + can_saveas = False + can_upload = False + CSVRadio = directory.get_radio('Generic_CSV') + if eset is not None: + can_close = True + can_save = eset.modified + can_saveas = True + can_upload = not (isinstance(eset.radio, CSVRadio) and not + isinstance(eset.radio, common.LiveAdapter)) + + items = [ + (wx.ID_CLOSE, can_close), + (wx.ID_SAVE, can_save), + (wx.ID_SAVEAS, can_saveas), + (self._upload_menu_item, can_upload), + ] + for ident, enabled in items: + menuitem = self.GetMenuBar().FindItemById(ident) + menuitem.Enable(enabled) + + def _window_close(self, event): + if any([self._editors.GetPage(i).modified + for i in range(self._editors.GetPageCount())]): + if wx.MessageBox( + 'Some files have not been saved. Exit anyway?', + 'Exit without saving?', + wx.YES_NO|wx.NO_DEFAULT|wx.ICON_WARNING) != wx.YES: + if event.CanVeto(): + event.Veto() + return + + size = self.GetSize() + CONF.set_int('window_w', size.GetWidth(), 'state') + CONF.set_int('window_h', size.GetHeight(), 'state') + config._CONFIG.save() + self.Destroy() + + def _update_editorset_title(self, editorset): + index = self._editors.GetPageIndex(editorset) + self._editors.SetPageText(index, '%s%s' % ( + os.path.basename(editorset.filename), + editorset.modified and '*' or '')) + + def _menu_new(self, event): + self.open_file('Untitled.csv', exists=False) + + def _menu_open(self, event): + wildcard = '|'.join(['Chirp Image Files (*.img)|*.img', + 'CSV Files (*.csv)|*.csv', + 'All Files (*.*)|*.*']) + with wx.FileDialog(self, 'Open a file', wildcard=wildcard, + style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fd: + if fd.ShowModal() == wx.ID_CANCEL: + return + filename = fd.GetPath() + self.open_file(str(filename)) + + def _menu_save_as(self, event): + eset = self.current_editorset + wildcard = 'CHIRP %(vendor)s %(model)s Files (*.%(ext)s)|*.%(ext)s' % { + 'vendor': eset._radio.VENDOR, + 'model': eset._radio.MODEL, + 'ext': eset._radio.FILE_EXTENSION} + + style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR + with wx.FileDialog(self, "Save file", defaultFile=eset.filename, + wildcard=wildcard, + style=style) as fd: + if fd.ShowModal() == wx.ID_CANCEL: + return + filename = fd.GetPath() + eset.save(filename) + self._update_editorset_title(eset) + + def _menu_save(self, event): + editorset = self.current_editorset + if not editorset.modified: + return + + if not os.path.exists(editorset.filename): + return self._menu_save_as(event) + + editorset.save() + self._update_editorset_title(self.current_editorset) + + def _menu_close(self, event): + self._editors.DeletePage(self._editors.GetSelection()) + self._update_window_for_editor() + + def _menu_exit(self, event): + self.Close(True) + + def _menu_copy(self, event, cut=False): + data = self.current_editorset.cb_copy(cut=cut) + if wx.TheClipboard.Open(): + wx.TheClipboard.SetData(data) + wx.TheClipboard.Close() + + def _menu_paste(self, event): + memdata = wx.CustomDataObject(common.CHIRP_DATA_MEMORY) + textdata = wx.TextDataObject() + if wx.TheClipboard.Open(): + gotchirpmem = wx.TheClipboard.GetData(memdata) + got = wx.TheClipboard.GetData(textdata) + wx.TheClipboard.Close() + if gotchirpmem: + self.current_editorset.cb_paste(memdata) + if got: + self.current_editorset.cb_paste(textdata) + + def _menu_selall(self, event): + self.current_editorset.select_all() + + def _menu_download(self, event): + with clone.ChirpDownloadDialog(self) as d: + d.Centre() + if d.ShowModal() == wx.ID_OK: + radio = d._radio + editorset = ChirpEditorSet(radio, None, self._editors) + self.add_editorset(editorset) + + def _menu_upload(self, event): + radio = self.current_editorset.radio + with clone.ChirpUploadDialog(radio, self) as d: + d.Centre() + d.ShowModal() + + @common.error_proof() + def _menu_reload_driver(self, event, andfile=False): + radio = self.current_editorset.radio + try: + # If we were loaded from a dynamic alias in directory, + # get the pointer to the original + orig_rclass = radio._orig_rclass + except AttributeError: + orig_rclass = radio.__class__ + + # Save a reference to the radio's internal mmap. If the radio does + # anything strange or does not follow the typical convention, this + # will not work + mmap = radio._mmap + + # Reload the actual module + module = sys.modules[orig_rclass.__module__] + LOG.warning('Going to reload %s' % module) + directory.enable_reregistrations() + import importlib + importlib.reload(module) + + # Grab a new reference to the updated module and pick out the + # radio class from it. + module = sys.modules[orig_rclass.__module__] + rclass = getattr(module, orig_rclass.__name__) + + filename = self.current_editorset.filename + if andfile: + # Reload the file while creating the radio + new_radio = rclass(filename) + else: + # Try to reload the driver in place, without + # re-reading the file; mimic the file loading + # procedure after jamming the memory back in + new_radio = rclass(None) + new_radio._mmap = mmap + new_radio.process_mmap() + + # Kill the current editorset now that we know the radio loaded + # successfully + last_editor = self.current_editorset.current_editor_index + self._menu_close(event) + + # Mimic the File->Open process to get a new editorset based + # on our franken-radio + editorset = ChirpEditorSet(new_radio, filename, self._editors) + self.add_editorset(editorset, select=True) + editorset.select_editor(last_editor) + + LOG.info('Reloaded radio driver%s in place; good luck!' % ( + andfile and ' (and file)' or '')) + + def _menu_interact_driver(self, event): + LOG.warning('Going to interact with radio at the console') + radio = self.current_editorset.radio + import code + locals = {'main': self, + 'radio': radio} + code.interact(banner='Locals are: %s' % (', '.join(locals.keys())), + local=locals) + + def _menu_about(self, event): + pyver = sys.version_info + aboutinfo = 'CHIRP %s on Python %s wxPython %s' % ( + CHIRP_VERSION, + '%s.%s.%s' % (pyver.major, pyver.minor, pyver.micro), + wx.version()) + wx.MessageBox(aboutinfo, 'About CHIRP', + wx.OK | wx.ICON_INFORMATION) + diff --git a/chirp/wxui/memedit.py b/chirp/wxui/memedit.py new file mode 100644 index 0000000..4eef1be --- /dev/null +++ b/chirp/wxui/memedit.py @@ -0,0 +1,601 @@ +import functools +import logging +import pickle + +import wx +import wx.lib.newevent +import wx.grid +import wx.propgrid + +from chirp import chirp_common +from chirp.ui import config +from chirp.wxui import common +from chirp.wxui import developer + +LOG = logging.getLogger(__name__) +CONF = config.get() + + +class ChirpMemoryColumn(object): + NAME = None + DEFAULT = '' + + def __init__(self, name, radio): + """ + :param name: The name on the Memory object that this represents + """ + self._name = name + self._radio = radio + self._features = radio.get_features() + + @property + def label(self): + return self.NAME or self._name.title() + + def hidden_for(self, memory): + return False + + @property + def valid(self): + if self._name in ['freq', 'rtone']: + return True + to_try = ['has_%s', 'valid_%ss', 'valid_%ses', 'valid_%s_levels'] + for thing in to_try: + try: + return bool(self._features[thing % self._name]) + except KeyError: + pass + + LOG.error('Unsure if %r is valid' % self._name) + return True + + def _render_value(self, memory, value): + if value is []: + raise Exception('Found empty list value for %s: %r' % ( + self._name, value)) + return str(value) + + def value(self, memory): + return getattr(memory, self._name) + + def render_value(self, memory): + if memory.empty: + return '' + if self.hidden_for(memory): + return '' + return self._render_value(memory, self.value(memory)) + + def _digest_value(self, memory, input_value): + return str(input_value) + + def digest_value(self, memory, input_value): + setattr(memory, self._name, self._digest_value(memory, input_value)) + + def get_editor(self): + return wx.grid.GridCellTextEditor() + + def get_propeditor(self, memory): + class ChirpStringProperty(wx.propgrid.StringProperty): + def ValidateValue(myself, value, validationInfo): + try: + self._digest_value(memory, value) + return True + except ValueError: + validationInfo.SetFailureMessage( + 'Invalid value: %r' % value) + return False + except Exception as e: + LOG.exception('Failed to validate %r for property %s' % ( + value, self._name)) + validationInfo.SetFailureMessage( + 'Invalid value: %r' % value) + return False + + editor = ChirpStringProperty(self.label, self._name) + editor.SetValue(self.render_value(memory)) + return editor + + +class ChirpFrequencyColumn(ChirpMemoryColumn): + DEFAULT = 0 + @property + def label(self): + if self._name == 'offset': + return 'Offset' + else: + return 'Frequency' + + def hidden_for(self, memory): + return self._name == 'offset' and not memory.duplex + + def _render_value(self, memory, value): + if not value: + value = 0 + return '%.5f' % (value / 1000000.0) + + def _digest_value(self, memory, input_value): + if not input_value.strip(): + input_value = 0 + return int(chirp_common.to_MHz(float(input_value))) + + +class ChirpToneColumn(ChirpMemoryColumn): + def __init__(self, name, radio): + super(ChirpToneColumn, self).__init__(name, radio) + self._choices = chirp_common.TONES + self._str_choices = [str(x) for x in self._choices] + + @property + def label(self): + if self._name == 'rtone': + return 'Tone' + else: + return 'ToneSql' + + def hidden_for(self, memory): + return ( + (self._name == 'rtone' and memory.tmode not in ('Tone', 'Cross')) + or + (self._name == 'ctone' and memory.tmode not in ('TSQL', 'Cross'))) + + def _digest_value(self, memory, input_value): + return float(input_value) + + def get_editor(self): + return wx.grid.GridCellChoiceEditor(['%.1f' % t + for t in chirp_common.TONES]) + + def get_propeditor(self, memory): + current = self.value(memory) + return wx.propgrid.EnumProperty(self.label, self._name, + self._str_choices, + range(len(self._choices)), + self._choices.index(current)) + + +class ChirpChoiceColumn(ChirpMemoryColumn): + def __init__(self, name, radio, choices): + super(ChirpChoiceColumn, self).__init__(name, radio) + self._choices = choices + self._str_choices = [str(x) for x in choices] + + def _digest_value(self, memory, input_value): + idx = self._str_choices.index(input_value) + return self._choices[idx] + + def get_editor(self): + return wx.grid.GridCellChoiceEditor(self._str_choices) + + def get_propeditor(self, memory): + current = self._render_value(memory, self.value(memory)) + try: + cur_index = self._choices.index(current) + except ValueError: + # This means the memory has some value set that the radio + # does not support, like the default cross_mode not being + # in rf.valid_cross_modes. This is likely because the memory + # just doesn't have that value set, so take the first choice + # in this case. + cur_index = 0 + return wx.propgrid.EnumProperty(self.label, self._name, + self._str_choices, + range(len(self._str_choices)), + cur_index) + + +class ChirpDTCSColumn(ChirpChoiceColumn): + def __init__(self, name, radio): + dtcs_codes = ['%03i' % code for code in chirp_common.DTCS_CODES] + super(ChirpDTCSColumn, self).__init__(name, radio, + dtcs_codes) + + @property + def label(self): + if self._name == 'dtcs': + return 'DTCS' + elif self._name == 'rx_dtcs': + return 'RX DTCS' + else: + return 'ErrDTCS' + + def _digest_value(self, memory, input_value): + return int(input_value) + + def _render_value(self, memory, value): + return '%03i' % value + + def hidden_for(self, memory): + return ( + (self._name == 'dtcs' and not ( + memory.tmode == 'DTCS' or (memory.tmode == 'Cross' and + 'DTCS->' in memory.cross_mode))) + or + (self._name == 'rx_dtcs' and not ( + memory.tmode == 'Cross' and '>DTCS' in memory.cross_mode))) + + +class ChirpCrossModeColumn(ChirpChoiceColumn): + def __init__(self, name, radio): + rf = radio.get_features() + super(ChirpCrossModeColumn, self).__init__(name, radio, + rf.valid_cross_modes) + + @property + def label(self): + return 'Cross mode' + + def hidden_for(self, memory): + return memory.tmode != 'Cross' + + +class ChirpMemEdit(common.ChirpEditor): + def __init__(self, radio, *a, **k): + super(ChirpMemEdit, self).__init__(*a, **k) + + self._radio = radio + self._features = self._radio.get_features() + + #self._radio.subscribe(self._job_done) + self._jobs = {} + self._memory_cache = {} + + self._col_defs = self._setup_columns() + + self._grid = wx.grid.Grid(self) + self._grid.CreateGrid(self._features.memory_bounds[1] + + len(self._features.valid_special_chans) + 1, + len(self._col_defs)) + self._grid.SetSelectionMode(wx.grid.Grid.SelectRows) + + self._fixed_font = wx.Font(pointSize=10, + family=wx.FONTFAMILY_TELETYPE, + style=wx.FONTSTYLE_NORMAL, + weight=wx.FONTWEIGHT_NORMAL) + + for col, col_def in enumerate(self._col_defs): + if not col_def.valid: + self._grid.HideCol(col) + else: + self._grid.SetColLabelValue(col, col_def.label) + attr = wx.grid.GridCellAttr() + attr.SetEditor(col_def.get_editor()) + attr.SetFont(self._fixed_font) + self._grid.SetColAttr(col, attr) + self._grid.SetColMinimalWidth(col, 75) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._grid, 1, wx.EXPAND) + self.SetSizer(sizer) + + wx.CallAfter(self._grid.AutoSizeColumns, setAsMin=False) + + self._grid.Bind(wx.grid.EVT_GRID_CELL_CHANGING, + self._memory_edited) + self._grid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, + self._memory_changed) + self._grid.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, + self._memory_rclick) + + # For resize calculations + self._dc = wx.ScreenDC() + self._dc.SetFont(self._fixed_font) + + def _setup_columns(self): + defs = [ + ChirpFrequencyColumn('freq', self._radio), + ChirpMemoryColumn('name', self._radio), + ChirpChoiceColumn('tmode', self._radio, + self._features.valid_tmodes), + ChirpToneColumn('rtone', self._radio), + ChirpToneColumn('ctone', self._radio), + ChirpDTCSColumn('dtcs', self._radio), + ChirpDTCSColumn('rx_dtcs', self._radio), + ChirpChoiceColumn('duplex', self._radio, + self._features.valid_duplexes), + ChirpFrequencyColumn('offset', self._radio), + ChirpChoiceColumn('mode', self._radio, + self._features.valid_modes), + ChirpChoiceColumn('tuning_step', self._radio, + self._features.valid_tuning_steps), + ChirpChoiceColumn('skip', self._radio, + self._features.valid_skips), + ChirpCrossModeColumn('cross_mode', self._radio), + ChirpChoiceColumn('power', self._radio, + self._features.valid_power_levels), + ChirpMemoryColumn('comment', self._radio), + ] + return defs + + def refresh_memory(self, memory): + self._memory_cache[memory.number] = memory + + row = memory.number - self._features.memory_bounds[0] + + if memory.extd_number: + self._grid.SetRowLabelValue(row, memory.extd_number) + else: + self._grid.SetRowLabelValue(row, str(memory.number)) + + for col, col_def in enumerate(self._col_defs): + self._grid.SetCellValue(row, col, col_def.render_value(memory)) + + def refresh(self): + for i in range(*self._features.memory_bounds): + try: + m = self._radio.get_memory(i) + except Exception as e: + LOG.exception('Failure retreiving memory %i from %s' % ( + i, '%s %s %s' % (self._radio.VENDOR, + self._radio.MODEL, + self._radio.VARIANT))) + continue + self.refresh_memory(m) + + return + + for i in self._features.valid_special_chans: + m = self._radio.get_memory(i) + self.refresh_memory(m) + + def _memory_edited(self, event): + """ + Called when the memory row in the UI is edited. + Writes the memory to the radio and displays an error if needed. + """ + row = event.GetRow() + col = event.GetCol() + val = event.GetString() + + col_def = self._col_defs[col] + + try: + mem = self._memory_cache[row + self._features.memory_bounds[0]] + except KeyError: + wx.MessageBox('Unable to edit memory before radio is loaded') + event.Veto() + return + + try: + if col_def.label == 'Name': + val = self._radio.filter_name(val) + col_def.digest_value(mem, val) + mem.empty = False + job = self._radio.set_memory(mem) + except Exception as e: + LOG.exception('Failed to edit memory') + wx.MessageBox('Invalid edit: %s' % e, 'Error') + event.Veto() + else: + LOG.debug('Memory %i changed, column: %i:%s' % (row, col, mem)) + + wx.CallAfter(self._resize_col_after_edit, row, col) + + def _resize_col_after_edit(self, row, col): + """Resize the column if the text in row,col does not fit.""" + size = self._dc.GetTextExtent(self._grid.GetCellValue(row, col)) + padsize = size[0] + 20 + if padsize > self._grid.GetColSize(col): + self._grid.SetColSize(col, padsize) + + def _memory_changed(self, event): + """ + Called when the memory in the UI has been changed. + Responsible for re-requesting the memory from the radio and updating + the UI accordingly. + Also provides the trigger to the editorset that we have changed. + """ + row = event.GetRow() + mem = self._radio.get_memory(row + self._features.memory_bounds[0]) + self.refresh_memory(mem) + wx.PostEvent(self, common.EditorChanged(self.GetId())) + + def delete_memory_at(self, row, event): + number = row + self._features.memory_bounds[0] + mem = self._radio.get_memory(number) + mem.empty = True + self._radio.set_memory(mem) + self.refresh_memory(mem) + wx.PostEvent(self, common.EditorChanged(self.GetId())) + + def _delete_memories_at(self, rows, event): + for row in rows: + self.delete_memory_at(row, event) + + def _memory_rclick(self, event): + menu = wx.Menu() + selected_rows = self._grid.GetSelectedRows() + + props_item = wx.MenuItem(menu, wx.NewId(), 'Properties') + self.Bind(wx.EVT_MENU, + functools.partial(self._mem_properties, selected_rows), + props_item) + menu.Append(props_item) + + if len(selected_rows) > 1: + del_item = wx.MenuItem(menu, wx.NewId(), + 'Delete %i Memories' % len(selected_rows)) + to_delete = selected_rows + else: + del_item = wx.MenuItem(menu, wx.NewId(), 'Delete') + to_delete = [event.GetRow()] + self.Bind(wx.EVT_MENU, + functools.partial(self._delete_memories_at, to_delete), + del_item) + menu.Append(del_item) + + if CONF.get_bool('developer', 'state'): + menu.Append(wx.MenuItem(menu, wx.ID_SEPARATOR)) + + raw_item = wx.MenuItem(menu, wx.NewId(), 'Show Raw Memory') + self.Bind(wx.EVT_MENU, + functools.partial(self._mem_showraw, event.GetRow()), + raw_item) + menu.Append(raw_item) + + if len(selected_rows) == 2: + diff_item = wx.MenuItem(menu, wx.NewId(), 'Diff Raw Memories') + self.Bind(wx.EVT_MENU, + functools.partial(self._mem_diff, selected_rows), + diff_item) + menu.Append(diff_item) + + self.PopupMenu(menu) + menu.Destroy() + + def row2mem(self, row): + return row + self._features.memory_bounds[0] + + def _mem_properties(self, rows, event): + memories = [ + self._radio.get_memory(self.row2mem(row)) + for row in rows] + with ChirpMemPropDialog(memories, self) as d: + if d.ShowModal() == wx.ID_OK: + for memory in d._memories: + self._radio.set_memory(memory) + self.refresh_memory(memory) + + def _mem_showraw(self, row, event): + mem = self._radio.get_raw_memory(self.row2mem(row)) + with developer.MemoryDialog(mem, self) as d: + d.ShowModal() + + def _mem_diff(self, rows, event): + mem_a = self._radio.get_raw_memory(self.row2mem(rows[0])) + mem_b = self._radio.get_raw_memory(self.row2mem(rows[1])) + with developer.MemoryDialog((mem_a, mem_b), self) as d: + d.ShowModal() + + def cb_copy(self, cut=False): + rows = self._grid.GetSelectedRows() + offset = self._features.memory_bounds[0] + mems = [] + for row in rows: + mem = self._radio.get_memory(row + offset) + # We can't pickle settings, nor would they apply if we + # paste across models + mem.extra = [] + mems.append(mem) + data = wx.CustomDataObject(common.CHIRP_DATA_MEMORY) + data.SetData(pickle.dumps(mems)) + for mem in mems: + if cut: + self._radio.erase_memory(mem.number) + mem = self._radio.get_memory(mem.number) + self.refresh_memory(mem) + return data + + def _cb_paste_memories(self, mems): + try: + row = self._grid.GetSelectedRows()[0] + except IndexError: + LOG.info('No row selected for paste') + return + + for mem in mems: + mem.empty = False + mem.number = row + self._features.memory_bounds[0] + row += 1 + self._radio.set_memory(mem) + self.refresh_memory(mem) + + def cb_paste(self, data): + if data.GetFormat() == common.CHIRP_DATA_MEMORY: + mems = pickle.loads(data.GetData().tobytes()) + self._cb_paste_memories(mems) + elif data.GetFormat() == wx.DF_UNICODETEXT: + LOG.debug('FIXME: handle pasted text: %r' % data.GetText()) + else: + LOG.warning('Unknown data format %s' % data.GetFormat().Type) + + def select_all(self): + self._grid.SelectAll() + + +class ChirpMemPropDialog(wx.Dialog): + def __init__(self, memories, memedit, *a, **k): + if len(memories) == 1: + title = 'Edit details for memory %i' % memories[0].number + else: + title = 'Edit details for %i memories' % len(memories) + + super(ChirpMemPropDialog, self).__init__( + memedit, *a, title=title, **k) + + self.Centre() + + self._memories = memories + self._col_defs = memedit._col_defs + + # The first memory sets the defaults + memory = self._memories[0] + + self._tabs = wx.Notebook(self) + + vbox = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(vbox) + vbox.Add(self._tabs, 1, wx.EXPAND) + + self._pg = wx.propgrid.PropertyGrid(self._tabs) + self._tabs.InsertPage(0, self._pg, 'Values') + + if memory.extra: + self._tabs.InsertPage(1, common.ChirpSettingGrid(memory.extra, + self._tabs), + 'Extra') + + for coldef in memedit._col_defs: + if coldef.valid: + self._pg.Append(coldef.get_propeditor(memory)) + + hbox = wx.BoxSizer(wx.HORIZONTAL) + hbox.Add(wx.Button(self, wx.ID_OK)) + hbox.Add(wx.Button(self, wx.ID_CANCEL)) + + vbox.Add(hbox, 0, wx.ALIGN_RIGHT|wx.ALL, border=10) + + self.Bind(wx.EVT_BUTTON, self._button) + + def _col_def_by_name(self, name): + for coldef in self._col_defs: + if coldef._name == name: + return coldef + LOG.error('No column definition for %s' % name) + + def _make_memories(self): + memories = [memory.dupe() for memory in self._memories] + + for mem in memories: + for prop in self._pg._Items(): + name = prop.GetName() + + coldef = self._col_def_by_name(name) + value = prop.GetValueAsString() + value = coldef._digest_value(mem, value) + + if (getattr(self._memories[0], name) == value): + LOG.debug('Skipping unchanged field %s' % name) + continue + + LOG.debug('Value for %s is %r' % (name, value)) + setattr(mem, prop.GetName(), value) + + if self._tabs.GetPageCount() == 2: + extra = self._tabs.GetPage(1).get_values() + for setting in mem.extra: + name = setting.get_name() + try: + setting.value = extra[name] + except KeyError: + raise + LOG.warning('Missing setting %r' % name) + continue + + return memories + + def _button(self, event): + button_id = event.GetEventObject().GetId() + if button_id == wx.ID_OK: + self._memories = self._make_memories() + + self.EndModal(button_id) diff --git a/chirp/wxui/settingsedit.py b/chirp/wxui/settingsedit.py new file mode 100644 index 0000000..8a61034 --- /dev/null +++ b/chirp/wxui/settingsedit.py @@ -0,0 +1,161 @@ +import logging + +import wx +import wx.dataview + +from chirp import settings +from chirp.wxui import clone +from chirp.wxui import common + +LOG = logging.getLogger(__name__) + + +class ChirpSettingsEdit(common.ChirpEditor): + def __init__(self, radio, *a, **k): + super(ChirpSettingsEdit, self).__init__(*a, **k) + + self._radio = radio + self._settings = None + + sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(sizer) + + self._pre_propgrid_hook(sizer) + + self._group_control = wx.Listbook(self, style=wx.LB_LEFT) + sizer.Add(self._group_control, 1, wx.EXPAND) + + self._initialized = False + self._group_control.Bind(wx.EVT_PAINT, self._activate) + + def _activate(self, event): + if not self._initialized: + self._initialized = True + wx.CallAfter(self._initialize) + + def _load_settings(self): + for group in self._settings: + self._add_group(group) + self._group_control.Layout() + + def _add_group(self, group): + propgrid = common.ChirpSettingGrid(group, self._group_control) + self.Bind(common.EVT_EDITOR_CHANGED, self._changed, propgrid) + LOG.debug('Adding page for %s' % group.get_shortname()) + self._group_control.AddPage(propgrid, group.get_shortname()) + + for element in group.values(): + if not isinstance(element, settings.RadioSetting): + self._add_group(element) + + def cb_copy(self, cut=False): + pass + + def cb_paste(self, data): + pass + + def _apply_settings(self): + try: + all_values = {} + for i in range(self._group_control.GetPageCount()): + page = self._group_control.GetPage(i) + all_values.update(page.get_values()) + for group in self._settings: + self._apply_setting_group(all_values, group) + return True + except Exception as e: + LOG.exception('Failed to apply settings') + wx.MessageBox(str(e), 'Error applying settings', + wx.OK | wx.ICON_ERROR) + return False + + def _apply_setting_group(self, all_values, group): + for element in group.values(): + if isinstance(element, settings.RadioSetting): + if element.value.get_mutable(): + element.value = all_values[element.get_name()] + else: + self._apply_setting_group(all_values, element) + + def _changed(self, event): + wx.PostEvent(self, common.EditorChanged(self.GetId())) + + def saved(self): + for i in range(self._group_control.GetPageCount()): + page = self._group_control.GetPage(i) + page.saved() + + +class ChirpCloneSettingsEdit(ChirpSettingsEdit): + + def __init__(self, *a, **k): + super(ChirpCloneSettingsEdit, self).__init__(*a, **k) + + self._settings = self._radio.get_settings() + + def _initialize(self): + self._load_settings() + + def _pre_propgrid_hook(self, sizer): + pass + + def _changed(self, event): + if not self._apply_settings(): + return + self._radio.set_settings(self._settings) + super(ChirpCloneSettingsEdit, self)._changed(event) + + +class ChirpLiveSettingsEdit(ChirpSettingsEdit): + def _pre_propgrid_hook(self, sizer): + buttons = wx.Panel(self) + hbox = wx.BoxSizer(wx.HORIZONTAL) + buttons.SetSizer(hbox) + sizer.Add(buttons, 0, flag=wx.ALIGN_RIGHT) + + self._apply_btn = wx.Button(buttons, wx.ID_APPLY) + hbox.Add(self._apply_btn, 0, + flag=wx.ALIGN_RIGHT|wx.ALL, border=10) + self._apply_btn.Disable() + self._apply_btn.Bind(wx.EVT_BUTTON, self._apply_settings_button) + + def _apply_setting_edit(self): + # Do not apply settings during edit for live radios + pass + + def _changed(self, event): + self._apply_btn.Enable() + # Do not send the changed event for live radios + + def saved(self): + # Do not allow saved event to change modified statuses + pass + + def _initialize(self): + LOG.debug('Loading settings for live radio') + prog = wx.ProgressDialog('Loading Settings', 'Please wait...', 100, + parent=self) + thread = clone.SettingsThread(self._radio, prog) + thread.start() + prog.ShowModal() + LOG.debug('Settings load complete') + self._settings = thread.settings + self._load_settings() + + def _apply_settings_button(self, event): + if not self._apply_settings(): + return + + prog = wx.ProgressDialog('Applying Settings', 'Please wait...', 100, + parent=self) + thread = clone.SettingsThread(self._radio, prog, self._settings) + thread.start() + prog.ShowModal() + + if thread.error: + wx.MessageBox('Error applying settings: %s' % thread.error, + 'Error', + wx.OK | wx.ICON_ERROR) + else: + self._apply_btn.Disable() + super(ChirpLiveSettingsEdit, self).saved() diff --git a/chirpc b/chirpc new file mode 100755 index 0000000..1663a05 --- /dev/null +++ b/chirpc @@ -0,0 +1,403 @@ +#!/usr/bin/env python +# +# Copyright 2008 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import glob +import serial +import os +import sys +import argparse +import logging + +from chirp import logger +# from chirp.drivers import * +from chirp import chirp_common, errors, directory, util +from chirp.ui import compat + +directory.safe_import_drivers() + +LOG = logging.getLogger("chirpc") +RADIOS = directory.DRV_TO_RADIO + + +def fail_unsupported(): + LOG.error("Operation not supported by selected radio") + sys.exit(1) + + +def fail_missing_mmap(): + LOG.error("mmap-only operation requires specification of an mmap file") + sys.exit(1) + + +class ToneAction(argparse.Action): + def __call__(self, parser, namespace, value, option_string=None): + if value in chirp_common.TONES: + raise argparse.ArgumentError("Invalid tone valeu: %.1f" % value) + setattr(namespace, self.dest, value) + + +class DTCSAction(argparse.Action): + def __call__(self, parser, namespace, value, option_string=None): + try: + value = int(value, 10) + except ValueError: + raise argparse.ArgumentError("Invalid DTCS value: %s" % value) + + if value not in chirp_common.DTCS_CODES: + raise argparse.ArgumentError("Invalid DTCS value: %03i" % value) + setattr(namespace, self.dest, value) + + +class DTCSPolarityAction(argparse.Action): + def __call__(self, parser, namespace, value, option_string=None): + if value not in ["NN", "RN", "NR", "RR"]: + raise optparse.OptionValueError("Invaid DTCS polarity: %s" % value) + setattr(parser.values, option.dest, value) + + +def parse_memory_number(radio, args): + if len(args) < 1: + LOG.error("You must provide an argument specifying the memory number.") + sys.exit(1) + + try: + memnum = int(args[0]) + except ValueError: + memnum = args[0] + + rf = radio.get_features() + start, end = rf.memory_bounds + if not (start <= memnum <= end or memnum in rf.valid_special_chans): + if len(rf.valid_special_chans) > 0: + LOG.error( + "memory number must be between %d and %d or one of %s" + " (got %s)", + start, end, ", ".join(rf.valid_special_chans), memnum) + else: + LOG.error("memory number must be between %d and %d (got %s)", + start, end, memnum) + sys.exit(1) + return memnum + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + logger.add_version_argument(parser) + parser.add_argument("-s", "--serial", dest="serial", + default="mmap", + help="Serial port (default: mmap)") + + parser.add_argument("--list-settings", action="store_true", + help="List settings") + + parser.add_argument("-i", "--id", dest="id", + default=False, + action="store_true", + help="Request radio ID string") + + memarg = parser.add_argument_group("Memory/Channel Options") + memarg.add_argument("--list-mem", action="store_true", + help="List all memory locations") + + memarg.add_argument("--list-special-mem", action="store_true", + help="List all special memory locations") + + memarg.add_argument("--raw", action="store_true", + help="Dump raw memory location") + + memarg.add_argument("--get-mem", action="store_true", + help="Get and print memory location") + memarg.add_argument("--copy-mem", action="store_true", + help="Copy memory location") + memarg.add_argument("--clear-mem", action="store_true", + help="Clear memory location") + + memarg.add_argument("--set-mem-name", help="Set memory name") + memarg.add_argument("--set-mem-freq", type=float, + help="Set memory frequency") + + memarg.add_argument("--set-mem-tencon", action="store_true", + help="Set tone encode enabled flag") + memarg.add_argument("--set-mem-tencoff", action="store_true", + help="Set tone decode disabled flag") + memarg.add_argument("--set-mem-tsqlon", action="store_true", + help="Set tone squelch enabled flag") + memarg.add_argument("--set-mem-tsqloff", action="store_true", + help="Set tone squelch disabled flag") + memarg.add_argument("--set-mem-dtcson", action="store_true", + help="Set DTCS enabled flag") + memarg.add_argument("--set-mem-dtcsoff", action="store_true", + help="Set DTCS disabled flag") + + memarg.add_argument("--set-mem-tenc", + type=float, action=ToneAction, nargs=1, + help="Set memory encode tone") + memarg.add_argument("--set-mem-tsql", + type=float, action=ToneAction, nargs=1, + help="Set memory squelch tone") + + memarg.add_argument("--set-mem-dtcs", + type=int, action=DTCSAction, nargs=1, + help="Set memory DTCS code") + memarg.add_argument("--set-mem-dtcspol", + action=DTCSPolarityAction, nargs=1, + help="Set memory DTCS polarity (NN, NR, RN, RR)") + + memarg.add_argument("--set-mem-dup", + help="Set memory duplex (+,-, or blank)") + memarg.add_argument("--set-mem-offset", type=float, + help="Set memory duplex offset (in MHz)") + + memarg.add_argument("--set-mem-mode", + help="Set mode (%s)" % ",".join(chirp_common.MODES)) + + parser.add_argument("-r", "--radio", dest="radio", + default=None, + help="Radio model (see --list-radios)") + parser.add_argument("--list-radios", action="store_true", + help="List radio models") + parser.add_argument("--mmap", dest="mmap", + default=None, + help="Radio memory map file location") + parser.add_argument("--download-mmap", dest="download_mmap", + action="store_true", + default=False, + help="Download memory map from radio") + parser.add_argument("--upload-mmap", dest="upload_mmap", + action="store_true", + default=False, + help="Upload memory map to radio") + logger.add_arguments(parser) + parser.add_argument("args", metavar="arg", nargs='*', + help="Some commands require additional arguments") + + if len(sys.argv) <= 1: + parser.print_help() + sys.exit(0) + + options = parser.parse_args() + args = options.args + + logger.handle_options(options) + + if options.list_radios: + print("Supported Radios:\n\t", "\n\t".join(sorted(RADIOS.keys()))) + sys.exit(0) + + if options.id: + from chirp import icf + s = serial.Serial(port=options.serial, baudrate=9600, timeout=0.5) + md = icf.get_model_data(s) + print("Model:\n%s" % util.hexprint(md)) + sys.exit(0) + + if not options.radio: + if options.mmap: + rclass = directory.get_radio_by_image(options.mmap).__class__ + else: + print("You must specify a radio model. See --list-radios.") + sys.exit(1) + else: + rclass = directory.get_radio(options.radio) + + if options.serial == "mmap": + if options.mmap: + s = options.mmap + else: + s = options.radio + ".img" + if not os.path.exists(s): + LOG.error("Image file '%s' does not exist" % s) + sys.exit(1) + else: + LOG.info("opening %s at %i" % (options.serial, rclass.BAUD_RATE)) + s = compat.CompatSerial.get(rclass.NEEDS_COMPAT_SERIAL, + port=options.serial, + baudrate=rclass.BAUD_RATE, + timeout=0.5) + + radio = rclass(s) + + if options.list_settings: + print(radio.get_settings()) + sys.exit(0) + + if options.list_mem: + rf = radio.get_features() + start, end = rf.memory_bounds + for i in range(start, end + 1): + mem = radio.get_memory(i) + if mem.empty and not logger.is_visible(logging.INFO): + continue + print(mem) + sys.exit(0) + + if options.list_special_mem: + rf = radio.get_features() + for i in sorted(rf.valid_special_chans): + mem = radio.get_memory(i) + if mem.empty and not logger.is_visible(logging.INFO): + continue + print(mem) + sys.exit(0) + + if options.copy_mem: + src = parse_memory_number(radio, args) + dst = parse_memory_number(radio, args[1:]) + try: + mem = radio.get_memory(src) + except errors.InvalidMemoryLocation as e: + LOG.exception(e) + sys.exit(1) + LOG.info("copying memory %s to %s", src, dst) + mem.number = dst + radio.set_memory(mem) + + if options.clear_mem: + memnum = parse_memory_number(radio, args) + try: + mem = radio.get_memory(memnum) + except errors.InvalidMemoryLocation as e: + LOG.exception(e) + sys.exit(1) + if mem.empty: + LOG.warn("memory %s is already empty, deleting again", memnum) + mem.empty = True + radio.set_memory(mem) + + if options.raw: + memnum = parse_memory_number(radio, args) + data = radio.get_raw_memory(memnum) + for i in data: + if ord(i) > 0x7F: + print("Memory location %s (%i):\n%s" % + (memnum, len(data), util.hexprint(data))) + sys.exit(0) + print(data) + sys.exit(0) + + if options.set_mem_dup is not None: + if options.set_mem_dup != "+" and \ + options.set_mem_dup != "-" and \ + options.set_mem_dup != "": + LOG.error("Invalid duplex value `%s'" % options.set_mem_dup) + LOG.error("Valid values are: '+', '-', ''") + sys.exit(1) + else: + _dup = options.set_mem_dup + else: + _dup = None + + if options.set_mem_mode: + LOG.info("Set mode: %s" % options.set_mem_mode) + if options.set_mem_mode not in chirp_common.MODES: + LOG.error("Invalid mode `%s'") + sys.exit(1) + else: + _mode = options.set_mem_mode + else: + _mode = None + + if options.set_mem_name or options.set_mem_freq or \ + options.set_mem_tencon or options.set_mem_tencoff or \ + options.set_mem_tsqlon or options.set_mem_tsqloff or \ + options.set_mem_dtcson or options.set_mem_dtcsoff or \ + options.set_mem_tenc or options.set_mem_tsql or \ + options.set_mem_dtcs or options.set_mem_dup is not None or \ + options.set_mem_mode or options.set_mem_dtcspol or\ + options.set_mem_offset: + memnum = parse_memory_number(radio, args) + try: + mem = radio.get_memory(memnum) + except errors.InvalidMemoryLocation as e: + LOG.exception(e) + sys.exit(1) + + if mem.empty: + LOG.info("creating new memory (#%s)", memnum) + mem = chirp_common.Memory() + mem.number = memnum + + mem.name = options.set_mem_name or mem.name + mem.freq = options.set_mem_freq or mem.freq + mem.rtone = options.set_mem_tenc or mem.rtone + mem.ctone = options.set_mem_tsql or mem.ctone + mem.dtcs = options.set_mem_dtcs or mem.dtcs + mem.dtcs_polarity = options.set_mem_dtcspol or mem.dtcs_polarity + if _dup is not None: + mem.duplex = _dup + mem.offset = options.set_mem_offset or mem.offset + mem.mode = _mode or mem.mode + + if options.set_mem_tencon: + mem.tencEnabled = True + elif options.set_mem_tencoff: + mem.tencEnabled = False + + if options.set_mem_tsqlon: + mem.tsqlEnabled = True + elif options.set_mem_tsqloff: + mem.tsqlEnabled = False + + if options.set_mem_dtcson: + mem.dtcsEnabled = True + elif options.set_mem_dtcsoff: + mem.dtcsEnabled = False + + radio.set_memory(mem) + + if options.get_mem: + pos = parse_memory_number(radio, args) + try: + mem = radio.get_memory(pos) + except errors.InvalidMemoryLocation as e: + mem = chirp_common.Memory() + mem.number = pos + + print(mem) + sys.exit(0) + + if options.download_mmap: + if not issubclass(rclass, chirp_common.CloneModeRadio): + LOG.error("%s is not a clone mode radio" % options.radio) + sys.exit(1) + if not options.mmap: + LOG.error("You must specify the destination file name with --mmap") + sys.exit(1) + try: + radio.sync_in() + radio.save_mmap(options.mmap) + except Exception as e: + LOG.exception(e) + sys.exit(1) + + if options.upload_mmap: + if not issubclass(rclass, chirp_common.CloneModeRadio): + LOG.error("%s is not a clone mode radio" % options.radio) + sys.exit(1) + if not options.mmap: + LOG.error("You must specify the source file name with --mmap") + sys.exit(1) + try: + radio.load_mmap(options.mmap) + radio.sync_out() + print("Upload successful") + except Exception as e: + LOG.exception(e) + sys.exit(1) + + if options.mmap and isinstance(radio, chirp_common.CloneModeRadio): + radio.save_mmap(options.mmap) diff --git a/chirpw b/chirpw new file mode 100755 index 0000000..4e1c88b --- /dev/null +++ b/chirpw @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# +# Copyright 2014 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import glob +import os +import six + +import sys +import os +import locale +import gettext +import argparse +import logging +import urllib + +if six.PY3: + from gi import pygtkcompat + pygtkcompat.enable() + pygtkcompat.enable_gtk(version='3.0') + +from chirp import chirp_common +from chirp import directory +from chirp import logger +from chirp import elib_intl +from chirp import platform +from chirp.ui import config + +directory.safe_import_drivers() + +LOG = logging.getLogger("chirpw") + +# Does not work with python3 chirpw +# urllib.URLopener.version = chirp_common.http_user_agent() + +localepath = platform.get_platform().find_resource("locale") + +conf = config.get() +manual_language = conf.get("language", "state") +langs = [] +if manual_language and manual_language != "Auto": + lang_codes = {"English": "en_US", + "Polish": "pl", + "Italian": "it", + "Dutch": "nl", + "German": "de", + "Hungarian": "hu", + "Russian": "ru", + "Portuguese (BR)": "pt_BR", + "French": "fr", + "Spanish": "es_ES", + } + try: + LOG.info("Language: %s", lang_codes[manual_language]) + langs = [lang_codes[manual_language]] + except KeyError: + LOG.error("Unsupported language `%s'" % manual_language) +else: + lc, encoding = locale.getdefaultlocale() + if (lc): + langs = [lc] + try: + langs += os.getenv("LANG").split(":") + except: + pass + +try: + if os.name == "nt": + elib_intl._putenv("LANG", langs[0]) + else: + os.putenv("LANG", langs[0]) +except IndexError: + pass +path = "locale" +gettext.bindtextdomain("CHIRP", localepath) +gettext.textdomain("CHIRP") +lang = gettext.translation("CHIRP", localepath, languages=langs, + fallback=True) + + +# Python <2.6 does not have str.format(), which chirp uses to make translation +# strings nicer. So, instead of installing the gettext standard "_()" function, +# we can install our own, which returns a string of the following class, +# which emulates the new behavior, thus allowing us to run on older Python +# versions. +class CompatStr(str): + def format(self, **kwargs): + base = lang.gettext(self) + for k, v in kwargs.items(): + base = base.replace("{%s}" % k, str(v)) + return base + +pyver = sys.version.split()[0] + +try: + vmaj, vmin, vrel = pyver.split(".", 3) +except: + vmaj, vmin = pyver.split(".", 2) + vrel = 0 + +if int(vmaj) == 2 and int(vmin) < 6: + # Python <2.6, emulate str.format() + import __builtin__ + + def lang_with_format(string): + return CompatStr(string) + __builtin__._ = lang_with_format +else: + # Python >=2.6, use normal gettext behavior + lang.install() + +parser = argparse.ArgumentParser() +parser.add_argument("files", metavar="file", nargs='*', help="File to open") +parser.add_argument("--module", metavar="module", + help="Load module on startup") +logger.add_version_argument(parser) +parser.add_argument("--profile", action="store_true", + help="Enable profiling") +logger.add_arguments(parser) +args = parser.parse_args() + +logger.handle_options(args) + +a = None +if True: + from chirp.ui import mainapp + a = mainapp.ChirpMain() + +# Be sure to load module before opening files +if args.module: + a.load_module(args.module) + +for i in args.files: + LOG.info("Opening %s", i) + a.do_open(i) + +a.show() + +if args.profile: + import cProfile + import pstats + import gtk + cProfile.run("gtk.main()", "chirpw.stats") + p = pstats.Stats("chirpw.stats") + p.sort_stats("cumulative").print_stats(10) +else: + import gtk + gtk.main() + +if config._CONFIG: + config._CONFIG.set("last_dir", + platform.get_platform().get_last_dir(), + "state") + config._CONFIG.save() diff --git a/chirpwx.py b/chirpwx.py new file mode 100755 index 0000000..044aca9 --- /dev/null +++ b/chirpwx.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import argparse +import collections +import functools +import gettext +import logging +import os +import sys + +import wx +import wx.aui +import wx.grid +import wx.lib.newevent + +from chirp import chirp_common +from chirp.drivers import ic2820, generic_csv +from chirp import directory +from chirp import logger + +from chirp.wxui import main + + + +if __name__ == '__main__': + gettext.install('CHIRP') + parser = argparse.ArgumentParser() + parser.add_argument("files", metavar="file", nargs='*', + help="File to open") + parser.add_argument("--module", metavar="module", + help="Load module on startup") + logger.add_version_argument(parser) + parser.add_argument("--profile", action="store_true", + help="Enable profiling") + parser.add_argument("--onlydriver", nargs="+", + help="Include this driver while loading") + parser.add_argument("--inspect", action="store_true", + help="Show wxPython inspector") + logger.add_arguments(parser) + args = parser.parse_args() + + logger.handle_options(args) + + directory.safe_import_drivers(limit=args.onlydriver) + + #logging.basicConfig(level=logging.DEBUG) + app = wx.App() + mainwindow = main.ChirpMain(None, title='CHIRP') + mainwindow.Show() + for fn in args.files: + mainwindow.open_file(fn, select=False) + + if args.inspect: + import wx.lib.inspection + wx.lib.inspection.InspectionTool().Show() + + app.MainLoop() diff --git a/locale/Makefile b/locale/Makefile new file mode 100644 index 0000000..5271862 --- /dev/null +++ b/locale/Makefile @@ -0,0 +1,28 @@ +LOCALES = en_US pl it nl de hu ru pt_BR fr uk_UA es_ES +MOFILES = $(patsubst %,%/LC_MESSAGES/CHIRP.mo,$(LOCALES)) + +COPY="Dan Smith " +PKG=CHIRP +XGT_OPTS=--copyright-holder=$(COPY) --package-name=$(PKG) + +all: $(MOFILES) + +clean: + rm -f $(MOFILES) *~ *.orig + find . -name '*.mo' -exec rm -f "{}" \; + find * -depth -type d -exec rmdir "{}" \; + +chirpui.pot: + /usr/bin/find ../chirp/ui -name '*.py' > .files + xgettext -L Python -k_ -o chirpui.pot -f .files $(XGT_OPTS) + +%.po: chirpui.pot + if [ -f $@ ]; then \ + msgmerge -U $@ chirpui.pot; \ + else \ + msginit --input=chirpui.pot --locale=$(@:%.po=%); \ + fi + +%/LC_MESSAGES/CHIRP.mo: %.po + mkdir -p $(shell dirname $@) + msgfmt --output-file=$@ $^ diff --git a/locale/check_parameters.py b/locale/check_parameters.py new file mode 100755 index 0000000..f0f3d51 --- /dev/null +++ b/locale/check_parameters.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import polib +from string import Formatter +import glob + +filelist = glob.glob("*.po") +pos = {filename: polib.pofile(filename) for filename in filelist} + +formatter = Formatter() + +for name, po in pos.iteritems(): + print "Testing", name + for entry in po: + if len(entry.msgstr) > 0: + try: + ids = [field_name + for literal_text, field_name, format_spec, conversion + in formatter.parse(entry.msgid)] + tids = [field_name + for literal_text, field_name, format_spec, conversion + in formatter.parse(entry.msgstr)] + except Exception as e: + print "Got exception!", e, "for entry", entry.msgid + else: + if tids is not None: + missing = [name for name in tids + if name is not None and name not in ids] + if len(missing) > 0: + print "Missing parameters", missing, \ + "in translation of", entry.msgid diff --git a/locale/de.po b/locale/de.po new file mode 100644 index 0000000..c7c8704 --- /dev/null +++ b/locale/de.po @@ -0,0 +1,1081 @@ +# German translations for CHIRP package. +# Copyright (C) 2012 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-10-02 00:01-0700\n" +"PO-Revision-Date: 2012-10-02 22:11+0100\n" +"Last-Translator: Benjamin, HB9EUK \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.5.3\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Abrufen der Speicherbank" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Setze Name für Bank" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Bank" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:258 +#: ../chirpui/importdialog.py:576 ../chirpui/memdetail.py:228 +#: ../chirpui/memedit.py:65 ../chirpui/memedit.py:86 ../chirpui/memedit.py:259 +#: ../chirpui/memedit.py:900 ../chirpui/memedit.py:956 +#: ../chirpui/memedit.py:1098 ../chirpui/memedit.py:1100 +msgid "Name" +msgstr "Name" + +#: ../chirpui/bankedit.py:201 +msgid "Updating bank index for memory {num}" +msgstr "Aktualisiere Bank Index für Speicher {num}" + +#: ../chirpui/bankedit.py:210 +msgid "Updating bank information for memory {num}" +msgstr "Aktualisiere Bankinformation für Speicher {num}" + +#: ../chirpui/bankedit.py:216 ../chirpui/bankedit.py:246 +msgid "Getting memory {num}" +msgstr "Lade Speicher {num}" + +#: ../chirpui/bankedit.py:231 +msgid "Setting index for memory {num}" +msgstr "Setze Index für Speicher {num}" + +#: ../chirpui/bankedit.py:240 +msgid "Getting bank for memory {num}" +msgstr "Lade Bank für Speicher {num}" + +#: ../chirpui/bankedit.py:256 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:174 ../chirpui/memedit.py:258 +#: ../chirpui/memedit.py:327 ../chirpui/memedit.py:347 +#: ../chirpui/memedit.py:361 ../chirpui/memedit.py:432 +#: ../chirpui/memedit.py:444 ../chirpui/memedit.py:468 +#: ../chirpui/memedit.py:470 ../chirpui/memedit.py:543 +#: ../chirpui/memedit.py:557 ../chirpui/memedit.py:559 +#: ../chirpui/memedit.py:600 ../chirpui/memedit.py:602 +#: ../chirpui/memedit.py:643 ../chirpui/memedit.py:849 +#: ../chirpui/memedit.py:898 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:954 +#: ../chirpui/memedit.py:1275 +msgid "Loc" +msgstr "Loc" + +#: ../chirpui/bankedit.py:257 ../chirpui/importdialog.py:577 +#: ../chirpui/memdetail.py:227 ../chirpui/memedit.py:64 +#: ../chirpui/memedit.py:87 ../chirpui/memedit.py:189 +#: ../chirpui/memedit.py:252 ../chirpui/memedit.py:260 +#: ../chirpui/memedit.py:283 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:316 ../chirpui/memedit.py:901 +#: ../chirpui/memedit.py:953 +msgid "Frequency" +msgstr "Frequenz" + +#: ../chirpui/bankedit.py:259 +msgid "Index" +msgstr "Index" + +#: ../chirpui/bankedit.py:346 +msgid "Getting bank information for memory {num}" +msgstr "Lade Bankinformation für Speicher {num}" + +#: ../chirpui/bankedit.py:368 +msgid "Getting bank information" +msgstr "Lade Bankinformation" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} auf {port}" + +#: ../chirpui/clone.py:103 ../chirpui/clone.py:104 ../chirpui/clone.py:166 +msgid "Detect" +msgstr "Erkennung" + +#: ../chirpui/clone.py:127 +msgid "Port" +msgstr "Port" + +#: ../chirpui/clone.py:128 +msgid "Vendor" +msgstr "Hersteller" + +#: ../chirpui/clone.py:129 +msgid "Model" +msgstr "Model" + +#: ../chirpui/clone.py:142 +msgid "Radio" +msgstr "Gerät" + +#: ../chirpui/clone.py:170 +msgid "Unable to detect radio on {port}" +msgstr "Kann Gerät auf {port} nicht erkennen" + +#: ../chirpui/clone.py:182 +msgid "Internal error: Unable to upload to {model}" +msgstr "Interner Fehler: Upload zu {model} nicht möglich" + +#: ../chirpui/clone.py:230 +msgid "Clone failed: {error}" +msgstr "Klonen fehlgeschlagen: {error}" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Klonen Fortschritt" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Klonen" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Abbrechen" + +#: ../chirpui/common.py:216 +msgid "Completed" +msgstr "Beendet" + +#: ../chirpui/common.py:217 +msgid "idle" +msgstr "Ruhezustand" + +#: ../chirpui/common.py:317 +msgid "Details" +msgstr "Details" + +#: ../chirpui/common.py:320 +msgid "Proceed?" +msgstr "Weiter?" + +#: ../chirpui/common.py:329 +msgid "Do not show this next time" +msgstr "Nicht wieder anzeigen" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Rufzeichen" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Ihr Rufzeichen" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Repeater Rufzeichen" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Mein Rufzeichen" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1416 +msgid "Downloading URCALL list" +msgstr "Downloading URCALL Liste" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1428 +msgid "Downloading RPTCALL list" +msgstr "Downloading RPTCALL Liste" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Downloading MYCALL Liste" + +#: ../chirpui/editorset.py:91 +msgid "Memories" +msgstr "Speicher" + +#: ../chirpui/editorset.py:96 +msgid "D-STAR" +msgstr "D-Star" + +#: ../chirpui/editorset.py:102 +msgid "Bank Names" +msgstr "Bank Namen" + +#: ../chirpui/editorset.py:108 +msgid "Banks" +msgstr "Bank" + +#: ../chirpui/editorset.py:114 +msgid "Settings" +msgstr "Einstellungen" + +#: ../chirpui/editorset.py:236 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "Der {vendor} {model} hat mehrere unabhängige Sub-Geräte" + +#: ../chirpui/editorset.py:239 +msgid "Choose one to import from:" +msgstr "Importieren von:" + +#: ../chirpui/editorset.py:244 +msgid "Cancelled" +msgstr "Abgebrochen" + +#: ../chirpui/editorset.py:249 +msgid "Internal Error" +msgstr "Interner Fehler" + +#: ../chirpui/editorset.py:281 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" +"Es gab Fehler beim Öffnen {file}. Die betroffenen Speicher werden nicht " +"importiert!" + +#: ../chirpui/editorset.py:293 +msgid "There was an error during import: {error}" +msgstr "Es gab einen Fehler beim Import: {error}" + +#: ../chirpui/editorset.py:303 +msgid "Unsupported file type" +msgstr "Nicht unterstuetzter Dateityp" + +#: ../chirpui/editorset.py:319 ../chirpui/editorset.py:334 +msgid "There was an error during export: {error}" +msgstr "Es gab einen Fehler beim Export: {error}" + +#: ../chirpui/editorset.py:346 +msgid "Priming memory" +msgstr "Primärspeicher" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another valü for 'New " +"Location' before selection 'Import'" +msgstr "" +"Standort {number} wurde bereits importiert. Wählen Sie einen anderen Wert " +"für 'Neuen Standort' vor der Auswahl 'Import'" + +#: ../chirpui/importdialog.py:123 +msgid "Invalid value. Must be an integer." +msgstr "Ungültiger Wert. Muss eine ganze Zahl sein." + +#: ../chirpui/importdialog.py:132 +msgid "Location {number} is already being imported" +msgstr "Standort {number} wurde bereits importiert" + +#: ../chirpui/importdialog.py:192 +msgid "Updating URCALL list" +msgstr "Updating URCALL Liste" + +#: ../chirpui/importdialog.py:197 +msgid "Updating RPTCALL list" +msgstr "Updating RPTCALL Liste" + +#: ../chirpui/importdialog.py:270 +msgid "Setting memory {number}" +msgstr "Setze Speicher {number}" + +#: ../chirpui/importdialog.py:274 +msgid "Importing bank information" +msgstr "Importieren von Speicherbänke" + +#: ../chirpui/importdialog.py:278 +msgid "Error importing memories:" +msgstr "Fehler beim Speicherimport" + +#: ../chirpui/importdialog.py:390 +msgid "All" +msgstr "Alle" + +#: ../chirpui/importdialog.py:396 +msgid "None" +msgstr "Keine" + +#: ../chirpui/importdialog.py:402 +msgid "Inverse" +msgstr "Umkehren" + +#: ../chirpui/importdialog.py:408 +msgid "Select" +msgstr "Wählen" + +#: ../chirpui/importdialog.py:454 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:460 +msgid "Reverse" +msgstr "Zurücksetzen" + +#: ../chirpui/importdialog.py:466 +msgid "Adjust New Location" +msgstr "Neuen Standort wählen" + +#: ../chirpui/importdialog.py:476 +msgid "Confirm overwrites" +msgstr "Überschreiben bestätigen" + +#: ../chirpui/importdialog.py:482 +msgid "Options" +msgstr "Optionen" + +#: ../chirpui/importdialog.py:535 +msgid "Cannot be imported because" +msgstr "Kann nicht Importiert werden weil" + +#: ../chirpui/importdialog.py:553 +msgid "Import From File" +msgstr "Importieren von Datei" + +#: ../chirpui/importdialog.py:554 ../chirpui/mainapp.py:1360 +msgid "Import" +msgstr "Importieren" + +#: ../chirpui/importdialog.py:574 +msgid "To" +msgstr "Nach" + +#: ../chirpui/importdialog.py:575 +msgid "From" +msgstr "Von" + +#: ../chirpui/importdialog.py:578 ../chirpui/memdetail.py:245 +#: ../chirpui/memedit.py:79 ../chirpui/memedit.py:101 +#: ../chirpui/memedit.py:915 ../chirpui/memedit.py:971 +#: ../chirpui/memedit.py:1103 +msgid "Comment" +msgstr "Kommentar" + +#: ../chirpui/importdialog.py:582 +msgid "Location memory will be imported into" +msgstr "Speicherstandort wird importiert nach" + +#: ../chirpui/importdialog.py:583 +msgid "Location of memory in the file being imported" +msgstr "Speicherposition der importierten Datei" + +#: ../chirpui/importdialog.py:606 +msgid "Preparing memory list..." +msgstr "Speicher Liste vorbereiten..." + +#: ../chirpui/importdialog.py:615 +msgid "Export To File" +msgstr "In Datei exportieren" + +#: ../chirpui/importdialog.py:616 ../chirpui/mainapp.py:1361 +msgid "Export" +msgstr "Exportieren" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Ein Fehler ist aufgetreten" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Überschreiben" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Datei existiert" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Datei {name} existiert bereit, wollen Sie diese überschreiben?" + +#: ../chirpui/mainapp.py:219 ../chirpui/mainapp.py:442 +msgid "Untitled" +msgstr "Namenlos" + +#: ../chirpui/mainapp.py:266 ../chirpui/mainapp.py:705 +msgid "CHIRP Radio Images" +msgstr "CHIRP Radio Images" + +#: ../chirpui/mainapp.py:267 ../chirpui/mainapp.py:704 +#: ../chirpui/mainapp.py:999 +msgid "CHIRP Files" +msgstr "CHIRP Datei" + +#: ../chirpui/mainapp.py:268 ../chirpui/mainapp.py:706 +#: ../chirpui/mainapp.py:998 +msgid "CSV Files" +msgstr "CSV Datei" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:707 +msgid "EVE Files (VX5)" +msgstr "EVE Files (VX5)" + +#: ../chirpui/mainapp.py:270 ../chirpui/mainapp.py:708 +msgid "ICF Files" +msgstr "ICF Datei" + +#: ../chirpui/mainapp.py:271 ../chirpui/mainapp.py:711 +msgid "VX5 Commander Files" +msgstr "VX5 Commander Files" + +#: ../chirpui/mainapp.py:272 ../chirpui/mainapp.py:712 +msgid "VX6 Commander Files" +msgstr "VX6 Commander Files" + +#: ../chirpui/mainapp.py:273 ../chirpui/mainapp.py:713 +msgid "VX7 Commander Files" +msgstr "VX7 Commander Files" + +#: ../chirpui/mainapp.py:283 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"ICF-Dateien können nicht bearbeitet werden, nur angezeigt oder importiert in " +"eine andere Datei. Öffnen im Read-Only-Modus?" + +#: ../chirpui/mainapp.py:326 +msgid "There was an error opening {fname}: {error}" +msgstr "Es gab einen Fehler beim Öffnen {fname}: {error}" + +#: ../chirpui/mainapp.py:341 +msgid "{num} errors during open:" +msgstr "{num} Fehler beim Öffnen:" + +#: ../chirpui/mainapp.py:347 +msgid "Note:" +msgstr "Hinweis:" + +#: ../chirpui/mainapp.py:348 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"Die {vendor} {model} arbeitet im Live-Modus. Dies bedeutet, dass " +"alle Änderungen sofort an das Radio gesendet werden. Aus diesem Grund können " +"Sie die Funktionen Speichern oder hochladen nicht " +"verwenden. Wenn Sie die Inhalte offline bearbeiten wollen, bitte mit " +"Exportieren in eine CSV-Datei speichern, unter Datei Exportieren ." + +#: ../chirpui/mainapp.py:357 +msgid "Don't show this again" +msgstr "Nicht wieder anzeigen" + +#: ../chirpui/mainapp.py:402 +msgid "{vendor} {model} image file" +msgstr "{vendor} {model} Imagedatei" + +#: ../chirpui/mainapp.py:410 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:412 +msgid "VX6 Commander" +msgstr "VX6 Commander" + +#: ../chirpui/mainapp.py:414 +msgid "EVE" +msgstr "EVE" + +#: ../chirpui/mainapp.py:415 +msgid "VX5 Commander" +msgstr "VX5 Commander" + +#: ../chirpui/mainapp.py:477 +msgid "Open recent file {name}" +msgstr "Letzte Datei öffnen {name}" + +#: ../chirpui/mainapp.py:538 +msgid "Import stock configuration {name}" +msgstr "Speichervorgabe importieren {name}" + +#: ../chirpui/mainapp.py:554 +msgid "Open stock configuration {name}" +msgstr "Vorgaben öffnen {name}" + +#: ../chirpui/mainapp.py:576 +msgid "Proceed with experimental driver?" +msgstr "Weiter mit dem experimentellen Treiber?" + +#: ../chirpui/mainapp.py:578 +msgid "This radio's driver is experimental. Do you want to proceed?" +msgstr "Dieser Geräte Treiber ist experimentell. Wollen Sie fortfahren?" + +#: ../chirpui/mainapp.py:671 +msgid "Save Changes?" +msgstr "Änderungen speichern?" + +#: ../chirpui/mainapp.py:676 +msgid "File is modified, save changes before closing?" +msgstr "Datei wurde geändert, speichern Sie die Änderungen vor dem Schliessen?" + +#: ../chirpui/mainapp.py:709 +msgid "Kenwood HMK Files" +msgstr "Kenwood HMK Datei" + +#: ../chirpui/mainapp.py:710 +msgid "Travel Plus Files" +msgstr "Travel Plus Datei" + +#: ../chirpui/mainapp.py:1042 +msgid "With significant contributions by:" +msgstr "Mit bedeutenden Beiträgen von:" + +#: ../chirpui/mainapp.py:1066 +msgid "Select Columns" +msgstr "Spalten auswählen" + +#: ../chirpui/mainapp.py:1081 +msgid "Visible columns for {radio}" +msgstr "Sichtbare Spalten für {radio}" + +#: ../chirpui/mainapp.py:1138 +msgid "Reporting is disabled" +msgstr "Report ist deaktiviert" + +#: ../chirpui/mainapp.py:1139 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"Die Reporting-Funktion von CHIRP soll helfen, die Qualität ständig zu " +"verbessern, in dem sich die Autoren auf die Fehler der am häufigsten " +"verwendeten Radio-Treiber konzentrieren können. Die Berichte enthalten keine " +"identifizierbaren Informationen und werden zu rein statistischen Zwecken " +"verwendet. Ihre Privatsphäre ist uns sehr wichtig, bitte lassen Sie diese " +"Funktion aktiviert, so helfen Sie mit, CHIRP weiter zu verbessern! \n" +"\n" +"Sind Sie sicher, dass Sie diese Funktion deaktivieren wollen?" + +#: ../chirpui/mainapp.py:1172 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" +"Wählen Sie eine Sprache oder Auto, um die Defaultsprache vom Betriebssystem " +"zu verwenden. Bevor die Änderungen wirksam werden, müssen Sie Chirp neu " +"starten." + +#: ../chirpui/mainapp.py:1185 +msgid "Python Modules" +msgstr "Python Modules" + +#: ../chirpui/mainapp.py:1332 +msgid "_File" +msgstr "_Datei" + +#: ../chirpui/mainapp.py:1335 +msgid "Open stock config" +msgstr "Speichervorgaben öffnen" + +#: ../chirpui/mainapp.py:1336 +msgid "_Recent" +msgstr "_Aktuell" + +#: ../chirpui/mainapp.py:1339 +msgid "Load Module" +msgstr "Module laden" + +#: ../chirpui/mainapp.py:1342 +msgid "_Edit" +msgstr "_Bearbeiten" + +#: ../chirpui/mainapp.py:1343 +msgid "_Cut" +msgstr "_Ausschneiden" + +#: ../chirpui/mainapp.py:1344 +msgid "_Copy" +msgstr "_Kopieren" + +#: ../chirpui/mainapp.py:1345 +msgid "_Paste" +msgstr "_Einfügen" + +#: ../chirpui/mainapp.py:1346 +msgid "_Delete" +msgstr "_Löschen" + +#: ../chirpui/mainapp.py:1347 +msgid "Move _Up" +msgstr "Nach _Oben" + +#: ../chirpui/mainapp.py:1348 +msgid "Move Dow_n" +msgstr "Nach _Unten" + +#: ../chirpui/mainapp.py:1349 +msgid "E_xchange" +msgstr "A_ustausch" + +#: ../chirpui/mainapp.py:1350 +msgid "_View" +msgstr "_Ansicht" + +#: ../chirpui/mainapp.py:1351 +msgid "Columns" +msgstr "Spalten" + +#: ../chirpui/mainapp.py:1352 +msgid "Developer" +msgstr "Entwickler" + +#: ../chirpui/mainapp.py:1353 +msgid "Show raw memory" +msgstr "Zeige Roh Speicher" + +#: ../chirpui/mainapp.py:1354 +msgid "Diff raw memories" +msgstr "Vergleiche Roh Speicher" + +#: ../chirpui/mainapp.py:1355 +msgid "Diff tabs" +msgstr "Registerkarten vergleichen" + +#: ../chirpui/mainapp.py:1356 +msgid "Change language" +msgstr "Sprache wählen" + +#: ../chirpui/mainapp.py:1357 +msgid "_Radio" +msgstr "_Gerät" + +#: ../chirpui/mainapp.py:1358 +msgid "Download From Radio" +msgstr "Download vom Gerät" + +#: ../chirpui/mainapp.py:1359 +msgid "Upload To Radio" +msgstr "Upload zum Gerät" + +#: ../chirpui/mainapp.py:1362 +msgid "Import from data source" +msgstr "Import aus der Datenquelle" + +#: ../chirpui/mainapp.py:1363 ../chirpui/mainapp.py:1367 +msgid "RadioReference.com" +msgstr "RadioReference.com" + +#: ../chirpui/mainapp.py:1364 ../chirpui/mainapp.py:1368 +msgid "RFinder" +msgstr "RFinder" + +#: ../chirpui/mainapp.py:1365 ../chirpui/mainapp.py:1369 +msgid "RepeaterBook" +msgstr "RepeaterBook" + +#: ../chirpui/mainapp.py:1366 +msgid "Query data source" +msgstr "Datenquelle abfragen" + +#: ../chirpui/mainapp.py:1370 +msgid "CHIRP Native File" +msgstr "CHIRP Native File" + +#: ../chirpui/mainapp.py:1371 +msgid "CSV File" +msgstr "CSV Datei" + +#: ../chirpui/mainapp.py:1372 +msgid "Import from stock config" +msgstr "Import von der Vorgabe" + +#: ../chirpui/mainapp.py:1374 +msgid "Help" +msgstr "Hilfe" + +#: ../chirpui/mainapp.py:1385 +msgid "Report statistics" +msgstr "Report Statistik" + +#: ../chirpui/mainapp.py:1386 +msgid "Hide Unused Fields" +msgstr "Unbenutzte Felder verbergen" + +#: ../chirpui/mainapp.py:1387 +msgid "Automatic Repeater Offset" +msgstr "Automatischer Repeater-Offset" + +#: ../chirpui/mainapp.py:1388 +msgid "Enable Developer Functions" +msgstr "Entwickler Funktionen aktivieren" + +#: ../chirpui/mainapp.py:1498 +msgid "A new version of CHIRP is available: " +msgstr "Eine neue Version von CHIRP ist verfügbar:" + +#: ../chirpui/mainapp.py:1572 +msgid "Error reporting is enabled" +msgstr "Fehler Reporting ist eingeschaltet" + +#: ../chirpui/mainapp.py:1575 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "" +"Wenn Sie diese Funktion deaktivieren möchten, können Sie das im Hilfe " +"Menu machen" + +#: ../chirpui/memdetail.py:214 +msgid "Edit Memory#{num}" +msgstr "Speiche editieren#{num}" + +#: ../chirpui/memdetail.py:229 ../chirpui/memedit.py:66 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:116 +#: ../chirpui/memedit.py:206 ../chirpui/memedit.py:324 +#: ../chirpui/memedit.py:902 ../chirpui/memedit.py:962 +#: ../chirpui/memedit.py:1104 ../chirpui/memedit.py:1168 +msgid "Tone Mode" +msgstr "Tone Mode" + +#: ../chirpui/memdetail.py:230 ../chirpui/memedit.py:67 +#: ../chirpui/memedit.py:88 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:218 ../chirpui/memedit.py:227 +#: ../chirpui/memedit.py:230 ../chirpui/memedit.py:322 +#: ../chirpui/memedit.py:903 ../chirpui/memedit.py:958 +#: ../chirpui/memedit.py:1105 +msgid "Tone" +msgstr "Tone" + +#: ../chirpui/memdetail.py:231 ../chirpui/memedit.py:68 +#: ../chirpui/memedit.py:89 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:228 +#: ../chirpui/memedit.py:231 ../chirpui/memedit.py:322 +#: ../chirpui/memedit.py:904 ../chirpui/memedit.py:959 +#: ../chirpui/memedit.py:1101 +msgid "ToneSql" +msgstr "ToneSql" + +#: ../chirpui/memdetail.py:232 ../chirpui/memedit.py:69 +#: ../chirpui/memedit.py:90 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:213 ../chirpui/memedit.py:220 +#: ../chirpui/memedit.py:225 ../chirpui/memedit.py:232 +#: ../chirpui/memedit.py:318 ../chirpui/memedit.py:905 +#: ../chirpui/memedit.py:960 ../chirpui/memedit.py:1093 +msgid "DTCS Code" +msgstr "DTCS Code" + +#: ../chirpui/memdetail.py:233 ../chirpui/memedit.py:71 +#: ../chirpui/memedit.py:92 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:215 ../chirpui/memedit.py:222 +#: ../chirpui/memedit.py:234 ../chirpui/memedit.py:907 +#: ../chirpui/memedit.py:964 ../chirpui/memedit.py:1095 +#: ../chirpui/memedit.py:1173 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memdetail.py:234 +msgid "Cross mode" +msgstr "Cross Mode" + +#: ../chirpui/memdetail.py:237 ../chirpui/memedit.py:73 +#: ../chirpui/memedit.py:94 ../chirpui/memedit.py:114 +#: ../chirpui/memedit.py:142 ../chirpui/memedit.py:207 +#: ../chirpui/memedit.py:261 ../chirpui/memedit.py:324 +#: ../chirpui/memedit.py:909 ../chirpui/memedit.py:965 +#: ../chirpui/memedit.py:1106 ../chirpui/memedit.py:1178 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memdetail.py:238 ../chirpui/memedit.py:74 +#: ../chirpui/memedit.py:95 ../chirpui/memedit.py:140 +#: ../chirpui/memedit.py:200 ../chirpui/memedit.py:237 +#: ../chirpui/memedit.py:262 ../chirpui/memedit.py:320 +#: ../chirpui/memedit.py:910 ../chirpui/memedit.py:966 +#: ../chirpui/memedit.py:1097 +msgid "Offset" +msgstr "Offset" + +#: ../chirpui/memdetail.py:239 ../chirpui/memedit.py:75 +#: ../chirpui/memedit.py:96 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:308 ../chirpui/memedit.py:911 +#: ../chirpui/memedit.py:967 ../chirpui/memedit.py:1096 +#: ../chirpui/memedit.py:1167 ../chirpui/memedit.py:1180 +#: ../chirpui/memedit.py:1181 ../chirpui/memedit.py:1340 +#: ../chirpui/memedit.py:1358 ../chirpui/memedit.py:1368 +msgid "Mode" +msgstr "Mode" + +#: ../chirpui/memdetail.py:241 ../chirpui/memedit.py:77 +#: ../chirpui/memedit.py:98 ../chirpui/memedit.py:115 +#: ../chirpui/memedit.py:145 ../chirpui/memedit.py:148 +#: ../chirpui/memedit.py:913 ../chirpui/memedit.py:969 +#: ../chirpui/memedit.py:1099 +msgid "Tune Step" +msgstr "Abstimmungsschritt" + +#: ../chirpui/memdetail.py:244 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:100 ../chirpui/memedit.py:914 +#: ../chirpui/memedit.py:970 ../chirpui/memedit.py:1107 +#: ../chirpui/memedit.py:1170 +msgid "Skip" +msgstr "Überspringen" + +#: ../chirpui/memdetail.py:289 +msgid "Memory validation failed:" +msgstr "Speicher-Validierung fehlgeschlagen:" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Ungültiger Wert für dieses Feld" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:91 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:221 +#: ../chirpui/memedit.py:233 ../chirpui/memedit.py:318 +#: ../chirpui/memedit.py:906 ../chirpui/memedit.py:961 +#: ../chirpui/memedit.py:1094 +msgid "DTCS Rx Code" +msgstr "DTCS Rx Code" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:93 ../chirpui/memedit.py:117 +#: ../chirpui/memedit.py:908 ../chirpui/memedit.py:963 +#: ../chirpui/memedit.py:1102 ../chirpui/memedit.py:1169 +msgid "Cross Mode" +msgstr "Cross Mode" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:97 ../chirpui/memedit.py:113 +#: ../chirpui/memedit.py:308 ../chirpui/memedit.py:912 +#: ../chirpui/memedit.py:968 ../chirpui/memedit.py:1108 +#: ../chirpui/memedit.py:1171 ../chirpui/memedit.py:1176 +msgid "Power" +msgstr "Leistung" + +#: ../chirpui/memedit.py:177 +msgid "Erasing memory {loc}" +msgstr "Lösche Speicher {loc}" + +#: ../chirpui/memedit.py:247 +msgid "Unable to make changes to this model" +msgstr "Keine Änderungen bei diesem Modell möglich" + +#: ../chirpui/memedit.py:253 +msgid "Editing new item, taking defaults" +msgstr "Neues Element editieren, verwende Standardwerte" + +#: ../chirpui/memedit.py:269 +msgid "Bad value for {col}: {val}" +msgstr "Falscher Wert für {col}: {val}" + +#: ../chirpui/memedit.py:293 +msgid "Error setting memory" +msgstr "Fehler beim Setzen der Speicher" + +#: ../chirpui/memedit.py:301 ../chirpui/memedit.py:368 +#: ../chirpui/memedit.py:635 ../chirpui/memedit.py:1323 +msgid "Writing memory {number}" +msgstr "Schreibe Speicher {number}" + +#: ../chirpui/memedit.py:373 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Dieser Vorgang erfordert eine Verschiebung aller nachfolgenden Kanäle, bis " +"eine leere Stelle gefunden wird. Dies kann sehr lange dauern. Sind Sie " +"sicher dass Sie das wollen?" + +#: ../chirpui/memedit.py:396 +msgid "Adding memory {number}" +msgstr "Hinzufügen von Speicher {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:943 +msgid "Erasing memory {number}" +msgstr "Löschen von Speicher {number}" + +#: ../chirpui/memedit.py:418 ../chirpui/memedit.py:527 +#: ../chirpui/memedit.py:573 ../chirpui/memedit.py:578 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:1202 +msgid "Getting memory {number}" +msgstr "Lade Speicher {number}" + +#: ../chirpui/memedit.py:506 ../chirpui/memedit.py:517 +#: ../chirpui/memedit.py:565 +msgid "Moving memory from {old} to {new}" +msgstr "Verschiebe Speicher von {old} nach {new}" + +#: ../chirpui/memedit.py:587 +msgid "Raw memory {number}" +msgstr "Rohspeicher {number}" + +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:619 +#: ../chirpui/memedit.py:624 +msgid "Getting raw memory {number}" +msgstr "Lade Rohspeicher {number}" + +#: ../chirpui/memedit.py:596 +msgid "You can only diff two memories!" +msgstr "Sie können nur zwei Speicher vergleichen!" + +#: ../chirpui/memedit.py:607 +msgid "Memory {number}" +msgstr "Speicher {number}" + +#: ../chirpui/memedit.py:613 +msgid "Diff of {a} and {b}" +msgstr "Diff of {a} and {b}" + +#: ../chirpui/memedit.py:650 +msgid "Memories must be contiguous" +msgstr "Speicher müssen fortlaufend sein" + +#: ../chirpui/memedit.py:726 +msgid "Edit" +msgstr "Bearbeiten" + +#: ../chirpui/memedit.py:727 +msgid "Insert row above" +msgstr "Zeile oben einfügen" + +#: ../chirpui/memedit.py:728 +msgid "Insert row below" +msgstr "Zeile unten einfügen" + +#: ../chirpui/memedit.py:729 +msgid "Delete" +msgstr "Löschen" + +#: ../chirpui/memedit.py:729 +msgid "Delete all" +msgstr "Alles löschen" + +#: ../chirpui/memedit.py:730 +msgid "Delete (and shift up)" +msgstr "Löschen (und nach oben verschieben)" + +#: ../chirpui/memedit.py:731 +msgid "Move up" +msgstr "Nach Oben" + +#: ../chirpui/memedit.py:732 +msgid "Move down" +msgstr "Nach Unten" + +#: ../chirpui/memedit.py:733 +msgid "Exchange memories" +msgstr "Speicher austauschen" + +#: ../chirpui/memedit.py:734 +msgid "Cut" +msgstr "Ausschneiden" + +#: ../chirpui/memedit.py:735 +msgid "Copy" +msgstr "Kopieren" + +#: ../chirpui/memedit.py:736 +msgid "Paste" +msgstr "Einfügen" + +#: ../chirpui/memedit.py:737 +msgid "Show Raw Memory" +msgstr "Zeige RAW Speicher" + +#: ../chirpui/memedit.py:738 +msgid "Diff Raw Memories" +msgstr "Vergleiche Rohspeicher" + +#: ../chirpui/memedit.py:862 +msgid "Internal Error: Column {name} not found" +msgstr "Interner Fehler: Spalte {name} wurde nicht gefunden" + +#: ../chirpui/memedit.py:891 +msgid "Getting channel {chan}" +msgstr "Lade Kanal {chan}" + +#: ../chirpui/memedit.py:983 +msgid "Internal Error: Invalid limit {number}" +msgstr "Interner Fehler: Ungültige Grenzwert {number}" + +#: ../chirpui/memedit.py:993 +msgid "Memory range:" +msgstr "Speicherbereich:" + +#: ../chirpui/memedit.py:1020 +msgid "Go" +msgstr "Go" + +#: ../chirpui/memedit.py:1043 +msgid "Special Channels" +msgstr "Spezial Kanäle" + +#: ../chirpui/memedit.py:1050 +msgid "Show Empty" +msgstr "Leere anzeigen" + +#: ../chirpui/memedit.py:1235 +msgid "Cutting memory {number}" +msgstr "Ausschneiden Speicher {number}" + +#: ../chirpui/memedit.py:1266 +msgid "" +"Unable to paste {src} memories into {dst} rows. Increase the memory bounds " +"or show empty memories." +msgstr "" +"Kann {src} Speicher in {dst} Zeilen einfügen. Erhöhen Sie die Speicher-" +"Grenzen oder zeigen leere Speicher." + +#: ../chirpui/memedit.py:1277 +msgid "Overwrite?" +msgstr "Überschreiben?" + +#: ../chirpui/memedit.py:1282 +msgid "Overwrite location {number}?" +msgstr "Überschreibe Standort {number}" + +#: ../chirpui/memedit.py:1301 +msgid "Incompatible Memory" +msgstr "Speicher nicht kompatibel" + +#: ../chirpui/memedit.py:1304 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "" +"Eingefügter Speicher {number} ist nicht mit diesem Gerät kompatibel weil:" + +#: ../chirpui/memedit.py:1360 ../chirpui/memedit.py:1375 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1360 ../chirpui/memedit.py:1376 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1360 ../chirpui/memedit.py:1377 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1361 ../chirpui/memedit.py:1378 +msgid "Digital Code" +msgstr "Digital Code" + +#: ../chirpui/settingsedit.py:138 +msgid "Enabled" +msgstr "Aktiviert" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "verschieben" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Verschieben {src} nach {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Suche nach einer freien Stelle ({number})" + +#: ../chirpui/shiftdialog.py:93 +msgid "No space to insert a row" +msgstr "Kein Platz zum Einfügen einer Zeile" + +#: ../chirpui/shiftdialog.py:140 +msgid "Moved {count} memories" +msgstr "Verschobene {count} Speicher" diff --git a/locale/en_US.po b/locale/en_US.po new file mode 100644 index 0000000..b394796 --- /dev/null +++ b/locale/en_US.po @@ -0,0 +1,955 @@ +# English translations for CHIRP package. +# Copyright (C) 2011 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2011-11-29 16:07-0800\n" +"Last-Translator: Dan Smith \n" +"Language-Team: English\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "" + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "" + +#: ../chirpui/importdialog.py:264 +#, fuzzy +msgid "Error importing memories:" +msgstr "Error reporting is enabled" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "" + +#: ../chirpui/importdialog.py:394 +#, fuzzy +msgid "Select" +msgstr "Select Columns" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "" + +#: ../chirpui/importdialog.py:513 +#, fuzzy +msgid "Import From File" +msgstr "Import from RFinder" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Import" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "" + +#: ../chirpui/importdialog.py:575 +#, fuzzy +msgid "Export To File" +msgstr "Import from RFinder" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Export" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Untitled" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "CHIRP Radio Images" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "CHIRP Files" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "CSV Files" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "ICF Files" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "VX7 Commander Files" + +#: ../chirpui/mainapp.py:330 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "There was an error opening {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Note:" + +#: ../chirpui/mainapp.py:395 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Don't show this again" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "{vendor} {model} image file" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Open recent file {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Discard Changes?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "File is modified, save changes before closing?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Select Columns" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Visible columns for {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Reporting is disabled" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" + +#: ../chirpui/mainapp.py:1045 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_File" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Recent" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Edit" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Cut" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Copy" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Paste" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Delete" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Move _Up" + +#: ../chirpui/mainapp.py:1184 +#, fuzzy +msgid "Move Dow_n" +msgstr "Move D_n" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "E_xchange" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "_View" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Columns" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Developer" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Show raw memory" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Diff raw memories" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Radio" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Download From Radio" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Upload To Radio" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Import from RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "CHIRP Native File" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "CSV File" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Import from RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +#, fuzzy +msgid "Import from stock config" +msgstr "Import from RepeaterBook" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Help" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Report statistics" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Hide Unused Fields" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Automatic Repeater Offset" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Enable Developer Functions" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Error reporting is enabled" + +#: ../chirpui/mainapp.py:1355 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "" +"If you wish to disable this feature you may do so in the Help menu" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "" + +#: ../chirpui/clone.py:35 +#, fuzzy +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} image file" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "" + +#: ../chirpui/clone.py:138 +#, fuzzy +msgid "Radio" +msgstr "_Radio" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "" + +#: ../chirpui/editorset.py:87 +#, fuzzy +msgid "Memories" +msgstr "Diff raw memories" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "" + +#: ../chirpui/editorset.py:248 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" + +#: ../chirpui/editorset.py:260 +#, fuzzy +msgid "There was an error during import: {error}" +msgstr "There was an error opening {fname}: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +#, fuzzy +msgid "There was an error during export: {error}" +msgstr "There was an error opening {fname}: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:361 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete" +msgstr "_Delete" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete all" +msgstr "_Delete" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "" + +#: ../chirpui/memedit.py:704 +#, fuzzy +msgid "Move up" +msgstr "Move _Up" + +#: ../chirpui/memedit.py:705 +#, fuzzy +msgid "Move down" +msgstr "Move D_n" + +#: ../chirpui/memedit.py:706 +#, fuzzy +msgid "Exchange memories" +msgstr "E_xchange" + +#: ../chirpui/memedit.py:707 +#, fuzzy +msgid "Cut" +msgstr "_Cut" + +#: ../chirpui/memedit.py:708 +#, fuzzy +msgid "Copy" +msgstr "_Copy" + +#: ../chirpui/memedit.py:709 +#, fuzzy +msgid "Paste" +msgstr "_Paste" + +#: ../chirpui/memedit.py:710 +#, fuzzy +msgid "Show Raw Memory" +msgstr "Show raw memory" + +#: ../chirpui/memedit.py:711 +#, fuzzy +msgid "Diff Raw Memories" +msgstr "Diff raw memories" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number" +msgstr "" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "" + +#~ msgid "%i errors during open, check the debug log for details" +#~ msgstr "%i errors during open, check the debug log for details" diff --git a/locale/es_ES.po b/locale/es_ES.po new file mode 100644 index 0000000..20ca77d --- /dev/null +++ b/locale/es_ES.po @@ -0,0 +1,946 @@ +# Spanish translations for CHIRP package. +# Copyright (C) 2011 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2016-03-28 02:44+0100\n" +"Last-Translator: Alfonso Moratalla \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.5.4\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Completado" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "inactivo" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Recibiendo información del banco" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Poniendo nombre al banco" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Banco" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Nombre" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Actualizando índice del banco para memoria {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Actualizando información del banco para memoria {num}" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Obteniendo memoria {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Fijando índice para memoria {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Obteniendo banco para memoria {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Loc" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Frecuencia" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "Índice" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Obteniendo información del banco para memoria {num}" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Obteniendo información del banco" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Ha ocurrido un error" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Sobreescribir" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "El fichero existe" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "El fichero {name} ya existe. ¿Quiere sobreescribirlo?" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "" +"La localización {number} ya ha sido importada. Elija otro valor para 'Nueva " +"Localización' antes de seleccionar 'Importar'" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Valor no válido. Debe ser un número entero." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "Localización {number} ya está siendo importada" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Actualizando listado URCALL" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Actualizando listado RPTCALL" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Fijando memoria {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Importando información del banco" + +#: ../chirpui/importdialog.py:264 +msgid "Error importing memories:" +msgstr "Error importando memorias:" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Todos" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Ninguno" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Inverso" + +#: ../chirpui/importdialog.py:394 +msgid "Select" +msgstr "Seleccionar" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Reverso" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Ajustar nueva localización" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Confirmar sobreescritura" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Opciones" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "No puede ser importado porque" + +#: ../chirpui/importdialog.py:513 +msgid "Import From File" +msgstr "Importar desde fichero" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Importar" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "A" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "Desde" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Comentario" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "La localización de memoria será importada en" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Localización de la memoria en el fichero que está siendo importado" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Preparando lista de memoria..." + +#: ../chirpui/importdialog.py:575 +msgid "Export To File" +msgstr "Exportar a fichero" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Exportar" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Sin nombre" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Imagenes de radio CHIRP" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "Ficheros CHIRP" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "Ficheros CSV" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "Ficheros ICF" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "Ficheros del VX7 Commander" + +#: ../chirpui/mainapp.py:330 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"Los ficheros ICF no se pueden editar, solo mostrarse o importarse en otro " +"fichero. ¿Abrirlo en modo solo-lectura?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Hubo un error abriendo {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} errores mientras se abría:" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Nota:" + +#: ../chirpui/mainapp.py:395 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"El {vendor} {model} opera en modo live. Esto significa que cualquier " +"cambio que haga es enviado inmediatamente a la radio. Por esto, no puede " +"usar las operaciones Guardar o Subir. Si quiere editar el " +"contenido offline, por favor Exporte a un fichero CSV, usando el menu " +"Archivo." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "No mostrar esto otra vez" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "fichero de imagen para {vendor} {model}" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Abrir fichero reciente {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Importar configuración por defecto {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Abrir configuración por defecto {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "¿Descartar cambios?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "El fichero ha sido modificado, ¿guardar los cambios antes de cerrarlo?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "Con contribuciones significativas de:" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Seleccionar columnas" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Columnas visibles para {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Informar está deshabilitado" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"La característica de informar de CHIRP está diseñada para ayudar a " +"mejorar la calidad permitiendo a los autores centrarse en los drivers " +"de las radios usadas mas frecuentemente y los errores que se experimentan " +"los usuarios. Los informes no contienen información que identifique al " +"usuario y solo son usadas para fines estadísticos por los autores. Su " +"privacidad es extremadamente importante, pero por favor considere dejar " +"esta característica habilitada para ayudar a hacer CHIRP mejor\n" +"\n" +"¿Seguro que quiere deshabilitar esta caracteristica?" + +#: ../chirpui/mainapp.py:1045 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" +"Elija un idioma o Auto para usar el de por defecto del sistema operativo. " +"Tendrá que reiniciar la aplicación para que el cambio surta efecto." + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Fichero" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Abrir configuración por defecto" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Reciente" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Editar" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Cortar" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Copiar" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Pegar" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Borrar" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Subir" + +#: ../chirpui/mainapp.py:1184 +msgid "Move Dow_n" +msgstr "Mover Abaj_o" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "I_ntercambio" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "_Ver" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Columnas" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Desarrollador" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Mostrar memoria raw" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Diferencias entre memorias raw" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Pestañas de diferencias" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Cambiar idioma" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Radio" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Descargar desde radio" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Subir a radio" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Importar desde RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "Fichero CHIRP nativo" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "Fichero CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Importar desde RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +msgid "Import from stock config" +msgstr "Importar desde configuración por defecto" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Ayuda" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Enviar estadísticas" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Ocultar campos no usados" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Desplazamiento del repetidor automático" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Habilitar funciones de desarrollador" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Informar de errores está habilitado" + +#: ../chirpui/mainapp.py:1355 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "" +"Si quieres deshabilitar esta característica tiene que hacerlo en el menú " +"Ayuda" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Progreso del clonado" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Clonando" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Cancelar" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Desplazamiento" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Moviendo {src} a {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Buscando un hueco libre({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "Movidas {count} memorias" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} en {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Detectar" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Puerto" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Proveedor" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Modelo" + +#: ../chirpui/clone.py:138 +msgid "Radio" +msgstr "Radio" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "No se puede detectar radio en {port}" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Error interno: No se ha podido subir a {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Clonado fallido: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Indicativo" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Tu indicativo" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Indicativo del repetidor" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Mi indicativ" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Descargando lista URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Descargando lista RPTCALL" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Descargando lista MYCALL" + +#: ../chirpui/editorset.py:87 +msgid "Memories" +msgstr "Memorias" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Nombres de los bancos" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Bancos" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "El {vendor} {model} tiene multiples sub-dispositivos independientes" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Elija uno desde el que importar:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Cancelado" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Error interno" + +#: ../chirpui/editorset.py:248 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" +"Hubo errores mientras se abría {file}. ¡Las memorias afectadas no se podrán " +"importar!" + +#: ../chirpui/editorset.py:260 +msgid "There was an error during import: {error}" +msgstr "Hubo un error durante la importación: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Tipo de fichero no soportado" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +msgid "There was an error during export: {error}" +msgstr "Hubo un error mientras se exportaba: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Priming memory" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Valor no válido para este campo" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Modo del tono" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Tono" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "ToneSql" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "Código DTCS" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Modo cruzado" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Desplazamiento" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Modo" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Potencia" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Escalón de sintonía" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Saltar" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Borrando memoria {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "No se pueden hacer cambios a este modelo" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Editando elemento nuevo, tomando valores por defecto" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Valor incorrecto para {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Error fijando memoria" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Escribiendo memoria {number}" + +#: ../chirpui/memedit.py:361 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Esta operación requiere mover los subsecuentes canales un hueco hasta " +"encontrar una localización vacía. Esto puede tardar MUCHO tiempo. ¿Está " +"seguro de hacerlo?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Añadiendo memoria {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Borrando memoria {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Obteniendo memoria {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Moviendo memoria de {old} a {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Memoria raw {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Obteniendo memoria raw {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "¡Solo puede ver diferencias entre dos memorias!" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Memoria {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Diferencia entre {a} y {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Las memorias deben ser continuas" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Insertar fila encima" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Insertar fila debajo" + +#: ../chirpui/memedit.py:702 +msgid "Delete" +msgstr "Borrar" + +#: ../chirpui/memedit.py:702 +msgid "Delete all" +msgstr "Borrar todo" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Borrar (y desplazar hacia arriba)" + +#: ../chirpui/memedit.py:704 +msgid "Move up" +msgstr "Subir" + +#: ../chirpui/memedit.py:705 +msgid "Move down" +msgstr "Bajar" + +#: ../chirpui/memedit.py:706 +msgid "Exchange memories" +msgstr "Intercambiar memorias" + +#: ../chirpui/memedit.py:707 +msgid "Cut" +msgstr "Cortar" + +#: ../chirpui/memedit.py:708 +msgid "Copy" +msgstr "Copiar" + +#: ../chirpui/memedit.py:709 +msgid "Paste" +msgstr "Pegar" + +#: ../chirpui/memedit.py:710 +msgid "Show Raw Memory" +msgstr "Mostrar memoria raw" + +#: ../chirpui/memedit.py:711 +msgid "Diff Raw Memories" +msgstr "Diferencias entre memorias raw" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Error interno: La columna {name} no se encuentra" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Obteniendo canal {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Error interno: límite {number} invalido" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Rango de memoria:" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Ir" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Canales especiales" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Mostrar vacíos" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Cortando memoria {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "¿Sobrescribir?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "¿Sobrescribir localización {number}?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Memoria incompatible" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "Memoria {number} pegada no es compatible con esta radio porque:" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Código digital" + +#~ msgid "%i errors during open, check the debug log for details" +#~ msgstr "%i errors during open, check the debug log for details" diff --git a/locale/fr.po b/locale/fr.po new file mode 100644 index 0000000..ae230d4 --- /dev/null +++ b/locale/fr.po @@ -0,0 +1,944 @@ +# French translations for CHIRP package. +# Copyright (C) 2014 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2014-04-05 22:18+0100\n" +"Last-Translator: Matthieu Lapadu-Hargues \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 1.5.4\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Termine" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "Pret" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Recuperation des information de banque" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Donner un nom a la banque" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Banque" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Nom" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Mise a jour de l'index de banque pour la memoire {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Mise a jour des informations de banque pour la memoire {num}" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Lecture memoire {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Assigner son numero a la memoire {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Lecture de la banque pour la memoire {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Mem" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Frequence" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "Index" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Lecture de l'information de banque pour la memoire {num}" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Lecture information de banque" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Une erreur s'est produite" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Ecraser" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Le fichier existe" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Le fichier {name} existe deja. Voulez-vous l'ecraser ?" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "" +"L'emplacement {number} a deja ete importe. Choisissez une autre valeur pour " +"'nouvel emplacement' avant d'utiliser 'Importer'" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Valeur invalide. Doit etre un entier." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "L'emplacement {number} a deja ete importe" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Mise a jour de la liste URCALL" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Mise a jour de la liste RPTCALL" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Regler la memoire {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Importation de l'information de banque" + +#: ../chirpui/importdialog.py:264 +msgid "Error importing memories:" +msgstr "Erreur lors de l'importation des memoires :" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Tout" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Aucun" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Inverse" + +#: ../chirpui/importdialog.py:394 +msgid "Select" +msgstr "Selectionner" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Reverse" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Ajuster nouvel emplacement" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Confirmer l'ecrasement" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Options" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "Ne peut pas etre importe car" + +#: ../chirpui/importdialog.py:513 +msgid "Import From File" +msgstr "Importer depuis un fichier" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Importer" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "Vers" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "De" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Commentaire" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "L'emplacement memoire sera importe dans" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Emplacement memoire du fichier importe" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Preparation de la liste des memoires..." + +#: ../chirpui/importdialog.py:575 +msgid "Export To File" +msgstr "Exporter vers un fichier" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Exporter" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Sans titre" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Images de radio CHIRP" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "Fichiers CHIRP" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "Fichiers CSV" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "Fichiers ICF" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "Fichiers VX7 Commander" + +#: ../chirpui/mainapp.py:330 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"Les fichiers ICF ne peuvent pas etre edites, uniquement affiches ou importes " +"dans un autre fichier. Ouvrir en lecture seule ?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Une erreur s'est produite a l'ouverture de {fname} : {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} erreurs durant l'ouverture :" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Note :" + +#: ../chirpui/mainapp.py:395 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"Le {vendor} {model} fonctionne en live mode. Cela signifie que tous " +"les changements que vous faites sont immediatement envoyes a la radio. Pour " +"cette raison, vous ne pouvez pas utiliser les fonctions Enregistrer " +"ou Telecharger vers la radio. Si vous souhaitez editer le contenu " +"hors connexion, vous pouvez Exporter vers un fichier CSV, en " +"utilisant le menu Fichier." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Ne plus montrer" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "Fichier images {vendor} {model}" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Ouvrir le fichier recent {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Importer la base de donnees {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Ouvrir la base de donnees {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Annuler les changements ?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "" +"Le fichier a ete modifie, enregistrer les modifications avant de le fermer ?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "Avec les contributions significatives de :" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Selectionner les colonnes" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Colonnes visibles pour {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Rapport d'utilisation desactive" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"La fonction de rapport de CHIRP est concue pour ameliorer " +"l'application en permettant aux auteurs de se concentrer sur les pilotes des " +"radios les plus frequemment utilisees et les errreurs rencontrees par les " +"utilisateurs. Les rapports ne contiennent aucune donnee d'identification et " +"ne sont utilisee qu'a des fins statistiques par les auteurs. Votre vie " +"privee est extremement importante, mais considerez s'il vous plait que " +"laisser cette fonction activee contribuera a rendre CHIRP encore meilleur !\n" +"\n" +"Etes-vous certain de vouloir desactiver cette fonction ?" + +#: ../chirpui/mainapp.py:1045 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" +"Choisir une langue ou Auto pour utiliser celle du systeme par defaut. Vous " +"devrez redemarrer l'application pour appliquer le changement." + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Fichier" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Ouvrir base de donnees" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Recent" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Editer" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Couper" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "C_opier" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "Co_ller" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Supprimer" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "_Monter" + +#: ../chirpui/mainapp.py:1184 +msgid "Move Dow_n" +msgstr "Desce_ndre" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "_Permuter" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "_Voir" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Colonnes" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Developpeur" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Montrer les memoires brutes" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Comparaison memoires brutes" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Comparaison listes" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Changer la langue" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Radio" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Telecharger depuis la radio - Lire" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Telecharger vers la radio - Ecrire" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Importer de RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "Fichier CHIRP natif" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "Fichier CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Importer de RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +msgid "Import from stock config" +msgstr "Importer de la base de donnees" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Aide" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Rapporter statistiques" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Cacher les champs inutilises" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Decalage automatique de relais ARS" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Activer les fonctions de developpement" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Le rapport d'erreur est active" + +#: ../chirpui/mainapp.py:1355 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "Pour desactiver cette fonction, utilisez le menu Aide." + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Clonage en cours" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Clonage" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Annuler" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Decalage" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Deplacer {src} vers {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Recherche d'un emplacement libre ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "{count} memoires deplacees" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} sur {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Detecter" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Port" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Fabricant" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Modele" + +#: ../chirpui/clone.py:138 +msgid "Radio" +msgstr "Radio" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Impossible de detecter la radio sur {port}" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Erreur interne : impossible de telecharger vers {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Erreur de clonage: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Indicatif" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Votre indicatif" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Indicatif du relais" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Mon indicatif" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Telechargement de la liste URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Telechargement de la liste RPTCALL" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Telechargement de la liste MYCALL" + +#: ../chirpui/editorset.py:87 +msgid "Memories" +msgstr "Memoires" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Nom des banques" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Banques" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "Le {vendor} {model} a des sous-series multiples et independantes" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Selectionner importation depuis : " + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Annule" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Erreur interne" + +#: ../chirpui/editorset.py:248 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" +"Erreurs a l'ouverture de {file}. Les memoires ne peuvent pas etre " +"importees ! " + +#: ../chirpui/editorset.py:260 +msgid "There was an error during import: {error}" +msgstr "Une erreur s'est produite durant l'importation : {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Type de fichier non-supporte" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +msgid "There was an error during export: {error}" +msgstr "Une erreur s'est produite durant l'exportation : {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Memoire d'amorcage" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Valeur invalide pour ce champ" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Tone Mode" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Tone" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "ToneSql" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "Code DTCS" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Cross mode" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Decalage" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Mode" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Puissance" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Pas" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Ignorer" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Effacer la memoire {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Impossible de realiser des changements pour ce modele" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Modification nouvel element, valeurs par defaut" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Valeur invalide pour {col} : {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Erreur ecriture memoire" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Ecrire la memoire {number}" + +#: ../chirpui/memedit.py:361 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Cette operation necessite le deplacement de tous les canaux suivants jusqu'a " +"ce qu'un emplacement libre soit trouve. Cela peut prendre BEAUCOUP de temps. " +"Etes-vous certain de vouloir le faire ?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Ajouter la memoire {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Effacer la memoire {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Lecture memoire {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Deplacer la memoire de {old} vers {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Memoire brute {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Lecture memoire brute {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "Vous ne pouvez comparer que deux memoires !" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Memoire {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Comparaison entre {a} et {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Les memoires doivent etre contigues" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Inserer une ligne avant" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Inserer une ligne apres" + +#: ../chirpui/memedit.py:702 +msgid "Delete" +msgstr "Supprimer" + +#: ../chirpui/memedit.py:702 +msgid "Delete all" +msgstr "Tout supprimer" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Supprimer (et remonter)" + +#: ../chirpui/memedit.py:704 +msgid "Move up" +msgstr "Remonter" + +#: ../chirpui/memedit.py:705 +msgid "Move down" +msgstr "Descendre" + +#: ../chirpui/memedit.py:706 +msgid "Exchange memories" +msgstr "Permuter les memoires" + +#: ../chirpui/memedit.py:707 +msgid "Cut" +msgstr "Couper" + +#: ../chirpui/memedit.py:708 +msgid "Copy" +msgstr "Copier" + +#: ../chirpui/memedit.py:709 +msgid "Paste" +msgstr "Coller" + +#: ../chirpui/memedit.py:710 +msgid "Show Raw Memory" +msgstr "Afficher les donnees de memoires brutes" + +#: ../chirpui/memedit.py:711 +msgid "Diff Raw Memories" +msgstr "Comparaison memoires brutes" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Erreur interne : colonne {name} non trouvee" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Lecture canal {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Erreur interne : {number} limite invalide" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Etendue memoire :" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Aller" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Canaux speciaux" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Montrer vides" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Couper la memoire {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Ecraser ?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Ecraser l'emplacement {number} ?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Memoire incompatible" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "La memoire collee {number} n'est pas compatible avec cette radio car :" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Digital code" diff --git a/locale/hu.po b/locale/hu.po new file mode 100644 index 0000000..20f9a9b --- /dev/null +++ b/locale/hu.po @@ -0,0 +1,1216 @@ +# English translations for CHIRP package. +# Copyright (C) 2011 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-10-08 11:40-0700\n" +"PO-Revision-Date: 2015-01-28 13:47+0100\n" +"Last-Translator: Attila Joubert \n" +"Language-Team: English\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.7.4\n" +"Language: hu_HU\n" + +#: ../chirpui/bandplans.py:92 +msgid "" +"Band plans define default channel settings for frequencies in a region. " +"Choose a band plan or None for completely manual channel settings." +msgstr "" +"Egy régió sávtervének alapértelmezett csatorna frekvenciái. Válassz egy " +"sávtervet, vagy ki is hagyhatod a kézi csatorna beállítás befejezéséhez." + +#: ../chirpui/bankedit.py:53 +#, python-format +msgid "Retrieving %s information" +msgstr "%s információk lekérése" + +#: ../chirpui/bankedit.py:76 +#, python-format +msgid "Setting name on %s" +msgstr "A %s név beállítása" + +#: ../chirpui/bankedit.py:88 ../chirpui/bankedit.py:276 +#: ../chirpui/importdialog.py:594 ../chirpui/memdetail.py:257 +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:87 ../chirpui/memedit.py:338 +#: ../chirpui/memedit.py:1053 ../chirpui/memedit.py:1109 +#: ../chirpui/memedit.py:1245 ../chirpui/memedit.py:1247 +msgid "Name" +msgstr "Név" + +#: ../chirpui/bankedit.py:215 +msgid "Updating {type} index for memory {num}" +msgstr "A {num}. memória {type} index frissítése" + +#: ../chirpui/bankedit.py:225 +msgid "Updating mapping information for memory {num}" +msgstr "A {num}. memória információjának frissítése" + +#: ../chirpui/bankedit.py:231 ../chirpui/bankedit.py:262 +msgid "Getting memory {num}" +msgstr "A {num}. memória beolvasása" + +#: ../chirpui/bankedit.py:246 +msgid "Setting index for memory {num}" +msgstr "A {num}. memória sorszámának beállítása" + +#: ../chirpui/bankedit.py:255 +msgid "Getting {type} for memory {num}" +msgstr "A {num}. memória {type} beolvasása" + +#: ../chirpui/bankedit.py:274 ../chirpui/memedit.py:64 +#: ../chirpui/memedit.py:194 ../chirpui/memedit.py:337 +#: ../chirpui/memedit.py:412 ../chirpui/memedit.py:432 +#: ../chirpui/memedit.py:446 ../chirpui/memedit.py:469 +#: ../chirpui/memedit.py:517 ../chirpui/memedit.py:529 +#: ../chirpui/memedit.py:553 ../chirpui/memedit.py:555 +#: ../chirpui/memedit.py:628 ../chirpui/memedit.py:642 +#: ../chirpui/memedit.py:644 ../chirpui/memedit.py:685 +#: ../chirpui/memedit.py:687 ../chirpui/memedit.py:760 +#: ../chirpui/memedit.py:889 ../chirpui/memedit.py:1002 +#: ../chirpui/memedit.py:1051 ../chirpui/memedit.py:1077 +#: ../chirpui/memedit.py:1090 ../chirpui/memedit.py:1107 +#: ../chirpui/memedit.py:1421 +msgid "Loc" +msgstr "Hely" + +#: ../chirpui/bankedit.py:275 ../chirpui/importdialog.py:595 +#: ../chirpui/memdetail.py:256 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:88 ../chirpui/memedit.py:209 +#: ../chirpui/memedit.py:331 ../chirpui/memedit.py:339 +#: ../chirpui/memedit.py:368 ../chirpui/memedit.py:393 +#: ../chirpui/memedit.py:401 ../chirpui/memedit.py:1054 +#: ../chirpui/memedit.py:1106 +msgid "Frequency" +msgstr "Frekvencia" + +#: ../chirpui/bankedit.py:277 +msgid "Index" +msgstr "Sorszám" + +#: ../chirpui/bankedit.py:370 +msgid "Getting {type} information for memory {num}" +msgstr "A {num}. memória {type} adatainak beolvasása" + +#: ../chirpui/bankedit.py:392 +#, python-format +msgid "Getting %s information" +msgstr "%s információk beolvasása" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} a {port} porton" + +#: ../chirpui/clone.py:105 ../chirpui/clone.py:106 ../chirpui/clone.py:168 +msgid "Detect" +msgstr "Érzékelés" + +#: ../chirpui/clone.py:129 +msgid "Port" +msgstr "Port" + +#: ../chirpui/clone.py:130 +msgid "Vendor" +msgstr "Gyártó" + +#: ../chirpui/clone.py:131 +msgid "Model" +msgstr "Modell" + +#: ../chirpui/clone.py:144 +msgid "Radio" +msgstr "Rádió" + +#: ../chirpui/clone.py:172 +msgid "Unable to detect radio on {port}" +msgstr "Nem érzékelek a {port} porton!" + +#: ../chirpui/clone.py:184 +msgid "Internal error: Unable to upload to {model}" +msgstr "Belső hiba: nem tölthető fel a {model} rádióra." + +#: ../chirpui/clone.py:232 +msgid "Clone failed: {error}" +msgstr "A klónozás sikertelen: {error}" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Klónozási folyamat" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Klónozás" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Megszakítás" + +#: ../chirpui/common.py:240 +msgid "Completed" +msgstr "Befejeződött" + +#: ../chirpui/common.py:241 +msgid "idle" +msgstr "várakozik" + +#: ../chirpui/common.py:341 +msgid "Details" +msgstr "Részletek" + +#: ../chirpui/common.py:344 +msgid "Proceed?" +msgstr "Folytatod?" + +#: ../chirpui/common.py:353 +msgid "Do not show this next time" +msgstr "Mégegyszer ne mutassa ezt" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Hívójel" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Az Ön hívójele" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Az átjátszó hívójele" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Az én hívójelem" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1566 +msgid "Downloading URCALL list" +msgstr "Az URCALL lista letöltése" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1578 +msgid "Downloading RPTCALL list" +msgstr "Az RPTCALL lista letöltése" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "A MYCALL lista letöltése" + +#: ../chirpui/editorset.py:79 +#, python-format +msgid "Memories (%(variant)s)" +msgstr "Memória (%(variant)s)" + +#: ../chirpui/editorset.py:83 +msgid "Memories" +msgstr "Memória" + +#: ../chirpui/editorset.py:94 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:142 +msgid "Settings" +msgstr "Beállítás" + +#: ../chirpui/editorset.py:151 +msgid "Browser" +msgstr "Böngésző" + +#: ../chirpui/editorset.py:262 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "A {vendor} {model} több független altípussal rendelkezik" + +#: ../chirpui/editorset.py:265 +msgid "Choose one to import from:" +msgstr "Válasszon egy importálandót:" + +#: ../chirpui/editorset.py:270 +msgid "Cancelled" +msgstr "Mekszakítva" + +#: ../chirpui/editorset.py:275 +msgid "Internal Error" +msgstr "Belső hiba" + +#: ../chirpui/editorset.py:314 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" +"Hiba történt a {file} fájl megnyitásakor. Az érintett memóriák nem lesznek " +"importálhatók." + +#: ../chirpui/editorset.py:326 +msgid "There was an error during import: {error}" +msgstr "Hiba történt az importáláskor: {error}" + +#: ../chirpui/editorset.py:336 +msgid "Unsupported file type" +msgstr "Nem támogatott fájltípus" + +#: ../chirpui/editorset.py:352 ../chirpui/editorset.py:367 +msgid "There was an error during export: {error}" +msgstr "Hiba a(z) {fname} exportálásakor: {error}" + +#: ../chirpui/editorset.py:381 +msgid "Priming memory" +msgstr "Memória feltöltés" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "" +"A(z) {number}. hely már importálva van. 'Új hely'-nek válasszon másik " +"értéket, mielőtt az 'importálást' választja!" + +#: ../chirpui/importdialog.py:123 +msgid "Invalid value. Must be an integer." +msgstr "Érvénytelen érték! Egész szám kell legyen." + +#: ../chirpui/importdialog.py:132 +msgid "Location {number} is already being imported" +msgstr "A {number} sorszámú hely már importálva van." + +#: ../chirpui/importdialog.py:192 +msgid "Updating URCALL list" +msgstr "URCALL lista frissítése." + +#: ../chirpui/importdialog.py:197 +msgid "Updating RPTCALL list" +msgstr "RPTCALL lista frissítése" + +#: ../chirpui/importdialog.py:270 +msgid "Setting memory {number}" +msgstr "A {number} memória beállítása" + +#: ../chirpui/importdialog.py:274 +msgid "Importing bank information" +msgstr "Bank információk importálása" + +#: ../chirpui/importdialog.py:278 +msgid "Error importing memories:" +msgstr "Hiba a memória importálásakor:" + +#: ../chirpui/importdialog.py:390 +msgid "All" +msgstr "Mind" + +#: ../chirpui/importdialog.py:396 +msgid "None" +msgstr "Egyik sem" + +#: ../chirpui/importdialog.py:402 +msgid "Inverse" +msgstr "Ellenkező" + +#: ../chirpui/importdialog.py:408 +msgid "Select" +msgstr "Kiválasztás" + +#: ../chirpui/importdialog.py:454 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:460 +msgid "Reverse" +msgstr "Fordított" + +#: ../chirpui/importdialog.py:466 +msgid "Adjust New Location" +msgstr "Új hely megadása" + +#: ../chirpui/importdialog.py:476 +msgid "Confirm overwrites" +msgstr "Felülírás jóváhagyása" + +#: ../chirpui/importdialog.py:482 +msgid "Options" +msgstr "Beállítások" + +#: ../chirpui/importdialog.py:553 +msgid "Cannot be imported because" +msgstr "Nem importálható, mert" + +#: ../chirpui/importdialog.py:571 +msgid "Import From File" +msgstr "Importálás fájlból" + +#: ../chirpui/importdialog.py:572 ../chirpui/mainapp.py:1515 +msgid "Import" +msgstr "Importálás" + +#: ../chirpui/importdialog.py:592 +msgid "To" +msgstr "-ig" + +#: ../chirpui/importdialog.py:593 +msgid "From" +msgstr "Forrás" + +#: ../chirpui/importdialog.py:596 ../chirpui/memdetail.py:274 +#: ../chirpui/memedit.py:80 ../chirpui/memedit.py:102 +#: ../chirpui/memedit.py:1068 ../chirpui/memedit.py:1124 +#: ../chirpui/memedit.py:1250 +msgid "Comment" +msgstr "Megjegyzés" + +#: ../chirpui/importdialog.py:600 +msgid "Location memory will be imported into" +msgstr "Memória helye lesz beimportálva" + +#: ../chirpui/importdialog.py:601 +msgid "Location of memory in the file being imported" +msgstr "Memória hely a fájlba importálva" + +#: ../chirpui/importdialog.py:624 +msgid "Preparing memory list..." +msgstr "Memória lista előkészítés..." + +#: ../chirpui/importdialog.py:633 +msgid "Export To File" +msgstr "Export fájlba" + +#: ../chirpui/importdialog.py:634 ../chirpui/mainapp.py:1516 +msgid "Export" +msgstr "Export" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Hiba történt" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Felülírás" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "A fájl már létezik" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "A {name} nevű fájl már létezik. Felül akarja írni?" + +#: ../chirpui/mainapp.py:227 ../chirpui/mainapp.py:428 +msgid "Untitled" +msgstr "Névtelen" + +#: ../chirpui/mainapp.py:274 ../chirpui/mainapp.py:734 +msgid "CHIRP Radio Images" +msgstr "CHIRP Radio adatképek" + +#: ../chirpui/mainapp.py:275 ../chirpui/mainapp.py:733 +#: ../chirpui/mainapp.py:1115 +msgid "CHIRP Files" +msgstr "CHIRP fájlok" + +#: ../chirpui/mainapp.py:276 ../chirpui/mainapp.py:735 +#: ../chirpui/mainapp.py:1114 +msgid "CSV Files" +msgstr "CSV fájlok" + +#: ../chirpui/mainapp.py:277 ../chirpui/mainapp.py:736 +msgid "EVE Files (VX5)" +msgstr "EVE fájl (VX5)" + +#: ../chirpui/mainapp.py:278 ../chirpui/mainapp.py:737 +msgid "ICF Files" +msgstr "ICF fájlok" + +#: ../chirpui/mainapp.py:279 ../chirpui/mainapp.py:741 +msgid "VX5 Commander Files" +msgstr "VX5 Commander fájlok" + +#: ../chirpui/mainapp.py:280 ../chirpui/mainapp.py:742 +msgid "VX6 Commander Files" +msgstr "VX6 Commander fájlok" + +#: ../chirpui/mainapp.py:281 ../chirpui/mainapp.py:743 +msgid "VX7 Commander Files" +msgstr "VX7 Commander fájlok" + +#: ../chirpui/mainapp.py:291 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"Az ICF fájlok nem szerkeszthetők, csak megnézhetők, vagy másik fájlba " +"importálhatók. Megnyissam olvashatóként?" + +#: ../chirpui/mainapp.py:325 +msgid "There was an error opening {fname}: {error}" +msgstr "A(z) {fname} nevű fájl megnyitásakor fellépő hiba: {error}" + +#: ../chirpui/mainapp.py:337 +msgid "{num} errors during open:" +msgstr "{num} hiba a megnyitás során:" + +#: ../chirpui/mainapp.py:344 +msgid "Note:" +msgstr "Megjegyzés:" + +#: ../chirpui/mainapp.py:345 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"A {vendor} {model} közvetlen kapcsolatban működik. Ez azt jelenti, " +"hogy minden elvégzett módosítás azonnal a rádióra töltődik. Emiatt Ön nem " +"választhatja a Mentés vagy Feltöltés műveleteket. Ha Ön " +"kapcsolat nélkül akarja a tartalmat szerkeszteni, kérem válassza az CSV-be " +"exportálást a Fájl menüben!" + +#: ../chirpui/mainapp.py:354 +msgid "Don't show this again" +msgstr "Mégegyszer ne mutassa" + +#: ../chirpui/mainapp.py:388 +msgid "{vendor} {model} image file" +msgstr "{vendor} {model} képfájl" + +#: ../chirpui/mainapp.py:396 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:398 +msgid "VX6 Commander" +msgstr "VX6 Commander" + +#: ../chirpui/mainapp.py:400 +msgid "EVE" +msgstr "EVE" + +#: ../chirpui/mainapp.py:401 +msgid "VX5 Commander" +msgstr "VX5 Commander" + +#: ../chirpui/mainapp.py:467 +msgid "Open recent file {name}" +msgstr "A legutóbbi {name} fájl megnyitása" + +#: ../chirpui/mainapp.py:528 +msgid "Import stock configuration {name}" +msgstr "A(z) {name} konfigurációs készlet beolvasása" + +#: ../chirpui/mainapp.py:544 +msgid "Open stock configuration {name}" +msgstr "A(z) {name} konfigurációs készlet megnyitása" + +#: ../chirpui/mainapp.py:566 +msgid "Proceed with experimental driver?" +msgstr "Folytassam kísérleti driver-rel?" + +#: ../chirpui/mainapp.py:568 +msgid "This radio's driver is experimental. Do you want to proceed?" +msgstr "A rádió driver-e kísérleti. Folytatni akarod?" + +#: ../chirpui/mainapp.py:586 +msgid "{name} Instructions" +msgstr "{name} Utasítások" + +#: ../chirpui/mainapp.py:588 +msgid "{instructions}" +msgstr "{instructions}" + +#: ../chirpui/mainapp.py:591 +msgid "Don't show instructions for any radio again" +msgstr "Mégegyszer ne mutasson utasításokat egy rádióhoz sem" + +#: ../chirpui/mainapp.py:700 +msgid "Save Changes?" +msgstr "Menti a változásokat?" + +#: ../chirpui/mainapp.py:705 +msgid "File is modified, save changes before closing?" +msgstr "A fájl megváltozott! Menti bezárás előtt?" + +#: ../chirpui/mainapp.py:738 +msgid "Kenwood HMK Files" +msgstr "Kenwood HMK fájlok" + +#: ../chirpui/mainapp.py:739 +msgid "Kenwood ITM Files" +msgstr "Kenwood ITM fájlok" + +#: ../chirpui/mainapp.py:740 +msgid "Travel Plus Files" +msgstr "Travel Plus fájlok" + +#: ../chirpui/mainapp.py:805 +msgid "RepeaterBook Query" +msgstr "RepeaterBook lekérdezés" + +#: ../chirpui/mainapp.py:864 +msgid "RepeaterBook query failed" +msgstr "RepeaterBook lekérdezés hiba" + +#: ../chirpui/mainapp.py:934 +#, python-format +msgid "Invalid value for %s" +msgstr "Érvénytelen %s mezőérték" + +#: ../chirpui/mainapp.py:958 +msgid "Query failed" +msgstr "Lekérdezés hiba" + +#: ../chirpui/mainapp.py:1055 +msgid "RadioReference.com Query" +msgstr "RadioReference.com lekérdezés" + +#: ../chirpui/mainapp.py:1158 +msgid "With significant contributions from:" +msgstr "Jelentősen hozzájárultak:" + +#: ../chirpui/mainapp.py:1185 +msgid "CHIRP Documentation" +msgstr "CHIRP dokumentáció" + +#: ../chirpui/mainapp.py:1186 +msgid "" +"Documentation for CHIRP, including FAQs, and help for common problems is " +"available on the CHIRP web site, please go to\n" +"\n" +"http://chirp.danplanet.com/projects/chirp/wiki/Documentation\n" +msgstr "" +"A leggyakoribb problémákhoz, GYIK-et is magába foglaló CHIRP dokumentáció és " +"súgó a CHIRP honlapján érhető el, kérlek látogass oda!\n" +"\n" +"http://chirp.danplanet.com/projects/chirp/wiki/Documentation\n" + +#: ../chirpui/mainapp.py:1202 +msgid "Select Columns" +msgstr "Oszlopok kiválasztása" + +#: ../chirpui/mainapp.py:1217 +msgid "Visible columns for {radio}" +msgstr "A(z) {radio} látható oszlopai" + +#: ../chirpui/mainapp.py:1280 +msgid "Reporting is disabled" +msgstr "Listázás letiltva" + +#: ../chirpui/mainapp.py:1281 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"A CHIRP jelentéskészítője a minőség növelésére került beépítése " +"lehetővé téve a program fejlesztőinek, hogy a leggyakoribb rádió driver-ekre " +"és a felhasználók által tapasztalt hibákra figyeljenek. A jelentések " +"semmiféle személyes információt nem tartalmaznak és a programozók által, " +"kizárólag statisztikai céllal kerülnek felhasználásra. Az Ön adatainak " +"védelme rendkívül fontos, mi mégis kérjük, engedélyezze a jelentések " +"készítését, hogy a CHIRP egyre jobb legyen!\n" +"\n" +"Biztosan letiltja ezt a lehetőséget?" + +#: ../chirpui/mainapp.py:1315 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" +"Válasszon egy nyelvet, vagy bízza az automatikus nyelvfelismerést az " +"Operációs rendszerből. Ahhoz, hogy a beállítás érvényre jusson, indítsa újra " +"az CHIRP-et! " + +#: ../chirpui/mainapp.py:1328 +msgid "Python Modules" +msgstr "Python modulok" + +#: ../chirpui/mainapp.py:1487 +msgid "_File" +msgstr "_Fájl" + +#: ../chirpui/mainapp.py:1490 +msgid "Open stock config" +msgstr "A csoportos beállítás megnyitása" + +#: ../chirpui/mainapp.py:1491 +msgid "_Recent" +msgstr "Leg_utóbbi" + +#: ../chirpui/mainapp.py:1494 +msgid "Load Module" +msgstr "Modul betöltése" + +#: ../chirpui/mainapp.py:1497 +msgid "_Edit" +msgstr "Sz_erkesztés" + +#: ../chirpui/mainapp.py:1498 +msgid "_Cut" +msgstr "_Kivágás" + +#: ../chirpui/mainapp.py:1499 +msgid "_Copy" +msgstr "_Másolás" + +#: ../chirpui/mainapp.py:1500 +msgid "_Paste" +msgstr "_Beillesztés" + +#: ../chirpui/mainapp.py:1501 +msgid "_Delete" +msgstr "_Törlés" + +#: ../chirpui/mainapp.py:1502 +msgid "Move _Up" +msgstr "Fe_lfelé" + +#: ../chirpui/mainapp.py:1503 +msgid "Move Dow_n" +msgstr "Lefel_é" + +#: ../chirpui/mainapp.py:1504 +msgid "E_xchange" +msgstr "Cse_re" + +#: ../chirpui/mainapp.py:1505 +msgid "_View" +msgstr "Né_zet" + +#: ../chirpui/mainapp.py:1506 +msgid "Columns" +msgstr "Oszlopok" + +#: ../chirpui/mainapp.py:1507 +msgid "Developer" +msgstr "Fejlesztői" + +#: ../chirpui/mainapp.py:1508 +msgid "Show raw memory" +msgstr "memóriasor mutatása" + +#: ../chirpui/mainapp.py:1509 +msgid "Diff raw memories" +msgstr "Memóriasorok különbsége" + +#: ../chirpui/mainapp.py:1510 +msgid "Diff tabs" +msgstr "Diff tabs" + +#: ../chirpui/mainapp.py:1511 +msgid "Change language" +msgstr "Nyelv választás" + +#: ../chirpui/mainapp.py:1512 +msgid "_Radio" +msgstr "Rá_dió" + +#: ../chirpui/mainapp.py:1513 +msgid "Download From Radio" +msgstr "Letöltés a rádióról" + +#: ../chirpui/mainapp.py:1514 +msgid "Upload To Radio" +msgstr "Feltöltés a rádióra" + +#: ../chirpui/mainapp.py:1517 +msgid "Import from data source" +msgstr "Importálás adatforrásból" + +#: ../chirpui/mainapp.py:1518 ../chirpui/mainapp.py:1523 +msgid "RadioReference.com" +msgstr "RadioReference.com" + +#: ../chirpui/mainapp.py:1519 ../chirpui/mainapp.py:1524 +msgid "RFinder" +msgstr "RFinder" + +#: ../chirpui/mainapp.py:1520 ../chirpui/mainapp.py:1526 +msgid "RepeaterBook" +msgstr "RepeaterBook" + +#: ../chirpui/mainapp.py:1521 ../chirpui/mainapp.py:1525 +msgid "przemienniki.net" +msgstr "przemienniki.net" + +#: ../chirpui/mainapp.py:1522 +msgid "Query data source" +msgstr "Lekérd. adatforrása" + +#: ../chirpui/mainapp.py:1527 +msgid "CHIRP Native File" +msgstr "CHIRP natív fájl" + +#: ../chirpui/mainapp.py:1528 +msgid "CSV File" +msgstr "CSV fájl" + +#: ../chirpui/mainapp.py:1529 +msgid "Import from stock config" +msgstr "Importálás a csoportos beállításokból" + +#: ../chirpui/mainapp.py:1530 +msgid "Channel defaults" +msgstr "Csatorna alapértékek" + +#: ../chirpui/mainapp.py:1532 +msgid "Help" +msgstr "Súgó" + +#: ../chirpui/mainapp.py:1534 +msgid "Documentation" +msgstr "Dokumentáció" + +#: ../chirpui/mainapp.py:1544 +msgid "Report statistics" +msgstr "Statisztikai lista" + +#: ../chirpui/mainapp.py:1545 +msgid "Hide Unused Fields" +msgstr "A nemhasznált mezők elrejtése" + +#: ../chirpui/mainapp.py:1546 +msgid "Smart Tone Modes" +msgstr "Hang (CTCSS) mód" + +#: ../chirpui/mainapp.py:1547 +msgid "Enable Developer Functions" +msgstr "Fejlesztői funkciók engedélyezése" + +#: ../chirpui/mainapp.py:1657 +msgid "A new version of CHIRP is available: " +msgstr "A CHIRP egy új verziója érhető el:" + +#: ../chirpui/mainapp.py:1756 +msgid "Error reporting is enabled" +msgstr "A hibajelentés engedélyezett" + +#: ../chirpui/mainapp.py:1759 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "Ha Ön ezt a lehetőséget le kívánja tiltani, a Súgóban megteheti" + +#: ../chirpui/memdetail.py:239 +msgid "Edit Memory #{num}" +msgstr "A(z) #{num} memória" + +#: ../chirpui/memdetail.py:258 ../chirpui/memedit.py:67 +#: ../chirpui/memedit.py:100 ../chirpui/memedit.py:115 +#: ../chirpui/memedit.py:230 ../chirpui/memedit.py:236 +#: ../chirpui/memedit.py:269 ../chirpui/memedit.py:409 +#: ../chirpui/memedit.py:1055 ../chirpui/memedit.py:1115 +#: ../chirpui/memedit.py:1251 ../chirpui/memedit.py:1311 +msgid "Tone Mode" +msgstr "Hang (CTCSS) mód" + +#: ../chirpui/memdetail.py:259 ../chirpui/memedit.py:68 +#: ../chirpui/memedit.py:89 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:165 ../chirpui/memedit.py:166 +#: ../chirpui/memedit.py:254 ../chirpui/memedit.py:284 +#: ../chirpui/memedit.py:291 ../chirpui/memedit.py:296 +#: ../chirpui/memedit.py:304 ../chirpui/memedit.py:342 +#: ../chirpui/memedit.py:407 ../chirpui/memedit.py:1056 +#: ../chirpui/memedit.py:1111 ../chirpui/memedit.py:1252 +msgid "Tone" +msgstr "CTCSS" + +#: ../chirpui/memdetail.py:260 ../chirpui/memedit.py:69 +#: ../chirpui/memedit.py:90 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:258 ../chirpui/memedit.py:277 +#: ../chirpui/memedit.py:292 ../chirpui/memedit.py:297 +#: ../chirpui/memedit.py:308 ../chirpui/memedit.py:343 +#: ../chirpui/memedit.py:407 ../chirpui/memedit.py:1057 +#: ../chirpui/memedit.py:1112 ../chirpui/memedit.py:1248 +msgid "ToneSql" +msgstr "ToneSql" + +#: ../chirpui/memdetail.py:261 ../chirpui/memedit.py:70 +#: ../chirpui/memedit.py:91 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:242 ../chirpui/memedit.py:278 +#: ../chirpui/memedit.py:286 ../chirpui/memedit.py:298 +#: ../chirpui/memedit.py:306 ../chirpui/memedit.py:344 +#: ../chirpui/memedit.py:403 ../chirpui/memedit.py:1058 +#: ../chirpui/memedit.py:1113 ../chirpui/memedit.py:1240 +#: ../chirpui/memedit.py:1317 +msgid "DTCS Code" +msgstr "DTCS kód" + +#: ../chirpui/memdetail.py:263 ../chirpui/memedit.py:72 +#: ../chirpui/memedit.py:93 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:280 +#: ../chirpui/memedit.py:288 ../chirpui/memedit.py:300 +#: ../chirpui/memedit.py:312 ../chirpui/memedit.py:346 +#: ../chirpui/memedit.py:1060 ../chirpui/memedit.py:1117 +#: ../chirpui/memedit.py:1242 ../chirpui/memedit.py:1316 +msgid "DTCS Pol" +msgstr "DTCS pol." + +#: ../chirpui/memdetail.py:264 +msgid "Cross mode" +msgstr "Kereszt-üzem" + +#: ../chirpui/memdetail.py:267 ../chirpui/memedit.py:74 +#: ../chirpui/memedit.py:95 ../chirpui/memedit.py:113 +#: ../chirpui/memedit.py:139 ../chirpui/memedit.py:146 +#: ../chirpui/memedit.py:270 ../chirpui/memedit.py:340 +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:1062 +#: ../chirpui/memedit.py:1118 ../chirpui/memedit.py:1253 +#: ../chirpui/memedit.py:1323 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memdetail.py:268 ../chirpui/memedit.py:75 +#: ../chirpui/memedit.py:96 ../chirpui/memedit.py:144 +#: ../chirpui/memedit.py:217 ../chirpui/memedit.py:315 +#: ../chirpui/memedit.py:341 ../chirpui/memedit.py:405 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1119 +#: ../chirpui/memedit.py:1244 +msgid "Offset" +msgstr "Offszet" + +#: ../chirpui/memdetail.py:269 ../chirpui/memedit.py:76 +#: ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:158 ../chirpui/memedit.py:159 +#: ../chirpui/memedit.py:162 ../chirpui/memedit.py:393 +#: ../chirpui/memedit.py:1064 ../chirpui/memedit.py:1120 +#: ../chirpui/memedit.py:1243 ../chirpui/memedit.py:1310 +#: ../chirpui/memedit.py:1325 ../chirpui/memedit.py:1326 +#: ../chirpui/memedit.py:1490 ../chirpui/memedit.py:1508 +#: ../chirpui/memedit.py:1518 +msgid "Mode" +msgstr "Mód" + +#: ../chirpui/memdetail.py:270 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:114 +#: ../chirpui/memedit.py:149 ../chirpui/memedit.py:150 +#: ../chirpui/memedit.py:155 ../chirpui/memedit.py:1066 +#: ../chirpui/memedit.py:1122 ../chirpui/memedit.py:1246 +msgid "Tune Step" +msgstr "Lépésköz" + +#: ../chirpui/memdetail.py:273 ../chirpui/memedit.py:79 +#: ../chirpui/memedit.py:101 ../chirpui/memedit.py:1067 +#: ../chirpui/memedit.py:1123 ../chirpui/memedit.py:1254 +#: ../chirpui/memedit.py:1313 +msgid "Skip" +msgstr "Ugrás" + +#: ../chirpui/memdetail.py:281 +msgid "RX DTCS Code" +msgstr "RX DTCS kód" + +#: ../chirpui/memdetail.py:287 ../chirpui/memedit.py:77 +#: ../chirpui/memedit.py:98 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:393 ../chirpui/memedit.py:1065 +#: ../chirpui/memedit.py:1121 ../chirpui/memedit.py:1255 +#: ../chirpui/memedit.py:1314 ../chirpui/memedit.py:1321 +msgid "Power" +msgstr "Teljesítmény" + +#: ../chirpui/memdetail.py:330 +msgid "Memory validation failed:" +msgstr "Memória érvényesítési hiba:" + +#: ../chirpui/memdetail.py:341 +msgid "Edit Multiple Memories" +msgstr "Több memória szerkesztése" + +#: ../chirpui/memdetail.py:363 +msgid "Check this to change the {name} value" +msgstr "Ennek változtatásához ellenőrizd a {name} értékét" + +#: ../chirpui/memedit.py:53 +msgid "Invalid value for this field" +msgstr "Érvénytelen mezőérték" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:246 ../chirpui/memedit.py:279 +#: ../chirpui/memedit.py:287 ../chirpui/memedit.py:294 +#: ../chirpui/memedit.py:299 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:345 ../chirpui/memedit.py:403 +#: ../chirpui/memedit.py:1059 ../chirpui/memedit.py:1114 +#: ../chirpui/memedit.py:1241 ../chirpui/memedit.py:1318 +msgid "DTCS Rx Code" +msgstr "DTCS RX kód" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:94 ../chirpui/memedit.py:116 +#: ../chirpui/memedit.py:231 ../chirpui/memedit.py:240 +#: ../chirpui/memedit.py:262 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:281 ../chirpui/memedit.py:289 +#: ../chirpui/memedit.py:293 ../chirpui/memedit.py:301 +#: ../chirpui/memedit.py:347 ../chirpui/memedit.py:1061 +#: ../chirpui/memedit.py:1116 ../chirpui/memedit.py:1249 +#: ../chirpui/memedit.py:1312 +msgid "Cross Mode" +msgstr "Kereszt-üzem" + +#: ../chirpui/memedit.py:197 +msgid "Erasing memory {loc}" +msgstr "A(z) {loc}. memóriahely törlése" + +#: ../chirpui/memedit.py:326 +msgid "Unable to make changes to this model" +msgstr "Ennél a modellnél nem változtatható" + +#: ../chirpui/memedit.py:332 +msgid "Editing new item, taking defaults" +msgstr "Új elem szerkesztése az alapértelmezettek szerint" + +#: ../chirpui/memedit.py:354 +msgid "Bad value for {col}: {val}" +msgstr "Hibás érték {col}: {val}" + +#: ../chirpui/memedit.py:378 +msgid "Error setting memory" +msgstr "Memória beállítási hiba" + +#: ../chirpui/memedit.py:386 ../chirpui/memedit.py:453 +#: ../chirpui/memedit.py:728 ../chirpui/memedit.py:750 +#: ../chirpui/memedit.py:1471 +msgid "Writing memory {number}" +msgstr "A(z) {number}. memória írása" + +#: ../chirpui/memedit.py:458 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Ez a művelet megköveteli, hogy minden csatorna egyetlen összefüggő, üres " +"területre kerüljön. Ez sokáig eltarthat. Biztos, hogy elkezdjem?" + +#: ../chirpui/memedit.py:481 +msgid "Adding memory {number}" +msgstr "A(z) {number}. memória hozzáadása" + +#: ../chirpui/memedit.py:494 ../chirpui/memedit.py:1096 +msgid "Erasing memory {number}" +msgstr "A(z) {number}. memória törlése" + +#: ../chirpui/memedit.py:503 ../chirpui/memedit.py:612 +#: ../chirpui/memedit.py:658 ../chirpui/memedit.py:663 +#: ../chirpui/memedit.py:1037 ../chirpui/memedit.py:1345 +msgid "Getting memory {number}" +msgstr "A(z) {number}. memória beolvasása" + +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:602 +#: ../chirpui/memedit.py:650 +msgid "Moving memory from {old} to {new}" +msgstr "Memória átmozgatás {old} -> {new}" + +#: ../chirpui/memedit.py:672 +msgid "Raw memory {number}" +msgstr "{number}. memóriasor" + +#: ../chirpui/memedit.py:676 ../chirpui/memedit.py:704 +#: ../chirpui/memedit.py:709 +msgid "Getting raw memory {number}" +msgstr "A(z) {number}. memóriasor kiolvasása" + +#: ../chirpui/memedit.py:681 +msgid "You can only diff two memories!" +msgstr "Csak két memória hasonlítható össze!" + +#: ../chirpui/memedit.py:692 +msgid "Memory {number}" +msgstr "A(z){number} memória" + +#: ../chirpui/memedit.py:698 +msgid "Diff of {a} and {b}" +msgstr "{a} és {b} összehasonlítása" + +#: ../chirpui/memedit.py:732 +msgid "Getting original memory {number}" +msgstr "A(z) eredeti {number}. memóriasor" + +#: ../chirpui/memedit.py:769 +msgid "Memories must be contiguous" +msgstr "Egybefüggő memóriaterület szükséges" + +#: ../chirpui/memedit.py:856 +msgid "Edit" +msgstr "Sz_erkesztés" + +#: ../chirpui/memedit.py:857 +msgid "Insert row above" +msgstr "Memória beszúrása fölé" + +#: ../chirpui/memedit.py:858 +msgid "Insert row below" +msgstr "Memória beszúrása alá" + +#: ../chirpui/memedit.py:859 +msgid "Delete" +msgstr "Törlés" + +#: ../chirpui/memedit.py:860 +msgid "this memory" +msgstr "ez a memória" + +#: ../chirpui/memedit.py:860 +msgid "these memories" +msgstr "ezek a memóriák" + +#: ../chirpui/memedit.py:861 +msgid "...and shift block up" +msgstr "... és a tömböt felfelé lépteti" + +#: ../chirpui/memedit.py:862 +msgid "...and shift all memories up" +msgstr "...és minden memória felfelé lép" + +#: ../chirpui/memedit.py:863 +msgid "Move up" +msgstr "Mozgatás felfelé" + +#: ../chirpui/memedit.py:864 +msgid "Move down" +msgstr "Mozgatás lefelé" + +#: ../chirpui/memedit.py:865 +msgid "Exchange memories" +msgstr "Memóriák cseréje" + +#: ../chirpui/memedit.py:866 +msgid "Cut" +msgstr "Kivágás" + +#: ../chirpui/memedit.py:867 +msgid "Copy" +msgstr "Másolás" + +#: ../chirpui/memedit.py:868 +msgid "Paste" +msgstr "Beillesztés" + +#: ../chirpui/memedit.py:869 +msgid "Show Raw Memory" +msgstr "Memóriasor mutatása" + +#: ../chirpui/memedit.py:870 +msgid "Diff Raw Memories" +msgstr "Memóriasorok összehasonlítása" + +#: ../chirpui/memedit.py:1015 +msgid "Internal Error: Column {name} not found" +msgstr "Belső hiba: nincs {name} nevű oszlop" + +#: ../chirpui/memedit.py:1044 +msgid "Getting channel {chan}" +msgstr "A(z) {chan}. csatorna kiolvasása" + +#: ../chirpui/memedit.py:1136 +msgid "Internal Error: Invalid limit {number}" +msgstr "Belső hiba: Érvénytelen határ {number}" + +#: ../chirpui/memedit.py:1146 +msgid "Memory range:" +msgstr "Memória tartomány:" + +#: ../chirpui/memedit.py:1173 +msgid "Go" +msgstr "Ok" + +#: ../chirpui/memedit.py:1196 +msgid "Special Channels" +msgstr "Spec. csatornák" + +#: ../chirpui/memedit.py:1203 +msgid "Show Empty" +msgstr "Üreset is" + +#: ../chirpui/memedit.py:1378 +msgid "Cutting memory {number}" +msgstr "A(z) {number}. memória kivágása" + +#: ../chirpui/memedit.py:1409 +msgid "" +"Unable to paste {src} memories into {dst} rows. Increase the memory bounds " +"or show empty memories." +msgstr "" +"Nem sikerült beilleszteni {src} memóriát {dst} sorokba. Növelje a memória " +"határokat, vagy mutasson üres memóriára!" + +#: ../chirpui/memedit.py:1423 +msgid "Overwrite?" +msgstr "Felülírja?" + +#: ../chirpui/memedit.py:1428 +msgid "Overwrite location {number}?" +msgstr "Felülírja a(z) {number}. helyet?" + +#: ../chirpui/memedit.py:1453 +msgid "Incompatible Memory" +msgstr "Nem kompatibilis memória" + +#: ../chirpui/memedit.py:1456 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "" +"A beillesztett {number}. számú memória nem kompatibilis ezzel a rádióval, " +"mert:" + +#: ../chirpui/memedit.py:1510 ../chirpui/memedit.py:1525 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1510 ../chirpui/memedit.py:1526 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1510 ../chirpui/memedit.py:1527 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1511 ../chirpui/memedit.py:1528 +msgid "Digital Code" +msgstr "Digitális kód" + +#: ../chirpui/settingsedit.py:67 +#, python-format +msgid "Error in setting value: %s" +msgstr "Hiba a(z) %s érték beállításakor" + +#: ../chirpui/settingsedit.py:124 +#, python-format +msgid "Invalid setting value: %s" +msgstr "Érvénytelen érték %s" + +#: ../chirpui/settingsedit.py:164 +msgid "Enabled" +msgstr "Engedélyezve" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Eltolás" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Mozgatás {src} -> {dst}" + +#: ../chirpui/shiftdialog.py:83 +msgid "Looking for a free spot ({number})" +msgstr "Üres terület keresése ({number})" + +#: ../chirpui/shiftdialog.py:96 +msgid "No space to insert a row" +msgstr "Nincs hely a sor beszúrásához" + +#: ../chirpui/shiftdialog.py:143 +msgid "Moved {count} memories" +msgstr "{count} memória átmozgatva" + +msgid "Automatic Repeater Offset" +msgstr "Automatic Repeater Offset" + +msgid "Delete all" +msgstr "_Törlés" + +msgid "%i errors during open, check the debug log for details" +msgstr "%i errors during open, check the debug log for details" diff --git a/locale/it.po b/locale/it.po new file mode 100644 index 0000000..fb2c688 --- /dev/null +++ b/locale/it.po @@ -0,0 +1,944 @@ +# English translations for CHIRP package. +# Copyright (C) 2011 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2014-11-08 17:58+0100\n" +"Last-Translator: Dan Smith \n" +"Language-Team: English\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ASCII\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Completato" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "pronto" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Recupero informazioni bank" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Scrittura nome del bank" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Bank" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Nome" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Aggiornamento indice bank per la memoria {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Aggiornamento informazioni bank per la memoria {num}" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Acquisizione memoria {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Scrittura indice per la memoria {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Acquisizione bank per la memoria {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Pos" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Frequenza" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "Indice" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Acquisizione informazioni bank per la memoria {num}" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Acquisizione informazioni bank" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Si e' verificato un errore" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Sovrascrivi" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Il File esiste" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Il file {name} esiste gia. Vuoi sovrascriverlo?" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "" +"La memoria numero {number} e' gia' stata importata. Scegliere un altro " +"valore per 'Nuova memoria' prima di selezionare 'Importa'" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Valore non valido. Deve essere un numero intero" + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "La memoria {number} e' gia' stata importata" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Aggiornamento lista URCALL" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Aggiornamento lista RPTCALL" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Scrittura memoria {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Importazione informazioni bank" + +#: ../chirpui/importdialog.py:264 +msgid "Error importing memories:" +msgstr "Errore di importazione memorie:" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Tutto" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Nessuno" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Inverti" + +#: ../chirpui/importdialog.py:394 +msgid "Select" +msgstr "Seleziona" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Rovescia" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Cambia nuova posizione" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Conferma sovrascrittura" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Opzioni" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "Non si puo' importare perche'" + +#: ../chirpui/importdialog.py:513 +msgid "Import From File" +msgstr "Importa da File" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Import" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "A" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "Da" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Commento" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "La memoria verra' importata in" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Posizione della memoria nel file che viene importato" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Preparazione lista memorie..." + +#: ../chirpui/importdialog.py:575 +msgid "Export To File" +msgstr "Esporta in File" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Esporta" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Senza Nome" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Immagine Radio CHIRP" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "Files CHIRP" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "Files CSV" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "Files ICF" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "Files VX7 Commander" + +#: ../chirpui/mainapp.py:330 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"I Files ICF non possono essere modificati, solo visualizzati o importati in " +"un altro file. Aprire in sola lettura?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Errore durante l'apertura di {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} errori durante l'apertura:" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Note:" + +#: ../chirpui/mainapp.py:395 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"La radio{vendor} {model} opera in live mode. Questo significa che " +"qualsiasi modifica viene immediatamente scritta nella radio. A causa di " +"questo, non puoi utilizzare le funzioni Salva or Scrivi. Se " +"vuoi modificare le memorie offline, per favore Esporta le memorie in " +"un file CSV, usando il menu File." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Non mostrarlo di nuovo" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "File immagine {vendor} {model}" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Apri file recente {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Importazione configurazione stock {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Apertura configurazione stock {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Scartare le modifiche?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "Il file e' stato modificato, salvare i cambiamenti prima di chiudere?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "Con il significativo contributo di:" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Seleziona colonne" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Colonne visibili per {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Reporting disattivato" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"La funzione reporting di CHIRP e' fatta per migliorare la qualita' " +"consentendo agli autori di concentrarsi sulle radio piu' utilizzate e sugli " +"errori piu' comuni. I report non contengono informazioni personali e sono " +"utilizzati per soli fini statistici dagli autori. La tua provacy e' molto " +"importante ma per favore considera di lasciare attiva questa opzione per " +"aiutarci a migliorare CHIRP!\n" +"\n" +"Sei sicuro di disabilitare questa opzione?" + +#: ../chirpui/mainapp.py:1045 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" +"Scegli una lingua o usa Auto per utilizzare la lingua del sistema operativo. " +"Devi riavviare l'applicazione per applicare i cambiamenti" + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "File" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Apri configurazione stock" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "Recenti" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "Modifica" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "Taglia" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "Copia" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "Incolla" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "Elimina" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Muovi Su" + +#: ../chirpui/mainapp.py:1184 +msgid "Move Dow_n" +msgstr "Muovi Giu" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "Scambia" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "Vista" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Colonne" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Sviluppatore" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Mostra memorie Raw" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Differenza memorie Raw" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Differenzia tabs" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Cambia lingua" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "Radio" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Leggi da Radio" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Scrivi su Radio" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Importa da RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "File nativo CHIRP" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "File CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Importa da RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +msgid "Import from stock config" +msgstr "Importa da Configurazione Stock" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Aiuto" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Report statistiche" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Nascondi campi inutilizzati" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Offset Ripetitori Automatico" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Abilita Funzioni Sviluppatore" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Rapporto errori abilitato" + +#: ../chirpui/mainapp.py:1355 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "Se vuoi disabilitare questa opzione puoi farlo nel menu Aiuto" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Progressione programmazione" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Programmazione" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Annulla" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Shift" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Spostamento da {src} a {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Ricerca di un posto libero ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "Spostate {count} memorie" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} su porta {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Rileva" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Porta" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Produttore" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Modello" + +#: ../chirpui/clone.py:138 +msgid "Radio" +msgstr "Radio" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Impossibile rilevare radio sulla porta {port}" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Errore interno: impossibile caricare su {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Programmazione fallita: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Nominativo" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Il tuo nominativo" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Nominativo Ripetitore" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Il mio nominativo" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Scaricamento lista URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Scaricamento lista RPTCALL" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Scaricamento lista MYCALL" + +#: ../chirpui/editorset.py:87 +msgid "Memories" +msgstr "Memorie" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Nomi Bank" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Bank" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "Il {vendor} {model} ha sub-device multipli indipendenti" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Scegli da dove importare:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Annullato" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Errore interno" + +#: ../chirpui/editorset.py:248 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" +"Ci sono stati degli errori nell'apertura del file {file}. Le memorie " +"interessate non potranno essere importate!" + +#: ../chirpui/editorset.py:260 +msgid "There was an error during import: {error}" +msgstr "Errore durante l'apertura del file: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Tipo file non supportato" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +msgid "There was an error during export: {error}" +msgstr "Errore durante l'esportazione del file: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Preparazione memoria" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Valore non valido per questo campo" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Modalita' tono" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Tone" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "ToneSql" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "DTCS Code" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Cross Mode" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Offset" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Modulazione" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Potenza" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Step sintonia" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Salta" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Cancellazione memoria {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Impossibile effettuare modifiche su questo modello" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Modifica di un nuovo elemento, verranno usati valori di default" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Valore non valido per {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Errore di scrittura della memoria" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Scrittura memoria {number}" + +#: ../chirpui/memedit.py:361 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Questa operazione richiede lo spostamento dei canali seguenti di un passo " +"alla volta fino alla nuova posizione. L'operazione puo' durare MOLTO tempo. " +"Sei sicuro di volerlo fare?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Aggiunta memoria {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Cancellatura memoria {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Acquisita memoria {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Spostata memoria da {old} a {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Memoria Raw {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Acquisizione memoria Raw {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "Puoi differenziare solo 2 memorie!" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Memoria {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Differenza di {a} e {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Le memorie devono essere contigue" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Inserisci riga sopra" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Inserisci riga sotto" + +#: ../chirpui/memedit.py:702 +msgid "Delete" +msgstr "Elimina" + +#: ../chirpui/memedit.py:702 +msgid "Delete all" +msgstr "Elimina tutto" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Elimina (e sposta sopra)" + +#: ../chirpui/memedit.py:704 +msgid "Move up" +msgstr "Sposta su" + +#: ../chirpui/memedit.py:705 +msgid "Move down" +msgstr "Muovi giu" + +#: ../chirpui/memedit.py:706 +msgid "Exchange memories" +msgstr "Scambia memorie" + +#: ../chirpui/memedit.py:707 +msgid "Cut" +msgstr "Taglia" + +#: ../chirpui/memedit.py:708 +msgid "Copy" +msgstr "Copia" + +#: ../chirpui/memedit.py:709 +msgid "Paste" +msgstr "Incolla" + +#: ../chirpui/memedit.py:710 +msgid "Show Raw Memory" +msgstr "Mostra memorie Raw" + +#: ../chirpui/memedit.py:711 +msgid "Diff Raw Memories" +msgstr "Diff memorie Raw" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Errore interno: nome colonna {name} non trovato" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Acquisizione canale {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Errore interno: limite non valido {number}" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Range memorie" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Vai" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Canali Speciali" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Mostra Vuoti" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Taglia memoria {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Sovrascrivere?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Sovrascrivere posizione {number}?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Memoria non compatibile" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "" +"La memoria incollata {number} non e' compatibile con questa radio perche':" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Digital Code" + +#~ msgid "%i errors during open, check the debug log for details" +#~ msgstr "%i errors during open, check the debug log for details" diff --git a/locale/nl.po b/locale/nl.po new file mode 100644 index 0000000..e22e916 --- /dev/null +++ b/locale/nl.po @@ -0,0 +1,945 @@ +# Dutch translations for CHIRP package. +# Copyright (C) 2012 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2012. +# Michael Tel , 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2012-03-18 12:01+0100\n" +"Last-Translator: Michael Tel \n" +"Language-Team: Dutch\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ISO-8859-1\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Volbracht" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "luieren" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Informatie van de bank ophalen" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Naam van de bank instellen" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Bank" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Naam" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Index van de bank voor kanaal {num} bijwerken" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Informatie van de bank voor kanaal {num} bijwerken" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Ophalen kanaal {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Instellen van de index voor kanaal {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Bank voor kanaal {num} ophalen" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Plaats" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Frequentie" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "Index" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Informatie van de bank voor kanaal {num} ophalen" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Informatie van de bank ophalen" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Er is een fout opgetreden" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Overschrijven" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Bestand bestaat" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Het bestand {name} bestaat al. Wilt u het overschrijven?" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "" +"Plaats {number} wordt al gemporteerd. Kies een andere waarde voor 'Nieuwe " +"locatie' vr 'Importeren' te selecteren" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Ongeldige waarde. Het moet een integer zijn." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "Plaats {number} is reeds gemporteerd" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Bijwerken URCALL lijst" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Bijwerken RPTCALL lijst" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Kanaal {number} instellen" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Importeren van informatie van de bank" + +#: ../chirpui/importdialog.py:264 +msgid "Error importing memories:" +msgstr "Fout bij het importen van geheugen" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Alles" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Geen" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Tegengesteld" + +#: ../chirpui/importdialog.py:394 +msgid "Select" +msgstr "Selecteren" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Omkeren" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Nieuwe locatie aanpassen" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Overschrijven bevestigen" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Opties" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "Kan niet worden gemporteerd, omdat" + +#: ../chirpui/importdialog.py:513 +msgid "Import From File" +msgstr "Importeren van bestand" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Importeren" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "Aan" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "Van" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Opmerking" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "Geheugenplaatsen zullen ook gemporteerd worden naar" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Plaats van het kanaal in het bestand wordt gemporteerd" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Lijst van het kanaal wordt voorbereid..." + +#: ../chirpui/importdialog.py:575 +msgid "Export To File" +msgstr "Exporteren naar bestand" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Exporteren" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Naamloos" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "CHIRP radio afbeeldingen" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "CHIRP bestanden" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "Komma gescheiden bestanden" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "ICF bestanden" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "VX7 Commander bestanden" + +#: ../chirpui/mainapp.py:330 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"ICF bestanden kunnen niet worden bewerkt, alleen worden weergegeven of in " +"een ander bestand gemporteerd. Openen in de modus alleen-lezen?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Er is een fout opgetreden bij het openen van {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} fouten tijdens het openen:" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Noot:" + +#: ../chirpui/mainapp.py:395 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"De {vendor} {model} opereert in de live modus. Dit betekent dat " +"wijzigingen die u aanbrengt onmiddellijk naar de radio verzonden worden. " +"Hierdoor kunt u het niet Opslaan of uploaden . Als u wenst de " +"inhoud off line te bewerken, gelieve exporteren naar een CSV-bestand, " +"met behulp van het menu bestand." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Deze melding niet opnieuw weergeven" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "{vendor} {model} afbeeldingsbestand" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Recent geopend bestand {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Importeren van aanwezige configuratie {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Openen van aanwezige configuratie {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Wijzigen verwerpen?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "Het bestand is aangepast. Wilt u de wijzigingen opslaan?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "Met dank aan:" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Kolommen selecteren" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Zichtbare kolommen voor {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Rapportering is uitgeschakeld" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"De rapportagefunctie van CHIRP is ontworpen om de kwaliteit te helpen " +"verbeteren door de auteurs toe te staan zich te concentreren op de radio " +"stuurprogramma's die het meest gebruikt worden en fouten ervaren door de " +"gebruikers. De verslagen bevatten geen identificerende informatie en worden " +"alleen voor statistische doeleinden gebruikt door de auteurs. Uw privacy is " +"uiterst belangrijk, maar kunt u overwegen deze functie ingeschakeld te " +"laten om zo te helpen CHIRP beter maken!\n" +"\n" +"Weet u zeker dat u deze functie wilt uitschakelen?" + +#: ../chirpui/mainapp.py:1045 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" +"Kies een taal of Auto voor het gebruik van het standaard besturingssysteem. " +"U dient de toepassing opnieuw te starten voordat de wijziging wordt " +"doorgevoerd" + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Bestanden" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Openen aanwezige configuratie" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Recent" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "Be_werken" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Knippen" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Kopiren" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Plakken" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Verwijderen" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Verplaats om_hoog" + +#: ../chirpui/mainapp.py:1184 +msgid "Move Dow_n" +msgstr "Verplaats omlaa_g" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "W_isselen" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "Bee_ld" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Kolommen" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Ontwerper" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Toon ruw geheugen" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Verschil ruwe kanalen" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Verschillen van tabs" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Taal wijzigen" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Radio" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Download van radio" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Naar radio uploaden" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Importeren uit RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "CHIRP oorspronkelijk bestand" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "Komma gescheiden bestand" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Importeren van RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +msgid "Import from stock config" +msgstr "Importeren van aanwezige configuratie" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Help" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Statistieken raporteren" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Ongebruikte velden verbergen" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Automatische repeater offset" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Ontwikkelaars functies inschakelen" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Foutrapportage is aangezet" + +#: ../chirpui/mainapp.py:1355 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "" +"Als u deze functie wilt uitschakelen, dan kunt u dit doen in het menu " +"Help" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Vooruitgang van het klonen" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Klonen" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Annuleren" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Shift" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Verplaatsen van {src} naar {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Zoeken naar een vrije plaats ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "{count} kanalen verplaatst" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} op {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Detecteren" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Poort" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Merk" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Model" + +#: ../chirpui/clone.py:138 +msgid "Radio" +msgstr "Radio" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Niet in staat om de radio op {port} te detecteren" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Interne fout: Niet in staat om te uploaden naar {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Klonen mislukt: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Roepletters" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Uw roepletters" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Roepletters van de repeater" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Mijn roepletters" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Downloaden van URCALL lijst" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Downloaden van RPTCALL lijst" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Downloaden van MYCALL lijst" + +#: ../chirpui/editorset.py:87 +msgid "Memories" +msgstr "Kanalen" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Banknamen" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Banken" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "De {vendor} {model} heeft meerdere onafhankelijke sub-apparaten" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Kies n om vanuit te importeren:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Geannuleerd" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Interne fout" + +#: ../chirpui/editorset.py:248 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" +"Er zijn fouten opgetreden tijdens het openen van {file}. De getroffen " +"kanalen zijn niet importeerbaar!" + +#: ../chirpui/editorset.py:260 +msgid "There was an error during import: {error}" +msgstr "Er is een fout ontstaan tijdens het importeren: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Niet-ondersteunende bestandstype" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +msgid "There was an error during export: {error}" +msgstr "Er is een fout opgetreden tijdens het exporteren: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Voorbereiden van geheugen" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Ongeldige waarde voor dit veld" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Toonmodus" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Toon" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "Toon squelch" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "DTCS code" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Cross-mode" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Offset" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Mode" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Vermogen" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Kanaal-afstand" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Overslaan" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Wis kanaal {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Niet in staat om wijzigingen voor dit model te maken" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Bewerken van een nieuw item. Standaardwaarden worden genomen" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Foute waarde voor {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Fout bij het instellen van het geheugen" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Schrijven kanaal {number}" + +#: ../chirpui/memedit.py:361 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Deze bewerking vereist het verplaatsen van alle volgende kanalen totdat een " +"lege locatie is bereikt. Dit kan een lange tijd duren. Weet u zeker dat u " +"dit wilt doen?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Kanaal toevoegen {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Wis kanaal {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Ophalen kanaal {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Verplaats kanaal van {old} naar {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Ruw kanaal {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Ophalen van onbewerkt kanaal {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "U kunt maar van 2 kanalen de verschillen zien" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Kanaal {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Verschil tussen {a} en {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "De kanalen moeten aangrenzend zijn" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Regel hierboven invoegen" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Regel hieronder invoegen" + +#: ../chirpui/memedit.py:702 +msgid "Delete" +msgstr "Verwijderen" + +#: ../chirpui/memedit.py:702 +msgid "Delete all" +msgstr "Alles verwijderen" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Verwijderen (en naar boven verplaatsen)" + +#: ../chirpui/memedit.py:704 +msgid "Move up" +msgstr "Verplaats omhoog" + +#: ../chirpui/memedit.py:705 +msgid "Move down" +msgstr "Verplaats omlaag" + +#: ../chirpui/memedit.py:706 +msgid "Exchange memories" +msgstr "Kanalen wisselen" + +#: ../chirpui/memedit.py:707 +msgid "Cut" +msgstr "Knippen" + +#: ../chirpui/memedit.py:708 +msgid "Copy" +msgstr "Kopiëren" + +#: ../chirpui/memedit.py:709 +msgid "Paste" +msgstr "Plakken" + +#: ../chirpui/memedit.py:710 +msgid "Show Raw Memory" +msgstr "Toon ruw kanaal" + +#: ../chirpui/memedit.py:711 +msgid "Diff Raw Memories" +msgstr "Verschillen ruwe kanalen" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Interne fout: Kolom {name} niet gevonden" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Ophalen kanaal {chan}" + +# You are missing the last bracket +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Interne fout: Ongeldige limiet {number}" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Geheugenbereik:" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Ga" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Speciale kanalen" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Lege tonen" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Knippen kanaal {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Overschrijven?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Plaats {number} overschrijven?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Onverenigbaar kanaal" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "Geplakt kanaal {number} is niet compatibel met deze radio omdat:" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Digitale code" diff --git a/locale/pl.po b/locale/pl.po new file mode 100644 index 0000000..2e19afc --- /dev/null +++ b/locale/pl.po @@ -0,0 +1,987 @@ +# Polish translations for CHIRP package. +# Copyright (C) 2011 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2011-12-07 11:35+0100\n" +"Last-Translator: Grzegorz Błoński-Kubiak \n" +"Language-Team: Polish\n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Ukończono pomyślnie" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "bezczynny" + +#: ../chirpui/bankedit.py:52 +#, fuzzy +msgid "Retrieving bank information" +msgstr "Pobieranie listy banków pamięci" + +#: ../chirpui/bankedit.py:75 +#, fuzzy +msgid "Setting name on bank" +msgstr "Pobieranie listy banków pamięci" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Bank" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Nazwa" + +#: ../chirpui/bankedit.py:185 +#, fuzzy +msgid "Updating bank index for memory {num}" +msgstr "Pobieranie surowej pamięci {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +#, fuzzy +msgid "Getting memory {num}" +msgstr "Pobieranie pamięci {num}" + +#: ../chirpui/bankedit.py:214 +#, fuzzy +msgid "Setting index for memory {num}" +msgstr "Pobieranie pamięci {num}" + +#: ../chirpui/bankedit.py:223 +#, fuzzy +msgid "Getting bank for memory {num}" +msgstr "Pobieranie surowej pamięci {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Lp." + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Częstotliwość" + +#: ../chirpui/bankedit.py:241 +#, fuzzy +msgid "Index" +msgstr "Indeks banków" + +#: ../chirpui/bankedit.py:302 +#, fuzzy +msgid "Getting bank information for memory {num}" +msgstr "Pobieranie surowej pamięci {num}" + +#: ../chirpui/bankedit.py:323 +#, fuzzy +msgid "Getting bank information" +msgstr "Pobieranie listy banków pamięci" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Wystąpił błąd" + +#: ../chirpui/inputdialog.py:130 +#, fuzzy +msgid "Overwrite" +msgstr "Nadpisać ?" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Plik już istnieje" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Plik {name} istneje.Czy chcesz nadpisać?" + +#: ../chirpui/importdialog.py:90 +msgid "" +"Location {number} is already being imported. Choose another value for 'New " +"Location' before selection 'Import'" +msgstr "Lokalizacja {number} jest zaimportowana.Wybierz inna lokalizację." + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Błąd.Wartość musi być liczbą." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "Lokalizacja {number} jest zaimportowana." + +#: ../chirpui/importdialog.py:182 +#, fuzzy +msgid "Updating URCALL list" +msgstr "Pobieranie listy URCALL" + +#: ../chirpui/importdialog.py:187 +#, fuzzy +msgid "Updating RPTCALL list" +msgstr "Pobieranie listy RPTCALL" + +#: ../chirpui/importdialog.py:256 +#, fuzzy +msgid "Setting memory {number}" +msgstr "Pobieranie pamięci {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "" + +#: ../chirpui/importdialog.py:264 +#, fuzzy +msgid "Error importing memories:" +msgstr "Błąd ustawiania pamięci" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Wszystkie" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr " Żaden" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "odwrotność" + +#: ../chirpui/importdialog.py:394 +#, fuzzy +msgid "Select" +msgstr "Wybierz kolumny" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Odwrotny" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Dostosuj nową lokalizację" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Potwierdź nadpisanie" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Opcje" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "" + +#: ../chirpui/importdialog.py:513 +#, fuzzy +msgid "Import From File" +msgstr "Importuj z RFinder" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Importuj" + +#: ../chirpui/importdialog.py:534 +#, fuzzy +msgid "To" +msgstr "Ton" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "" + +#: ../chirpui/importdialog.py:542 +#, fuzzy +msgid "Location memory will be imported into" +msgstr "Lokalizacja {number} jest zaimportowana." + +#: ../chirpui/importdialog.py:543 +#, fuzzy +msgid "Location of memory in the file being imported" +msgstr "Lokalizacja {number} jest zaimportowana." + +#: ../chirpui/importdialog.py:566 +#, fuzzy +msgid "Preparing memory list..." +msgstr "Kasowanie pamięci {loc}" + +#: ../chirpui/importdialog.py:575 +#, fuzzy +msgid "Export To File" +msgstr "Importuj z RFinder" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Eksportuj" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Bez nazwy" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Zrzuty z urządzeń CHIRP" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "Pliki CHIRP" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "Pliki CSV" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "Pliki ICF" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "Pliki VX7 Commander " + +#: ../chirpui/mainapp.py:330 +msgid "" +"ICF files cannot be edited, only displayed or imported into another file. " +"Open in read-only mode?" +msgstr "" +"Plik ICF nie może być edytowany, może być wyświetlany oraz importowany do " +"innego pliku.Otworzyć w trybie do odczytu?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Wystąpił błąd podczas otwierania {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Uwaga:" + +#: ../chirpui/mainapp.py:395 +msgid "" +"The {vendor} {model} operates in live mode. This means that any " +"changes you make are immediately sent to the radio. Because of this, you " +"cannot perform the Save or Upload operations. If you wish to " +"edit the contents offline, please Export to a CSV file, using the " +"File menu." +msgstr "" +"Urządzenie {vendor} {model} pracuje w trybie na żywo. To oznacza, że " +"każda zmiana zostaje natychmiast wysłana do urządzenia. Z tego powodu nie " +"możesz wykonać akcji Zapisz lub Wyślij.Jeśli chcesz edytować " +"zwartość w trybie bez połączenia użyj polecenia Eksportuj do pliku " +"CSV korzystając z menu Plik." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Nie pokazuj tego więcej" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "{vendor} {model} plik zrzutu" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Otwórz poprzednio otwarty plik {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Anulować zmiany?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "Plik został zmodyfikowany, zapisać przed zakończeniem?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Wybierz kolumny" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Widoczne kolumny dla {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Raportowanie jest wyłączone" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by " +"allowing the authors to focus on the radio drivers used most often and " +"errors experienced by the users. The reports contain no identifying " +"information and are used only for statistical purposes by the authors. Your " +"privacy is extremely important, but please consider leaving this feature " +"enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"Funkcja raportowanie programu CHIRP została zaprojektowana by pomóc " +"podnosić jakość dając autorom możliwość skupienia się na sterownikach " +"urządzeń najczęściej używanych i błędach które napotykają użytkowników." +"Raporty nie zawierają danych identyfikujących użytkownika i są " +"wykorzystywane tylko w celach statystycznych przez autorów.Twoja prywatność " +"jest najważniejsza , lecz rozważ pozostawienie tej funkcji włączonej by " +"pomóc ulepszać oprogramowanie CHIRP!" + +#: ../chirpui/mainapp.py:1045 +msgid "" +"Choose a language or Auto to use the operating system default. You will need " +"to restart the application before the change will take effect" +msgstr "" + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Plik" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Niedawny" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Edycja" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Wytnij" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Kopiuj" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Wklej" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Usuń" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "W górę" + +#: ../chirpui/mainapp.py:1184 +#, fuzzy +msgid "Move Dow_n" +msgstr "W dół" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "Zamień" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "_Podgląd" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Kolumny" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Deweloper" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Pokaż surową pamięć" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Porównaj surowe pamięci" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Urządzenie" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Pobierz z urządzenia" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Wyślij do urządzenia" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Importuj z RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "Natywny plik CHIRP" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "Plik CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Importuj z RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +#, fuzzy +msgid "Import from stock config" +msgstr "Importuj z RepeaterBook" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Pomoc" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Raport statystyk" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Ukryj nieużywane pola" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Automatyczny offset repeatera" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Włącz funkcje deweloperskie" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Raportowanie błędów jest włączone" + +#: ../chirpui/mainapp.py:1355 +msgid "" +"If you wish to disable this feature you may do so in the Help menu" +msgstr "Jeśli chcesz wyłączyć tą funkcję możesz to zrobić w menu Pomoc" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Postęp klonowania" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Klonowanie" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Anuluj" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Przełącz" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Przenoszę {src} do {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Szukaj wolnego miejsca ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "Przeniesiono {count} pamięci" + +#: ../chirpui/clone.py:35 +#, fuzzy +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} plik zrzutu" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Wykryj" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Port" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Producent" + +#: ../chirpui/clone.py:125 +#, fuzzy +msgid "Model" +msgstr "Tryb" + +#: ../chirpui/clone.py:138 +#, fuzzy +msgid "Radio" +msgstr "_Urządzenie" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Nie mogę wykryć urządzenia na porcie {port}" + +#: ../chirpui/clone.py:178 +#, fuzzy +msgid "Internal error: Unable to upload to {model}" +msgstr "Błąd wewnętrzny: Niewłaściwy limit {number}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Klonowanie nieudane: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Znak wywoławczy" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Znak wywoławczy korespondenta" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Znak wywoławczy repeatera" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Mój znak wywoławczy" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Pobieranie listy URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Pobieranie listy RPTCALL" + +#: ../chirpui/dstaredit.py:178 +#, fuzzy +msgid "Downloading MYCALL list" +msgstr "Pobieranie listy URCALL" + +#: ../chirpui/editorset.py:87 +#, fuzzy +msgid "Memories" +msgstr "Porównaj surowe pamięci" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +#, fuzzy +msgid "Bank Names" +msgstr "Bank" + +#: ../chirpui/editorset.py:104 +#, fuzzy +msgid "Banks" +msgstr "Bank" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "{vendor} {model} posiada kilka niezależnych urządzeń wbudowanych" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Wybierz do zaimportowania:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Anulowano" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Błąd wewnętrzny" + +#: ../chirpui/editorset.py:248 +msgid "" +"There were errors while opening {file}. The affected memories will not be " +"importable!" +msgstr "" + +#: ../chirpui/editorset.py:260 +#, fuzzy +msgid "There was an error during import: {error}" +msgstr "Wystąpił błąd podczas otwierania {fname}: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Nieobsługiwany typ pliku" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +#, fuzzy +msgid "There was an error during export: {error}" +msgstr "Wystąpił błąd podczas otwierania {fname}: {error}" + +#: ../chirpui/editorset.py:313 +#, fuzzy +msgid "Priming memory" +msgstr "Zapisywanie pamięci {number}" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Niewłaściwa wartość" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Tryb tonu" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Ton" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "Blokada tonu" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "Kod DTCS" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "Polaryzacja DTCS" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Tryb krzyżowy" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Dupleks" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Przesunięcie" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Tryb" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Moc" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Krok strojenia" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Przeskocz" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Kasowanie pamięci {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Nie potrafię wprowadzić zmian w tym modelu" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Edycja nowej pozycji, przyjmuję wartości standardowe" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Zła wartość dla {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Błąd ustawiania pamięci" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Zapisywanie pamięci {number}" + +#: ../chirpui/memedit.py:361 +msgid "" +"This operation requires moving all subsequent channels by one spot until an " +"empty location is reached. This can take a LONG time. Are you sure you " +"want to do this?" +msgstr "" +"Ta operacja wymaga przesunięcia wszystkich komórek pamięci o jeden.Może to " +"zająć dużo czasu.Czy na pewno chcesz to zrobić?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Dodawanie pamięci {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Kasowanie pamięci {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Pobieranie pamięci {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Przenoszenie pamięci z {old} do {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Surowa pamięć {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Pobieranie surowej pamięci {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "Aby porównać wybierz dwie pamięci !" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Pamięć {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Różnice {a} i {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Pamięci muszą zachować ciągłość" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Umieść wiersz wyżej" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Umieść wiersz niżej" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete" +msgstr "_Usuń" + +#: ../chirpui/memedit.py:702 +msgid "Delete all" +msgstr "_Usuń wszystkie" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Skasuj (i skocz wyżej)" + +#: ../chirpui/memedit.py:704 +#, fuzzy +msgid "Move up" +msgstr "W górę" + +#: ../chirpui/memedit.py:705 +#, fuzzy +msgid "Move down" +msgstr "W dół" + +#: ../chirpui/memedit.py:706 +msgid "Exchange memories" +msgstr "Zamień pamięci" + +#: ../chirpui/memedit.py:707 +#, fuzzy +msgid "Cut" +msgstr "_Wytnij" + +#: ../chirpui/memedit.py:708 +#, fuzzy +msgid "Copy" +msgstr "_Kopiuj" + +#: ../chirpui/memedit.py:709 +#, fuzzy +msgid "Paste" +msgstr "_Wklej" + +#: ../chirpui/memedit.py:710 +#, fuzzy +msgid "Show Raw Memory" +msgstr "Pokaż surową pamięć" + +#: ../chirpui/memedit.py:711 +#, fuzzy +msgid "Diff Raw Memories" +msgstr "Porównaj surowe pamięci" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Błąd wewnętrzny: Kolumna {name} nie znaleziona" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Pobieranie kanału {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Błąd wewnętrzny: Niewłaściwy limit {number}" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Zakres pamięci:" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Ok" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Kanały specjalne" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Pokaż puste" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Wycinanie pamięci {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Nadpisać ?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Nadpisać pozycję {number}?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Niekompatybilna pamięć" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "" +"Wklejona komórka pamięci {number} jest nie kompatybilna z tym urządzeniem:" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "" + +#~ msgid "Unsuppported by destination radio: {msgs}" +#~ msgstr "Nieobsługiwany przez urządzenie: {msgs}" + +#~ msgid "New location" +#~ msgstr "Nowa lokalizacja" + +#~ msgid "Location" +#~ msgstr "Lokalizacja" + +#~ msgid "%i errors during open, check the debug log for details" +#~ msgstr "%i błędów podczas otwierania, sprawdź log by obejrzeć szczegóły" diff --git a/locale/pt_BR.po b/locale/pt_BR.po new file mode 100644 index 0000000..d3ba3cf --- /dev/null +++ b/locale/pt_BR.po @@ -0,0 +1,897 @@ +# Language pt-BR translations for CHIRP package. +# Copyright (C) 2013 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2013-03-30 22:04-0300\n" +"Last-Translator: Crezivando \n" +"Language-Team: Language pt-BR\n" +"Language: pt-BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=ISO-8859-15\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Concludo" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "ocioso" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Recuperando informaes do banco" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Configurando nome no banco" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Banco" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Nome" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Atualizando ndice do banco para memria {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Atualizando informao do banco para memria {num}" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Obtendo memria {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Configurando ndice para memria {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Obtendo banco para memria {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Loc" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Frequncia" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "ndice" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Obtendo informao do banco para memria {num}" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Obtendo informao do banco" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Ocorreu um erro" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Sobrescrever" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Arquivo j Existe" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "O arquivo {name} j existe. Voc quer sobrescrev-lo" + +#: ../chirpui/importdialog.py:90 +msgid "Location {number} is already being imported. Choose another value for 'New Location' before selection 'Import'" +msgstr "Localizao {number} est sendo importada. Escolha outro valor para 'Nova Localizao' antes de selecionar 'Importar'" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Valor invlido. Deve ser um nmero inteiro." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "Localizao {number} est sendo importada" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Atualizando lista URCALL" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Atualizando lista RPTCALL" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Configurando memria {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Importando informao do banco" + +#: ../chirpui/importdialog.py:264 +msgid "Error importing memories:" +msgstr "Erro ao importar memrias:" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Tudo" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Nenhum" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Inverso" + +#: ../chirpui/importdialog.py:394 +msgid "Select" +msgstr "Selecione" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Auto" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Reverso" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Ajustar Nova Localizao" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Confirmar sobrescrever" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Opes" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "No pode ser importado porque" + +#: ../chirpui/importdialog.py:513 +msgid "Import From File" +msgstr "Importar do Arquivo" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Importar" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "Para" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "De" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Comentrio" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "Memria de localizao ser importada para" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Localizao da memria no arquivo sendo importada" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Preparando lista de memria..." + +#: ../chirpui/importdialog.py:575 +msgid "Export To File" +msgstr "Exportar Para Arquivo" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Exportar" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Sem-Ttulo" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Rdio Imagens CHIRP" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "Arquivos CHIRP" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "Arquivos CSV" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "Arquivos ICF" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "Arquivos VX7 Commander" + +#: ../chirpui/mainapp.py:330 +msgid "ICF files cannot be edited, only displayed or imported into another file. Open in read-only mode?" +msgstr "Arquivos ICF no podem ser editados, somente exibidos ou importados para outro arquivo. Abrir no modo somente-leitura?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Houve um erro ao abrir {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} erros durante abertura:" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Nota:" + +#: ../chirpui/mainapp.py:395 +msgid "The {vendor} {model} operates in live mode. This means that any changes you make are immediately sent to the radio. Because of this, you cannot perform the Save or Upload operations. If you wish to edit the contents offline, please Export to a CSV file, using the File menu." +msgstr "O {vendor} {model} opera em modo ativo. Isto significa que quaisquer alteraes que voc fizer so imediatamente enviadas para o rdio. Devido a isto voc no pode executar as operaes Salvar ou Carregar. Se voc quiser editar o contedo off-line, por favor Exportar para um arquivo CSV, usando o menu Arquivo." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "No mostrar isto novamente" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "Arquivo Imagem {vendor} {model}" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Abrir arquivo recente {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Importar configurao de estoque {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Abrir configurao de estoque {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Descartar Alteraes?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "Arquivo modificado, salvar alteraes antes de fechar?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "Com importantes contribuies de:" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Selecionar Colunas" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Colunas Visveis para {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Emisso de Relatrios desabilitada" + +#: ../chirpui/mainapp.py:1013 +msgid "" +"The reporting feature of CHIRP is designed to help improve quality by allowing the authors to focus on the radio drivers used most often and errors experienced by the users. The reports contain no identifying information and are used only for statistical purposes by the authors. Your privacy is extremely important, but please consider leaving this feature enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "" +"O recurso de relatrio do CHIRP projetado para ajudar melhorar a qualidade permitindo aos autores focalizar os drivers de rdio mais frequentemente usados e erros experimentados pelos usurios. Os relatrios no contm nenhuma informao de identificao e so utilizados apenas para fins estatsticos pelos autores. Sua privacidade extremamente importante, mas por favor considere deixar este recurso ativado para ajudar a melhorar o CHIRP!\n" +"\n" +"Voc tem certeza de que quer desabilitar este recurso?" + +#: ../chirpui/mainapp.py:1045 +msgid "Choose a language or Auto to use the operating system default. You will need to restart the application before the change will take effect" +msgstr "Escolha um idioma ou Auto para usar o sistema padro. Voc precisar reinicializar o aplicativo antes para que as mudanas tenham efeito." + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Arquivo" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Abrir config do estoque" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Recente" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Editar" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Cut" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Copiar" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Colar" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Apagar" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Mover _Acima" + +#: ../chirpui/mainapp.py:1184 +msgid "Move Dow_n" +msgstr "Mover Abaix_o" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "T_roca" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "_Visualizar" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Colunas" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Desenvolvedor" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Mostrar memria raw" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Diff memrias raw" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Diff tabs" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Mudar idioma" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Rdio" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Descarregar a partir do Rdio" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Carregar para o Rdio" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Importar de RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "Arquivo Nativo CHIRP" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "Arquivo CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Importar do Catlogo de Repetidoras" + +#: ../chirpui/mainapp.py:1202 +msgid "Import from stock config" +msgstr "Importar do config estoque" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Ajuda" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Relatrio estatstico" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Ocultar Campos No-utilizados" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Offset Automtico Repetidoras" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Habilitar Funes de Desenvolvedor" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Relatrio de Erros habilitado" + +#: ../chirpui/mainapp.py:1355 +msgid "If you wish to disable this feature you may do so in the Help menu" +msgstr "Se voc desejar desabilitar este recurso voc pode faz-lo no menu Ajuda" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Clonagem em Progresso" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Clonando" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Cancelar" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Shift" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Movendo {src} para {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Procurando por um ponto livre ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "Memrias {count} movidas" + +#: ../chirpui/clone.py:35 +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} na {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Detectar" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Porta" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Fornecedor" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Modelo" + +#: ../chirpui/clone.py:138 +msgid "Radio" +msgstr "Rdio" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Incapaz de detectar rdio na {port}" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Erro Interno: Incapaz de descarregar para {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Clonagem falhou: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Indicativo" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Seu indicativo" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Indicativo da Repetidora" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Meu indicativo" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Descarregando lista URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Descarregando lista RPTCALL" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Descarregando lista MYCALL" + +#: ../chirpui/editorset.py:87 +msgid "Memories" +msgstr "Memrias" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Nomes do Banco" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Bancos" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "O {vendor} {model} tem vrios sub-dispositivos independentes" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Selecionar um para importar de:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Cancelado" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Erro Interno" + +#: ../chirpui/editorset.py:248 +msgid "There were errors while opening {file}. The affected memories will not be importable!" +msgstr "Houve erros ao abrir {file}. As memrias afetadas no sero importveis!" + +#: ../chirpui/editorset.py:260 +msgid "There was an error during import: {error}" +msgstr "Ocorreu um erro durante importao: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Tipo de arquivo no-suportado" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +msgid "There was an error during export: {error}" +msgstr "Existiu um erro durante exportao: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Memria de escorva" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Valor Invlido para este campo" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Modo Tom" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Tom" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "TomSql" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "DTCS Code" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Modo Cross" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Duplex" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Offset" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Modo" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Power" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Tune Step" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Skip" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Apagando memria {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Incapaz de efetuar alteraes para este modelo" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Editando novo item, tomando padres" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Valor invlido para {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Erro gravando memria" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Gravando memria {number}" + +#: ../chirpui/memedit.py:361 +msgid "This operation requires moving all subsequent channels by one spot until an empty location is reached. This can take a LONG time. Are you sure you want to do this?" +msgstr "Esta operao requer mover todos os canais subsequentes por um ponto, at que seja preenchido um local vazio. Isto pode levar MUITO tempo. Tem certeza de que quer fazer isto?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Adicionando memria {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Apagando memria {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Obtendo memria {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Movendo memria de {old} para {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Memria raw {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Obtendo memria raw {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "Voc s pode diff duas memrias!" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Memria {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Diff de {a} e {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Memrias devem ser contguas" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Inserir row acima" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Inserir row abaixo" + +#: ../chirpui/memedit.py:702 +msgid "Delete" +msgstr "Apagar" + +#: ../chirpui/memedit.py:702 +msgid "Delete all" +msgstr "Apagar tudo" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Apagar (e deslocar)" + +#: ../chirpui/memedit.py:704 +msgid "Move up" +msgstr "Mover acima" + +#: ../chirpui/memedit.py:705 +msgid "Move down" +msgstr "Mover abaixo" + +#: ../chirpui/memedit.py:706 +msgid "Exchange memories" +msgstr "Trocar memrias" + +#: ../chirpui/memedit.py:707 +msgid "Cut" +msgstr "Cortar" + +#: ../chirpui/memedit.py:708 +msgid "Copy" +msgstr "Copiar" + +#: ../chirpui/memedit.py:709 +msgid "Paste" +msgstr "Colar" + +#: ../chirpui/memedit.py:710 +msgid "Show Raw Memory" +msgstr "Mostrar Memria Raw" + +#: ../chirpui/memedit.py:711 +msgid "Diff Raw Memories" +msgstr "Diff Memrias Raw" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Erro Interno: Coluna {name} no encontrada" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Obtendo canal {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Erro Interndo: limite Invlido {number}" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Intervalo de Memria" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Ir" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Canais Especiais" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Mostrar Vazias" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Cortando memria {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Sobrescrever?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Sobrescrever local {number}?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Memria Incompatvel" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "Memria colada {number} no compatvel com este rdio porque:" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Cdigo Digital" + diff --git a/locale/ru.po b/locale/ru.po new file mode 100644 index 0000000..24b76ef --- /dev/null +++ b/locale/ru.po @@ -0,0 +1,907 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: CHIRP\n" +"Language: ru\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Завершено" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "ожидание" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Получение информации" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Установка имени банка" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Банк" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Имя" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Обновление индекса банка в памяти {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Обновление информации банка в памяти {num}" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Получение из памяти {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Установка индекса для памяти {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Получение банка памяти {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "№" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Частота" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "Индекс" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Получение информации банка для памяти {num}" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Получение информации из банка" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Ошибка" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Перезаписать" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Файл существует" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Файл {name} уже существует. Перезаписать его?" + +#: ../chirpui/importdialog.py:90 +msgid "Location {number} is already being imported. Choose another value for 'New Location' before selection 'Import'" +msgstr "Позиция {number} уже импортирована. Выберите другое значение для \"Новая позиция\" перед выбором \"Импорт\"" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Неверное значение. Должно быть целое число." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "Позиция {number} уже импортирована" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Обновление URCALL" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Обновление RPTCALL" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Установка памяти {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Импорт информации банка" + +#: ../chirpui/importdialog.py:264 +#, fuzzy +msgid "Error importing memories:" +msgstr "Ошибка импорта памяти:" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Все" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Ничего" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Инверсия" + +#: ../chirpui/importdialog.py:394 +#, fuzzy +msgid "Select" +msgstr "Выбор" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Авто" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Обратный" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Новая позиция" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Подтверждение перезаписи" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Опции" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "Невозможно импортировать, поскольку" + +#: ../chirpui/importdialog.py:513 +#, fuzzy +msgid "Import From File" +msgstr "Импорт из файла" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Импорт" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "Для" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "От" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Комментарий" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "Позиция памяти будет импортирована в" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Позиция памяти в файле импортируется" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Подготовка списка памяти..." + +#: ../chirpui/importdialog.py:575 +#, fuzzy +msgid "Export To File" +msgstr "Экспорт в файл" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Экспорт" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Новый" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Образы памяти Chirp" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "CHIRP" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "CSV" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "ICF" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:330 +msgid "ICF files cannot be edited, only displayed or imported into another file. Open in read-only mode?" +msgstr "Файлы ICF не могут быть отредактированы, возможно только показать или импортировать в другой файл. Открыть только для чтения?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Ошибка открытия {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} ошибки(ок) во время открытия:" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Заметка:" + +#: ../chirpui/mainapp.py:395 +msgid "The {vendor} {model} operates in live mode. This means that any changes you make are immediately sent to the radio. Because of this, you cannot perform the Save or Upload operations. If you wish to edit the contents offline, please Export to a CSV file, using the File menu." +msgstr "{vendor} {model} программируется в режиме онлайн. Это означает, что любые изменения немедленно загружаются в станцию. По этой причине невозможно использовать операции Сохранить или Загрузить. Если вы хотите редактировать данные офлайн, то используйте Экспорт в файл CSV из меню Файл." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Не отображать снова" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "Изображение {vendor} {model}" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "VX7 Commander" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Открыть недавний файл {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Импорт предустановленную конфигурацию {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Открыть предустановленную конфигурацию {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Не сохранять изменения?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "Файл изменён, сохранить изменения?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "При значительном участии:" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Выбрать столбцы" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Видимые столбцы для {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Отчёты отключены" + +#: ../chirpui/mainapp.py:1013 +msgid "The reporting feature of CHIRP is designed to help improve quality by allowing the authors to focus on the radio drivers used most often and errors experienced by the users. The reports contain no identifying information and are used only for statistical purposes by the authors. Your privacy is extremely important, but please consider leaving this feature enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "Функция отправки отчётов позволяет авторам получать информацию об ошибках программы, чтобы своевремменно улучшать её для Вас. Никакой персональной информации при этом не собирается и не отправляется. Позволяя программе отправлять отчёты вы помогаете авторам в дальнейшем усовершенствовании программы.\n" +"\n" +"\n" +"Вы действительно хотите отключить эту функцию?" + +#: ../chirpui/mainapp.py:1045 +msgid "Choose a language or Auto to use the operating system default. You will need to restart the application before the change will take effect" +msgstr "Выберите язык или Авто для использования языка операционной системы. Вам нужно будет перезапустить приложение, прежде чем изменения вступят в силу." + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Файл" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Открыть предустановленные конфигурации" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "_Недавние" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Правка" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Вырезать" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Копировать" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Вставить" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Удалить" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Вверх_" + +#: ../chirpui/mainapp.py:1184 +#, fuzzy +msgid "Move Dow_n" +msgstr "Вниз_" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "Обмен_" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "Вид_" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Столбцы" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Разработчик" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Show raw memory" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Diff raw memories" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Diff tabs" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Изменить язык" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Станция" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Чтение из станции" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Запись в станцию" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Импорт из RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "Файл CHIRP" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Импорт из RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +#, fuzzy +msgid "Import from stock config" +msgstr "Импорт из предустановленных конфигураций" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Справка" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Отправка статистики" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Скрыть неиспользуемые поля" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Авторазнос для репитера" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Режим разработчика" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Отчёты об ошибках включены" + +#: ../chirpui/mainapp.py:1355 +msgid "If you wish to disable this feature you may do so in the Help menu" +msgstr "Отключить эту функцию можно в меню Справка" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Загрузка" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Загрузка" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Отмена" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Сдвиг" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Перемещение {src} до {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Поиск свободной точки ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "Перемещенно {count} памяти" + +#: ../chirpui/clone.py:35 +#, fuzzy +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} на {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Проверка" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Порт" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Изготовитель" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Модель" + +#: ../chirpui/clone.py:138 +#, fuzzy +msgid "Radio" +msgstr "Станция" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Станция не обнаружена на {port}" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Ошибка: Невозможно загрузить в {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Ошибка загрузки {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Позывной" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Ваш позывной" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Позывной репитера" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Мой позывной" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Загрузка URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Загрузка RPTCALL" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Загрузка MYCALL" + +#: ../chirpui/editorset.py:87 +#, fuzzy +msgid "Memories" +msgstr "Память" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Имена банков" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Банки" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "{vendor} {model} имеет несколько различных подустройства" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Выберите один для импорта из:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Отменено" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Ошибка" + +#: ../chirpui/editorset.py:248 +msgid "There were errors while opening {file}. The affected memories will not be importable!" +msgstr "Были обнаружены ошибки при открытии. Затронутая память не сможет быть импортирована." + +#: ../chirpui/editorset.py:260 +#, fuzzy +msgid "There was an error during import: {error}" +msgstr "Ошибка открытия{fname}: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Неподдерживаемый тип файла" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +#, fuzzy +msgid "There was an error during export: {error}" +msgstr "Ошибка открытия {fname}: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Первичная память" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Неверное значение для этого поля" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Вид субтона" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "ТонПРД" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "ТонШПД" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "DTCSПРД" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Кроссрежим" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Дуплекс" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Смещение" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Режим" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Мощность" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Шаг" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Пропуск" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Стирание памяти {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Невозможно внести изменения в эту модель" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Для новой позиции настройки по умолчанию" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Некорректное значение для {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Ошибка установки памяти" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "запись памяти {number}" + +#: ../chirpui/memedit.py:361 +msgid "This operation requires moving all subsequent channels by one spot until an empty location is reached. This can take a LONG time. Are you sure you want to do this?" +msgstr "Эта операция потребует перемещения всех последующих каналов в одно место, пока свободное место не дудет достигнуто. Это может занять ОЧЕНЬ много времени. Вы уверены, что хотите это сделать?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Добавление памяти {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Стирание памяти {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Получение памяти {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Перемещение памяти из {old} в {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "Бинарная память {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Получение бинарной памяти {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "You can only diff two memories!" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Память {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Diff of {a} and {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Память должна быть непрерывной" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Вставка строки выше" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Вставка строки ниже" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete" +msgstr "_Удалить" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete all" +msgstr "_Удалить всё" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Удалить (и сдвинуть вверх)" + +#: ../chirpui/memedit.py:704 +#, fuzzy +msgid "Move up" +msgstr "Вверх" + +#: ../chirpui/memedit.py:705 +#, fuzzy +msgid "Move down" +msgstr "Вниз" + +#: ../chirpui/memedit.py:706 +#, fuzzy +msgid "Exchange memories" +msgstr "Обмен памяти" + +#: ../chirpui/memedit.py:707 +#, fuzzy +msgid "Cut" +msgstr "Вырезать" + +#: ../chirpui/memedit.py:708 +#, fuzzy +msgid "Copy" +msgstr "Копировать" + +#: ../chirpui/memedit.py:709 +#, fuzzy +msgid "Paste" +msgstr "Вставить" + +#: ../chirpui/memedit.py:710 +#, fuzzy +msgid "Show Raw Memory" +msgstr "Show Raw Memory\t" + +#: ../chirpui/memedit.py:711 +#, fuzzy +msgid "Diff Raw Memories" +msgstr "Diff raw memories" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Ошибка: Столбец {name} не найден" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Получение канала {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Ошибка: недопустимый предел {number}" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Диапазон каналов" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Ок" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Специальные каналы" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Показывать пустые" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Обрезка памяти {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Перезаписать?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Перезаписать расположение {number}?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Несовместимая память" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "Вставленная память {number} не совместима с этой станцией, потому что:" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Цифровой код" + diff --git a/locale/uk_UA.po b/locale/uk_UA.po new file mode 100644 index 0000000..a57ce2d --- /dev/null +++ b/locale/uk_UA.po @@ -0,0 +1,917 @@ +# Ukrainian translations for CHIRP package. +# Copyright (C) 2011 Dan Smith +# This file is distributed under the same license as the CHIRP package. +# Dan Smith , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: CHIRP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-02-16 12:06-0800\n" +"PO-Revision-Date: 2015-11-30 10:36+0200\n" +"Last-Translator: laser \n" +"Language-Team: laser \n" +"Language: uk_UA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../chirpui/common.py:204 +msgid "Completed" +msgstr "Завершено" + +#: ../chirpui/common.py:205 +msgid "idle" +msgstr "простоювання" + +#: ../chirpui/bankedit.py:52 +msgid "Retrieving bank information" +msgstr "Отримання інформації банку" + +#: ../chirpui/bankedit.py:75 +msgid "Setting name on bank" +msgstr "Настройка назви бана" + +#: ../chirpui/bankedit.py:85 +msgid "Bank" +msgstr "Банк" + +#: ../chirpui/bankedit.py:86 ../chirpui/bankedit.py:240 +#: ../chirpui/importdialog.py:536 ../chirpui/memedit.py:65 +#: ../chirpui/memedit.py:85 ../chirpui/memedit.py:247 +#: ../chirpui/memedit.py:872 ../chirpui/memedit.py:926 +#: ../chirpui/memedit.py:1063 ../chirpui/memedit.py:1065 +msgid "Name" +msgstr "Ім'я" + +#: ../chirpui/bankedit.py:185 +msgid "Updating bank index for memory {num}" +msgstr "Оновлення індексу банку пам'яті {num}" + +#: ../chirpui/bankedit.py:194 +msgid "Updating bank information for memory {num}" +msgstr "Оновлення інформації банку пам'яті {num}" + +#: ../chirpui/bankedit.py:200 ../chirpui/bankedit.py:229 +msgid "Getting memory {num}" +msgstr "Отримання пам'яті {num}" + +#: ../chirpui/bankedit.py:214 +msgid "Setting index for memory {num}" +msgstr "Установка індексу для пам'яті {num}" + +#: ../chirpui/bankedit.py:223 +msgid "Getting bank for memory {num}" +msgstr "Отримання банку пам'яті {num}" + +#: ../chirpui/bankedit.py:238 ../chirpui/memedit.py:63 +#: ../chirpui/memedit.py:172 ../chirpui/memedit.py:246 +#: ../chirpui/memedit.py:315 ../chirpui/memedit.py:335 +#: ../chirpui/memedit.py:349 ../chirpui/memedit.py:423 +#: ../chirpui/memedit.py:435 ../chirpui/memedit.py:459 +#: ../chirpui/memedit.py:461 ../chirpui/memedit.py:534 +#: ../chirpui/memedit.py:548 ../chirpui/memedit.py:550 +#: ../chirpui/memedit.py:591 ../chirpui/memedit.py:593 +#: ../chirpui/memedit.py:621 ../chirpui/memedit.py:822 +#: ../chirpui/memedit.py:870 ../chirpui/memedit.py:895 +#: ../chirpui/memedit.py:907 ../chirpui/memedit.py:924 +#: ../chirpui/memedit.py:1230 +msgid "Loc" +msgstr "Loc" + +#: ../chirpui/bankedit.py:239 ../chirpui/importdialog.py:537 +#: ../chirpui/memedit.py:64 ../chirpui/memedit.py:86 ../chirpui/memedit.py:187 +#: ../chirpui/memedit.py:248 ../chirpui/memedit.py:271 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:304 +#: ../chirpui/memedit.py:873 ../chirpui/memedit.py:923 +msgid "Frequency" +msgstr "Частота" + +#: ../chirpui/bankedit.py:241 +msgid "Index" +msgstr "Зміст" + +#: ../chirpui/bankedit.py:302 +msgid "Getting bank information for memory {num}" +msgstr "Отримання інформації банку для пам'яті {num}" + +#: ../chirpui/bankedit.py:323 +msgid "Getting bank information" +msgstr "Отримання інформації банку" + +#: ../chirpui/inputdialog.py:81 +msgid "An error has occurred" +msgstr "Сталася помилка" + +#: ../chirpui/inputdialog.py:130 +msgid "Overwrite" +msgstr "Перезаписати" + +#: ../chirpui/inputdialog.py:133 +msgid "File Exists" +msgstr "Файл існує" + +#: ../chirpui/inputdialog.py:136 +msgid "The file {name} already exists. Do you want to overwrite it?" +msgstr "Файл {name} вже існує. Ви дійсно хочете перезаписати його?" + +#: ../chirpui/importdialog.py:90 +msgid "Location {number} is already being imported. Choose another value for 'New Location' before selection 'Import'" +msgstr "Розташування {number} вже імпортуються. Виберіть інше значення для 'Нове розташування' перед вибором 'Імпорт'" + +#: ../chirpui/importdialog.py:121 +msgid "Invalid value. Must be an integer." +msgstr "Неприпустиме значення. Повинно бути цілим числом." + +#: ../chirpui/importdialog.py:130 +msgid "Location {number} is already being imported" +msgstr "Розташування {number} вже імпортованого" + +#: ../chirpui/importdialog.py:182 +msgid "Updating URCALL list" +msgstr "Оновлення списку URCALL" + +#: ../chirpui/importdialog.py:187 +msgid "Updating RPTCALL list" +msgstr "Оновлення списку RPTCALL" + +#: ../chirpui/importdialog.py:256 +msgid "Setting memory {number}" +msgstr "Установка пам'яті {number}" + +#: ../chirpui/importdialog.py:260 +msgid "Importing bank information" +msgstr "Імпортування інформації банку" + +#: ../chirpui/importdialog.py:264 +#, fuzzy +msgid "Error importing memories:" +msgstr "Помилка імпорту пам'яті:" + +#: ../chirpui/importdialog.py:376 +msgid "All" +msgstr "Все" + +#: ../chirpui/importdialog.py:382 +msgid "None" +msgstr "Не вказано" + +#: ../chirpui/importdialog.py:388 +msgid "Inverse" +msgstr "Інверсія" + +#: ../chirpui/importdialog.py:394 +#, fuzzy +msgid "Select" +msgstr "Обрати" + +#: ../chirpui/importdialog.py:416 +msgid "Auto" +msgstr "Авто" + +#: ../chirpui/importdialog.py:422 +msgid "Reverse" +msgstr "Реверс" + +#: ../chirpui/importdialog.py:428 +msgid "Adjust New Location" +msgstr "Настроїти нове розташування" + +#: ../chirpui/importdialog.py:438 +msgid "Confirm overwrites" +msgstr "Підтвердіть заміну" + +#: ../chirpui/importdialog.py:444 +msgid "Options" +msgstr "Опції" + +#: ../chirpui/importdialog.py:495 +msgid "Cannot be imported because" +msgstr "Не можна імпортувати, оскільки" + +#: ../chirpui/importdialog.py:513 +#, fuzzy +msgid "Import From File" +msgstr "Імпортувати з файлу" + +#: ../chirpui/importdialog.py:514 ../chirpui/mainapp.py:1196 +msgid "Import" +msgstr "Імпорт" + +#: ../chirpui/importdialog.py:534 +msgid "To" +msgstr "До" + +#: ../chirpui/importdialog.py:535 +msgid "From" +msgstr "З" + +#: ../chirpui/importdialog.py:538 ../chirpui/memedit.py:78 +#: ../chirpui/memedit.py:99 ../chirpui/memedit.py:886 +#: ../chirpui/memedit.py:940 ../chirpui/memedit.py:1068 +msgid "Comment" +msgstr "Коментар" + +#: ../chirpui/importdialog.py:542 +msgid "Location memory will be imported into" +msgstr "Розташування пам'яті буде імпортовано до" + +#: ../chirpui/importdialog.py:543 +msgid "Location of memory in the file being imported" +msgstr "Розташування пам'яті у файлі, що імпортуються" + +#: ../chirpui/importdialog.py:566 +msgid "Preparing memory list..." +msgstr "Підготовка списку пам'яті..." + +#: ../chirpui/importdialog.py:575 +#, fuzzy +msgid "Export To File" +msgstr "Експорт до файлу" + +#: ../chirpui/importdialog.py:576 ../chirpui/mainapp.py:1197 +msgid "Export" +msgstr "Експорт" + +#: ../chirpui/mainapp.py:269 ../chirpui/mainapp.py:483 +msgid "Untitled" +msgstr "Без назви" + +#: ../chirpui/mainapp.py:316 ../chirpui/mainapp.py:715 +msgid "CHIRP Radio Images" +msgstr "Образи радіостанцій CHIRP" + +#: ../chirpui/mainapp.py:317 ../chirpui/mainapp.py:714 +#: ../chirpui/mainapp.py:880 +msgid "CHIRP Files" +msgstr "CHIRP файли" + +#: ../chirpui/mainapp.py:318 ../chirpui/mainapp.py:716 +#: ../chirpui/mainapp.py:879 +msgid "CSV Files" +msgstr "CSV-файли" + +#: ../chirpui/mainapp.py:319 ../chirpui/mainapp.py:717 +msgid "ICF Files" +msgstr "ICF файли" + +#: ../chirpui/mainapp.py:320 ../chirpui/mainapp.py:718 +msgid "VX7 Commander Files" +msgstr "Файли VX7 командира" + +#: ../chirpui/mainapp.py:330 +msgid "ICF files cannot be edited, only displayed or imported into another file. Open in read-only mode?" +msgstr "ICF файли не можна редагувати, тільки відобразити або імпортувати. Відкрити в режимі тільки для читання?" + +#: ../chirpui/mainapp.py:373 +msgid "There was an error opening {fname}: {error}" +msgstr "Сталася помилка при відкритті {fname}: {error}" + +#: ../chirpui/mainapp.py:388 +msgid "{num} errors during open:" +msgstr "{num} помилки під час відкриття:" + +#: ../chirpui/mainapp.py:394 +msgid "Note:" +msgstr "Примітка:" + +#: ../chirpui/mainapp.py:395 +msgid "The {vendor} {model} operates in live mode. This means that any changes you make are immediately sent to the radio. Because of this, you cannot perform the Save or Upload operations. If you wish to edit the contents offline, please Export to a CSV file, using the File menu." +msgstr "{vendor} {model} працює в режимі реального часу. Це означає, що будь-які зміни негайно відправляються на радіостанцію. Через це не вдалося виконати Збереження або Запис операцій. Якщо ви хочете редагувати вміст в автономному режимі, будь ласка, Експортуйте файл CSV, за допомогою Меню Файл." + +#: ../chirpui/mainapp.py:404 +msgid "Don't show this again" +msgstr "Не показувати наступного разу" + +#: ../chirpui/mainapp.py:448 +msgid "{vendor} {model} image file" +msgstr "файлу образу {vendor} {model}" + +#: ../chirpui/mainapp.py:456 +msgid "VX7 Commander" +msgstr "Командир VX7" + +#: ../chirpui/mainapp.py:518 +msgid "Open recent file {name}" +msgstr "Відкриті останній файл {name}" + +#: ../chirpui/mainapp.py:579 +msgid "Import stock configuration {name}" +msgstr "Імпорт заводської конфігурації {name}" + +#: ../chirpui/mainapp.py:595 +msgid "Open stock configuration {name}" +msgstr "Відкрите заводські конфігурації {name}" + +#: ../chirpui/mainapp.py:681 +msgid "Discard Changes?" +msgstr "Скасувати зміни?" + +#: ../chirpui/mainapp.py:686 +msgid "File is modified, save changes before closing?" +msgstr "Файл змінено, зберегти зміни перед закриттям?" + +#: ../chirpui/mainapp.py:923 +msgid "With significant contributions by:" +msgstr "Значні внески зробили:" + +#: ../chirpui/mainapp.py:940 +msgid "Select Columns" +msgstr "Виберіть Стовпці" + +#: ../chirpui/mainapp.py:955 +msgid "Visible columns for {radio}" +msgstr "Видимі стовпці для {radio}" + +#: ../chirpui/mainapp.py:1012 +msgid "Reporting is disabled" +msgstr "Звіти вимкнуто" + +#: ../chirpui/mainapp.py:1013 +msgid "The reporting feature of CHIRP is designed to help improve quality by allowing the authors to focus on the radio drivers used most often and errors experienced by the users. The reports contain no identifying information and are used only for statistical purposes by the authors. Your privacy is extremely important, but please consider leaving this feature enabled to help make CHIRP better!\n" +"\n" +"Are you sure you want to disable this feature?" +msgstr "Функцію звітування CHIRP розроблено, щоб допомогти поліпшити якість, дозволяючи авторам зосередитися на драйверах радіостанцій, що найчастіше використовується і помилках досвідчених користувачів. Звіти не містять ідентифікаційну інформацію і використовуються тільки для статистичних цілей авторів. Ваша конфіденційність є надзвичайно важлива, але будь ласка, залиште цю функцію включеною, щоб допомогти зробити CHIRP краще!\n" +"\n" +"ви впевнені, що хочете відключити цю функцію?" + +#: ../chirpui/mainapp.py:1045 +msgid "Choose a language or Auto to use the operating system default. You will need to restart the application before the change will take effect" +msgstr "Виберіть мову або авто для використання в операційній системі. Вам потрібно буде перезапустити додаток до того, як зміни вступлять в силу" + +#: ../chirpui/mainapp.py:1169 +msgid "_File" +msgstr "_Файл" + +#: ../chirpui/mainapp.py:1172 +msgid "Open stock config" +msgstr "Відкрити заводські конфігурації" + +#: ../chirpui/mainapp.py:1173 +msgid "_Recent" +msgstr "Ос_танні" + +#: ../chirpui/mainapp.py:1178 +msgid "_Edit" +msgstr "_Редагувати" + +#: ../chirpui/mainapp.py:1179 +msgid "_Cut" +msgstr "_Вирізати" + +#: ../chirpui/mainapp.py:1180 +msgid "_Copy" +msgstr "_Копіювати" + +#: ../chirpui/mainapp.py:1181 +msgid "_Paste" +msgstr "_Вставити" + +#: ../chirpui/mainapp.py:1182 +msgid "_Delete" +msgstr "_Видалити" + +#: ../chirpui/mainapp.py:1183 +msgid "Move _Up" +msgstr "Перемістити В_гору" + +#: ../chirpui/mainapp.py:1184 +#, fuzzy +msgid "Move Dow_n" +msgstr "Перемістити В_низ" + +#: ../chirpui/mainapp.py:1185 +msgid "E_xchange" +msgstr "_Обмін" + +#: ../chirpui/mainapp.py:1186 +msgid "_View" +msgstr "В_игляд" + +#: ../chirpui/mainapp.py:1187 +msgid "Columns" +msgstr "Стовпці" + +#: ../chirpui/mainapp.py:1188 +msgid "Developer" +msgstr "Розробник" + +#: ../chirpui/mainapp.py:1189 +msgid "Show raw memory" +msgstr "Показати raw пам'ять" + +#: ../chirpui/mainapp.py:1190 +msgid "Diff raw memories" +msgstr "Порівняти raw пам'ять" + +#: ../chirpui/mainapp.py:1191 +msgid "Diff tabs" +msgstr "Вкладки порівняння" + +#: ../chirpui/mainapp.py:1192 +msgid "Change language" +msgstr "Змінити мову" + +#: ../chirpui/mainapp.py:1193 +msgid "_Radio" +msgstr "_Радіостанція" + +#: ../chirpui/mainapp.py:1194 +msgid "Download From Radio" +msgstr "Скопіювати з радіостанції" + +#: ../chirpui/mainapp.py:1195 +msgid "Upload To Radio" +msgstr "Записати в радіостанцію" + +#: ../chirpui/mainapp.py:1198 +msgid "Import from RFinder" +msgstr "Імпорт з RFinder" + +#: ../chirpui/mainapp.py:1199 +msgid "CHIRP Native File" +msgstr "Файл CHIRP" + +#: ../chirpui/mainapp.py:1200 +msgid "CSV File" +msgstr "Файл CSV" + +#: ../chirpui/mainapp.py:1201 +msgid "Import from RepeaterBook" +msgstr "Імпорт з RepeaterBook" + +#: ../chirpui/mainapp.py:1202 +#, fuzzy +msgid "Import from stock config" +msgstr "Імпорт з сховища конфігурацій" + +#: ../chirpui/mainapp.py:1204 +msgid "Help" +msgstr "Довідка" + +#: ../chirpui/mainapp.py:1215 +msgid "Report statistics" +msgstr "Звіт статистики" + +#: ../chirpui/mainapp.py:1216 +msgid "Hide Unused Fields" +msgstr "Приховувати невикористовувані поля" + +#: ../chirpui/mainapp.py:1217 +msgid "Automatic Repeater Offset" +msgstr "Автоматичний рознос репітера" + +#: ../chirpui/mainapp.py:1218 +msgid "Enable Developer Functions" +msgstr "Увімкнення функції розробника" + +#: ../chirpui/mainapp.py:1352 +msgid "Error reporting is enabled" +msgstr "Звітування про критичні помилки ввімкнуто" + +#: ../chirpui/mainapp.py:1355 +msgid "If you wish to disable this feature you may do so in the Help menu" +msgstr "Якщо ви хочете вимкнути цю функцію, ви можете зробити це в меню Довідка" + +#: ../chirpui/cloneprog.py:43 +msgid "Clone Progress" +msgstr "Поступ клонування" + +#: ../chirpui/cloneprog.py:46 +msgid "Cloning" +msgstr "Клонування" + +#: ../chirpui/cloneprog.py:55 +msgid "Cancel" +msgstr "Скасувати" + +#: ../chirpui/shiftdialog.py:27 +msgid "Shift" +msgstr "Зміщення" + +#: ../chirpui/shiftdialog.py:63 +msgid "Moving {src} to {dst}" +msgstr "Переміщення {src} до {dst}" + +#: ../chirpui/shiftdialog.py:80 +msgid "Looking for a free spot ({number})" +msgstr "Пошук вільної точки ({number})" + +#: ../chirpui/shiftdialog.py:135 +msgid "Moved {count} memories" +msgstr "Перемещенно {count} записів пам'яті" + +#: ../chirpui/clone.py:35 +#, fuzzy +msgid "{vendor} {model} on {port}" +msgstr "{vendor} {model} на {port}" + +#: ../chirpui/clone.py:100 ../chirpui/clone.py:162 +msgid "Detect" +msgstr "Визначити" + +#: ../chirpui/clone.py:123 +msgid "Port" +msgstr "Порт" + +#: ../chirpui/clone.py:124 +msgid "Vendor" +msgstr "Виробник" + +#: ../chirpui/clone.py:125 +msgid "Model" +msgstr "Модель" + +#: ../chirpui/clone.py:138 +#, fuzzy +msgid "Radio" +msgstr "Радіостанція" + +#: ../chirpui/clone.py:166 +msgid "Unable to detect radio on {port}" +msgstr "Не вдалося виявити радіо на {port}" + +#: ../chirpui/clone.py:178 +msgid "Internal error: Unable to upload to {model}" +msgstr "Внутрішня помилка: не вдалося записати на {model}" + +#: ../chirpui/clone.py:226 +msgid "Clone failed: {error}" +msgstr "Помилка клонування: {error}" + +#: ../chirpui/dstaredit.py:40 +msgid "Callsign" +msgstr "Позивний" + +#: ../chirpui/dstaredit.py:124 +msgid "Your callsign" +msgstr "Ваш позивний" + +#: ../chirpui/dstaredit.py:132 +msgid "Repeater callsign" +msgstr "Позивний репітера" + +#: ../chirpui/dstaredit.py:140 +msgid "My callsign" +msgstr "Мій позивний" + +#: ../chirpui/dstaredit.py:170 ../chirpui/memedit.py:1365 +msgid "Downloading URCALL list" +msgstr "Завантаження списку URCALL" + +#: ../chirpui/dstaredit.py:174 ../chirpui/memedit.py:1377 +msgid "Downloading RPTCALL list" +msgstr "Завантаження списку RPTCALL" + +#: ../chirpui/dstaredit.py:178 +msgid "Downloading MYCALL list" +msgstr "Завантаження списку MYCALL" + +#: ../chirpui/editorset.py:87 +#, fuzzy +msgid "Memories" +msgstr "Пам'ять" + +#: ../chirpui/editorset.py:92 +msgid "D-STAR" +msgstr "D-STAR" + +#: ../chirpui/editorset.py:98 +msgid "Bank Names" +msgstr "Імена банків" + +#: ../chirpui/editorset.py:104 +msgid "Banks" +msgstr "Банки" + +#: ../chirpui/editorset.py:222 +msgid "The {vendor} {model} has multiple independent sub-devices" +msgstr "{vendor} {model} має кілька незалежних суб пристроїв" + +#: ../chirpui/editorset.py:225 +msgid "Choose one to import from:" +msgstr "Виберіть один для Імпорту з:" + +#: ../chirpui/editorset.py:230 +msgid "Cancelled" +msgstr "Скасовано" + +#: ../chirpui/editorset.py:235 +msgid "Internal Error" +msgstr "Внутрішня помилка" + +#: ../chirpui/editorset.py:248 +msgid "There were errors while opening {file}. The affected memories will not be importable!" +msgstr "Там були помилки при відкритті {file}. Порушена пам'ять не буде імпортована!" + +#: ../chirpui/editorset.py:260 +msgid "There was an error during import: {error}" +msgstr "Виникла помилка під час імпортування: {error}" + +#: ../chirpui/editorset.py:270 +msgid "Unsupported file type" +msgstr "Файл непідтримуваного типу" + +#: ../chirpui/editorset.py:286 ../chirpui/editorset.py:301 +#, fuzzy +msgid "There was an error during export: {error}" +msgstr "Виникла помилка під час експортування: {error}" + +#: ../chirpui/editorset.py:313 +msgid "Priming memory" +msgstr "Первинна пам'ять" + +#: ../chirpui/memedit.py:52 +msgid "Invalid value for this field" +msgstr "Неприпустиме значення для цього поля" + +#: ../chirpui/memedit.py:66 ../chirpui/memedit.py:97 ../chirpui/memedit.py:111 +#: ../chirpui/memedit.py:204 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:874 ../chirpui/memedit.py:931 +#: ../chirpui/memedit.py:1069 ../chirpui/memedit.py:1133 +msgid "Tone Mode" +msgstr "Тоновий режим" + +#: ../chirpui/memedit.py:67 ../chirpui/memedit.py:87 ../chirpui/memedit.py:103 +#: ../chirpui/memedit.py:214 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:220 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:875 ../chirpui/memedit.py:928 +#: ../chirpui/memedit.py:1070 +msgid "Tone" +msgstr "Тон" + +#: ../chirpui/memedit.py:68 ../chirpui/memedit.py:88 ../chirpui/memedit.py:104 +#: ../chirpui/memedit.py:210 ../chirpui/memedit.py:218 +#: ../chirpui/memedit.py:221 ../chirpui/memedit.py:310 +#: ../chirpui/memedit.py:876 ../chirpui/memedit.py:929 +#: ../chirpui/memedit.py:1066 +msgid "ToneSql" +msgstr "ToneSql" + +#: ../chirpui/memedit.py:69 ../chirpui/memedit.py:89 ../chirpui/memedit.py:105 +#: ../chirpui/memedit.py:211 ../chirpui/memedit.py:215 +#: ../chirpui/memedit.py:222 ../chirpui/memedit.py:306 +#: ../chirpui/memedit.py:877 ../chirpui/memedit.py:930 +#: ../chirpui/memedit.py:1059 +msgid "DTCS Code" +msgstr "DTCS код" + +#: ../chirpui/memedit.py:70 ../chirpui/memedit.py:90 ../chirpui/memedit.py:106 +#: ../chirpui/memedit.py:212 ../chirpui/memedit.py:216 +#: ../chirpui/memedit.py:223 ../chirpui/memedit.py:878 +#: ../chirpui/memedit.py:933 ../chirpui/memedit.py:1060 +msgid "DTCS Pol" +msgstr "DTCS Pol" + +#: ../chirpui/memedit.py:71 ../chirpui/memedit.py:91 ../chirpui/memedit.py:112 +#: ../chirpui/memedit.py:879 ../chirpui/memedit.py:932 +#: ../chirpui/memedit.py:1067 ../chirpui/memedit.py:1134 +msgid "Cross Mode" +msgstr "Кросрежим" + +#: ../chirpui/memedit.py:72 ../chirpui/memedit.py:92 ../chirpui/memedit.py:109 +#: ../chirpui/memedit.py:137 ../chirpui/memedit.py:205 +#: ../chirpui/memedit.py:249 ../chirpui/memedit.py:312 +#: ../chirpui/memedit.py:880 ../chirpui/memedit.py:934 +#: ../chirpui/memedit.py:1071 ../chirpui/memedit.py:1144 +msgid "Duplex" +msgstr "Дуплекс" + +#: ../chirpui/memedit.py:73 ../chirpui/memedit.py:93 ../chirpui/memedit.py:135 +#: ../chirpui/memedit.py:198 ../chirpui/memedit.py:226 +#: ../chirpui/memedit.py:250 ../chirpui/memedit.py:308 +#: ../chirpui/memedit.py:881 ../chirpui/memedit.py:935 +#: ../chirpui/memedit.py:1062 +msgid "Offset" +msgstr "Зсув" + +#: ../chirpui/memedit.py:74 ../chirpui/memedit.py:94 ../chirpui/memedit.py:107 +#: ../chirpui/memedit.py:882 ../chirpui/memedit.py:936 +#: ../chirpui/memedit.py:1061 ../chirpui/memedit.py:1132 +#: ../chirpui/memedit.py:1289 ../chirpui/memedit.py:1307 +#: ../chirpui/memedit.py:1317 +msgid "Mode" +msgstr "Режим" + +#: ../chirpui/memedit.py:75 ../chirpui/memedit.py:95 ../chirpui/memedit.py:108 +#: ../chirpui/memedit.py:296 ../chirpui/memedit.py:883 +#: ../chirpui/memedit.py:937 ../chirpui/memedit.py:1073 +#: ../chirpui/memedit.py:1136 ../chirpui/memedit.py:1140 +msgid "Power" +msgstr "Потужність" + +#: ../chirpui/memedit.py:76 ../chirpui/memedit.py:96 ../chirpui/memedit.py:110 +#: ../chirpui/memedit.py:140 ../chirpui/memedit.py:143 +#: ../chirpui/memedit.py:884 ../chirpui/memedit.py:938 +#: ../chirpui/memedit.py:1064 +msgid "Tune Step" +msgstr "Крок настройки" + +#: ../chirpui/memedit.py:77 ../chirpui/memedit.py:98 ../chirpui/memedit.py:885 +#: ../chirpui/memedit.py:939 ../chirpui/memedit.py:1072 +#: ../chirpui/memedit.py:1135 +msgid "Skip" +msgstr "Пропустити" + +#: ../chirpui/memedit.py:175 +msgid "Erasing memory {loc}" +msgstr "Стирання пам'яті {loc}" + +#: ../chirpui/memedit.py:236 +msgid "Unable to make changes to this model" +msgstr "Не вдається внести зміни до цієї моделі" + +#: ../chirpui/memedit.py:241 +msgid "Editing new item, taking defaults" +msgstr "Редагування нового елемента, беручи за замовчуванням" + +#: ../chirpui/memedit.py:257 +msgid "Bad value for {col}: {val}" +msgstr "Погане значення для {col}: {val}" + +#: ../chirpui/memedit.py:281 +msgid "Error setting memory" +msgstr "Помилка настройки пам'яті" + +#: ../chirpui/memedit.py:289 ../chirpui/memedit.py:356 +#: ../chirpui/memedit.py:1272 +msgid "Writing memory {number}" +msgstr "Запис пам'яті {number}" + +#: ../chirpui/memedit.py:361 +msgid "This operation requires moving all subsequent channels by one spot until an empty location is reached. This can take a LONG time. Are you sure you want to do this?" +msgstr "Ця операція вимагає переміщення всіх наступних каналів на одне місце до тих пір, поки пусте місце не буде досягнуто. Це може зайняти багато часу. Ви впевнені, що хочете це зробити?" + +#: ../chirpui/memedit.py:387 +msgid "Adding memory {number}" +msgstr "Додавання пам'яті {number}" + +#: ../chirpui/memedit.py:400 ../chirpui/memedit.py:913 +msgid "Erasing memory {number}" +msgstr "Стирання пам'яті {number}" + +#: ../chirpui/memedit.py:409 ../chirpui/memedit.py:518 +#: ../chirpui/memedit.py:564 ../chirpui/memedit.py:569 +#: ../chirpui/memedit.py:856 ../chirpui/memedit.py:1166 +msgid "Getting memory {number}" +msgstr "Отримання пам'яті {number}" + +#: ../chirpui/memedit.py:497 ../chirpui/memedit.py:508 +#: ../chirpui/memedit.py:556 +msgid "Moving memory from {old} to {new}" +msgstr "Переміщення пам'яті з {old} до {new}" + +#: ../chirpui/memedit.py:578 +msgid "Raw memory {number}" +msgstr "RAW пам'ять {number}" + +#: ../chirpui/memedit.py:582 ../chirpui/memedit.py:610 +#: ../chirpui/memedit.py:615 +msgid "Getting raw memory {number}" +msgstr "Отримання RAW пам'яті {number}" + +#: ../chirpui/memedit.py:587 +msgid "You can only diff two memories!" +msgstr "Ви можете порівняти тільки дві пам'яті!" + +#: ../chirpui/memedit.py:598 +msgid "Memory {number}" +msgstr "Пам'ять {number}" + +#: ../chirpui/memedit.py:604 +msgid "Diff of {a} and {b}" +msgstr "Порівняння {a} та {b}" + +#: ../chirpui/memedit.py:628 +msgid "Memories must be contiguous" +msgstr "Пам'ять має бути безперервна" + +#: ../chirpui/memedit.py:700 +msgid "Insert row above" +msgstr "Додати рядок зверху" + +#: ../chirpui/memedit.py:701 +msgid "Insert row below" +msgstr "Додати рядок знизу" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete" +msgstr "Видалити" + +#: ../chirpui/memedit.py:702 +#, fuzzy +msgid "Delete all" +msgstr "Видалити все" + +#: ../chirpui/memedit.py:703 +msgid "Delete (and shift up)" +msgstr "Видалити (та зсунути вгору)" + +#: ../chirpui/memedit.py:704 +#, fuzzy +msgid "Move up" +msgstr "Перемістити вгору" + +#: ../chirpui/memedit.py:705 +#, fuzzy +msgid "Move down" +msgstr "Перемістити вниз" + +#: ../chirpui/memedit.py:706 +#, fuzzy +msgid "Exchange memories" +msgstr "Обмін пам'яттю" + +#: ../chirpui/memedit.py:707 +#, fuzzy +msgid "Cut" +msgstr "Вирізати" + +#: ../chirpui/memedit.py:708 +#, fuzzy +msgid "Copy" +msgstr "Копіювати" + +#: ../chirpui/memedit.py:709 +#, fuzzy +msgid "Paste" +msgstr "Вставити" + +#: ../chirpui/memedit.py:710 +#, fuzzy +msgid "Show Raw Memory" +msgstr "Показати Raw пам'ять" + +#: ../chirpui/memedit.py:711 +#, fuzzy +msgid "Diff Raw Memories" +msgstr "Порівняти Raw пам'ять" + +#: ../chirpui/memedit.py:835 +msgid "Internal Error: Column {name} not found" +msgstr "Внутрішня помилка: не знайдено стовпець {name}" + +#: ../chirpui/memedit.py:863 +msgid "Getting channel {chan}" +msgstr "Отримання каналу {chan}" + +#: ../chirpui/memedit.py:952 +msgid "Internal Error: Invalid limit {number}" +msgstr "Внутрішня помилка: Неприпустиме обмеження {number}" + +#: ../chirpui/memedit.py:962 +msgid "Memory range:" +msgstr "Діапазон пам'яті:" + +#: ../chirpui/memedit.py:989 +msgid "Go" +msgstr "Іти" + +#: ../chirpui/memedit.py:1012 +msgid "Special Channels" +msgstr "Спеціальні канали" + +#: ../chirpui/memedit.py:1019 +msgid "Show Empty" +msgstr "Показувати порожні" + +#: ../chirpui/memedit.py:1198 +msgid "Cutting memory {number}" +msgstr "Отримання пам'яті {number}" + +#: ../chirpui/memedit.py:1232 +msgid "Overwrite?" +msgstr "Перезаписати?" + +#: ../chirpui/memedit.py:1237 +msgid "Overwrite location {number}?" +msgstr "Перезаписати місце {number}?" + +#: ../chirpui/memedit.py:1254 +msgid "Incompatible Memory" +msgstr "Несумісна пам'ять" + +#: ../chirpui/memedit.py:1257 +msgid "Pasted memory {number} is not compatible with this radio because:" +msgstr "Вставлена пам'ять {number} несумісна із цією радіостанцією тому що:" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1324 +msgid "URCALL" +msgstr "URCALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1325 +msgid "RPT1CALL" +msgstr "RPT1CALL" + +#: ../chirpui/memedit.py:1309 ../chirpui/memedit.py:1326 +msgid "RPT2CALL" +msgstr "RPT2CALL" + +#: ../chirpui/memedit.py:1310 ../chirpui/memedit.py:1327 +msgid "Digital Code" +msgstr "Цифровий код" + +#~ msgid "%i errors during open, check the debug log for details" +#~ msgstr "%i errors during open, check the debug log for details" diff --git a/py3syntax.patch b/py3syntax.patch new file mode 100644 index 0000000..a096fd2 --- /dev/null +++ b/py3syntax.patch @@ -0,0 +1,31 @@ +# HG changeset patch +# User mpoletiek +# Date 1606329087 21600 +# Wed Nov 25 12:31:27 2020 -0600 +# Branch py3 +# Node ID 055093bd90da079d927661375df69b1ac71346c6 +# Parent 68534f20c1418ae8e4cc09f3ff468d0375ba843a +[mq]: py3syntaxfix.patch + +diff --git a/chirp/drivers/vx6.py b/chirp/drivers/vx6.py +--- a/chirp/drivers/vx6.py ++++ b/chirp/drivers/vx6.py +@@ -871,5 +871,5 @@ + elif setting == "password": + newval = self._encode_chars(newval, 4) + setattr(_settings, setting, newval) +- except Exception, e: ++ except (Exception, e): + raise +diff --git a/chirp/ui/mainapp.py b/chirp/ui/mainapp.py +--- a/chirp/ui/mainapp.py ++++ b/chirp/ui/mainapp.py +@@ -1137,7 +1137,7 @@ + + query = "http://chirp.danplanet.com/query/rb/1.0/app_direct" \ + "?loc=%s&band=%s&dist=%s" % (loc, band, dist) +- print query ++ print(query) + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..b622882 --- /dev/null +++ b/pylintrc @@ -0,0 +1,249 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). +#disable= + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=text + +# Include message's id in output +include-ids=no + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_|dummy|foo + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,v,f,e,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=bar,baz,toto,tutu,tata + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__|_.* + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=20 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=25 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=40 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e7fcfa9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +six +PyGObject;python_version>="3.0" +pyserial +future diff --git a/rpttool b/rpttool new file mode 100755 index 0000000..60cc4c8 --- /dev/null +++ b/rpttool @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# +# Copyright 2009 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import serial +import commands + +from chirp import idrp, chirp_common + + +def open_device(): + try: + s = serial.Serial(port="/dev/icom", + baudrate=idrp.IDRPx000V.BAUD_RATE, + timeout=0.1) + except Exception, e: + print "Unable to open serial port %s: %s" % ("/dev/icom", e) + return None + + rp = idrp.IDRPx000V(s) + + return rp + + +def read_freq(): + rp = open_device() + if not rp: + return 0 + + try: + mem = rp.get_memory(0) + except Exception, e: + print "Unable to read memory from device: %s" % e + return 0 + + rp.pipe.close() + + return mem.freq + + +def _set_freq(rp): + try: + mem = rp.get_memory(0) + except Exception, e: + print "Unable to read memory from device: %s" % e + return False + + print "\nNew frequency [%s]: " % chirp_common.format_freq(mem.freq), + input = sys.stdin.readline().strip() + + if not input: + print "Frequency unchanged" + return False + + try: + mem.freq = chirp_common.parse_freq(input) + except Exception: + print "Invalid entry `%s'" % input + return False + + try: + rp.set_memory(mem) + except Exception, e: + print "Failed to set frequency to %s: %s" % \ + (chirp_common.format_freq(mem.freq), e) + return False + + print "Successfully set frequency to %s" % \ + chirp_common.format_freq(mem.freq) + return True + + +def set_freq(): + rp = open_device() + if not rp: + return + + try: + res = _set_freq(rp) + except Exception, e: + print "Unknown error while setting frequency: %s" % e + res = False + + rp.pipe.close() + return res + + +def main_menu(): + print "Looking for a repeater...", + sys.stdout.flush() + freq = read_freq() + if not freq: + return 1 + print "\r \r", + + cmd = "" + while cmd != "3": + print """ +KK7DS ID-RP* Frequency Tool +Current Setting: %s +-------------------------------- +1. Set repeater frequency +2. Re-read current frequency +3. Quit +-------------------------------- +> """ % chirp_common.format_freq(freq), + + cmd = sys.stdin.readline().strip() + + if cmd == "1": + if set_freq(): + freq = read_freq() + elif cmd == "2": + freq = read_freq() + elif cmd != "3": + print "Invalid entry" + + return 0 + + +if __name__ == "__main__": + if os.path.exists("tools/icomsio.sh"): + path = "tools/icomsio.sh" + else: + path = "icomsio.sh" + r = os.system(path) + if r: + sys.exit(r) + + if not os.geteuid() == 0: + print "Sorry, this must be run as root" + sys.exit(1) + sys.exit(main_menu()) diff --git a/run_all_tests.bat b/run_all_tests.bat new file mode 100644 index 0000000..399ed54 --- /dev/null +++ b/run_all_tests.bat @@ -0,0 +1,3 @@ +@echo off +python tests\run_tests.py +python tools\cpep8.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ba216f1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_rpm] +requires = pyserial +packager = Dan Smith +description = A frequency tool for Icom D-STAR Repeaters +vendor = KK7DS diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e403d91 --- /dev/null +++ b/setup.py @@ -0,0 +1,163 @@ +from __future__ import print_function + +import sys +import glob +import os + +from chirp import CHIRP_VERSION +import chirp +from chirp import directory + +directory.safe_import_drivers() + + +def staticify_chirp_module(): + import chirp + + with open("chirp/__init__.py", "w") as init: + print("CHIRP_VERSION = \"%s\"" % CHIRP_VERSION, file=init) + print("__all__ = %s\n" % str(chirp.__all__), file=init) + + print("Set chirp/__init__.py::__all__ = %s" % str(chirp.__all__)) + + +def staticify_drivers_module(): + import chirp.drivers + + with file("chirp/drivers/__init__.py", "w") as init: + print("__all__ = %s\n" % str(chirp.drivers.__all__), file=init) + + print("Set chirp/drivers/__init__.py::__all__ = %s" % str( + chirp.drivers.__all__)) + + +def win32_build(): + from distutils.core import setup + import py2exe + + try: + # if this doesn't work, try import modulefinder + import py2exe.mf as modulefinder + import win32com + for p in win32com.__path__[1:]: + modulefinder.AddPackagePath("win32com", p) + for extra in ["win32com.shell"]: # ,"win32com.mapi" + __import__(extra) + m = sys.modules[extra] + for p in m.__path__[1:]: + modulefinder.AddPackagePath(extra, p) + except ImportError: + # no build path setup, no worries. + pass + + staticify_chirp_module() + staticify_drivers_module() + + opts = { + "py2exe": { + "includes": "pango,atk,gobject,cairo,pangocairo," + + "win32gui,win32com,win32com.shell," + + "email.iterators,email.generator,gio", + + "compressed": 1, + "optimize": 2, + "bundle_files": 3, + # "packages": "" + } + } + + mods = [] + for mod in chirp.__all__: + mods.append("chirp.%s" % mod) + for mod in chirp.drivers.__all__: + mods.append("chirp.drivers.%s" % mod) + opts["py2exe"]["includes"] += ("," + ",".join(mods)) + + setup( + zipfile=None, + windows=[{'script': "chirpw", + 'icon_resources': [(0x0004, 'share/chirp.ico')], + }], + options=opts) + + +def macos_build(): + from setuptools import setup + import shutil + + APP = ['chirp-%s.py' % CHIRP_VERSION] + shutil.copy("chirpw", APP[0]) + DATA_FILES = [('../Frameworks', ['/opt/local/lib/libpangox-1.0.dylib']), + ('../Resources/', ['/opt/local/lib/pango']), + ] + OPTIONS = {'argv_emulation': True, "includes": "gtk,atk,pangocairo,cairo"} + + setup( + app=APP, + data_files=DATA_FILES, + options={'py2app': OPTIONS}, + setup_requires=['py2app'], + ) + + EXEC = 'bash ./build/macos/make_pango.sh ' + \ + '/opt/local dist/chirp-%s.app' % CHIRP_VERSION + # print "exec string: %s" % EXEC + os.system(EXEC) + + +def default_build(): + from distutils.core import setup + from glob import glob + + os.system("make -C locale clean all") + + desktop_files = glob("share/*.desktop") + # form_files = glob("forms/*.x?l") + image_files = glob("images/*") + _locale_files = glob("locale/*/LC_MESSAGES/CHIRP.mo") + stock_configs = glob("stock_configs/*") + + locale_files = [] + for f in _locale_files: + locale_files.append(("share/chirp/%s" % os.path.dirname(f), [f])) + + print("LOC: %s" % str(locale_files)) + + xsd_files = glob("chirp*.xsd") + + setup( + name="chirp", + packages=["chirp", "chirp.drivers", "chirp.ui", "tests", "tests.unit", + "chirp.wxui"], + version=CHIRP_VERSION, + scripts=["chirpw", "rpttool", "chirpwx.py"], + data_files=[('share/applications', desktop_files), + ('share/chirp/images', image_files), + ('share/chirp', xsd_files), + ('share/doc/chirp', ['COPYING']), + ('share/pixmaps', ['share/chirp.png']), + ('share/man/man1', ["share/chirpw.1"]), + ('share/chirp/stock_configs', stock_configs), + ] + locale_files) + + +def nuke_manifest(*files): + for i in ["MANIFEST", "MANIFEST.in"]: + if os.path.exists(i): + os.remove(i) + + if not files: + return + + f = file("MANIFEST.in", "w") + for fn in files: + print(fn, file=f) + f.close() + + +if sys.platform == "darwin": + macos_build() +elif sys.platform == "win32": + win32_build() +else: + default_build() diff --git a/share/chirp.desktop b/share/chirp.desktop new file mode 100644 index 0000000..966e2a0 --- /dev/null +++ b/share/chirp.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=CHIRP +GenericName=Radio Programming Tool +Comment=Program amateur radios +Icon=chirp +Exec=chirpw %F +Terminal=false +MimeType=inode/directory +Categories=Utility;HamRadio +Keywords=Hamradio;Programming;Handheld;Radio;Amateur;Programmer +StartupNotify=true diff --git a/share/chirp.icns b/share/chirp.icns new file mode 100644 index 0000000..2ac96be Binary files /dev/null and b/share/chirp.icns differ diff --git a/share/chirp.ico b/share/chirp.ico new file mode 100755 index 0000000..4006ff9 Binary files /dev/null and b/share/chirp.ico differ diff --git a/share/chirp.png b/share/chirp.png new file mode 100644 index 0000000..fe57c1a Binary files /dev/null and b/share/chirp.png differ diff --git a/share/chirp.svg b/share/chirp.svg new file mode 100644 index 0000000..38d0d05 --- /dev/null +++ b/share/chirp.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + c h i r p + + + + + + + + diff --git a/share/chirpw.1 b/share/chirpw.1 new file mode 100644 index 0000000..ba9b9e8 --- /dev/null +++ b/share/chirpw.1 @@ -0,0 +1,46 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH CHIRP 1 "February 12, 2011" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.SH NAME +chirpw \- A tool for programming two-way radio equipment +.SH SYNOPSIS +.B chirpw +.RI [ options ] +.br +.SH DESCRIPTION +This manual page documents briefly the +.B chirpw +command. +.PP +\fBchirpw\fP is a tool for programming two-way radio equipment +It provides a generic user interface to the programming data and +process that can drive many radio models under the hood. +.SH OPTIONS +This program follows the usual GNU command line syntax, with long +options starting with two dashes (`--'). +A summary of options is included below. +.TP +.B \-\-help +Show summary of options. +.TP +.B \-\-profile +Enable Profiling. +.SH AUTHOR +chirpw was written by Dan Smith. +.PP +This manual page was written by Dan Smith (with help from Steve Conklin), +for the Debian project (and may be used by others). diff --git a/share/contrib/chirp.rnc b/share/contrib/chirp.rnc new file mode 100644 index 0000000..6ea2715 --- /dev/null +++ b/share/contrib/chirp.rnc @@ -0,0 +1,28 @@ +# +# CHIRP XML Schema +# Copyright 2008 Dan Smith +# + +include "chirp_memory.rnc" +include "chirp_banks.rnc" + +start = radio + +radio = element radio { + attribute version { chirpSchemaVersionType }?, + comment?, + memories, + banks +} + +comment = element comment { xsd:string }? + +memories = element memories { + element memory { memoryType }* +} + +banks = element banks { + element bank { bankType }* +} + +chirpSchemaVersionType = xsd:string { pattern = "[0-9][0-9]*.[0-9][0-9]*.[0-9]{1,4}" } diff --git a/share/contrib/chirp.rng b/share/contrib/chirp.rng new file mode 100644 index 0000000..d6020e5 --- /dev/null +++ b/share/contrib/chirp.rng @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [0-9][0-9]*.[0-9][0-9]*.[0-9]{1,4} + + + diff --git a/share/contrib/chirp_banks.rnc b/share/contrib/chirp_banks.rnc new file mode 100644 index 0000000..d832539 --- /dev/null +++ b/share/contrib/chirp_banks.rnc @@ -0,0 +1,3 @@ +bankType = + attribute id { xsd:nonNegativeInteger }, + attribute label { xsd:string } diff --git a/share/contrib/chirp_banks.rng b/share/contrib/chirp_banks.rng new file mode 100644 index 0000000..fa25081 --- /dev/null +++ b/share/contrib/chirp_banks.rng @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/share/contrib/chirp_memory.rnc b/share/contrib/chirp_memory.rnc new file mode 100644 index 0000000..ad7ee2e --- /dev/null +++ b/share/contrib/chirp_memory.rnc @@ -0,0 +1,64 @@ +memoryType = + attribute location { xsd:nonNegativeInteger }?, + shortName, + longName?, + frequency, + SquelchList, + squelchSetting?, + duplex, + offset, + mode, + tuningStep, + skip?, + bank?, + dv? + +shortName = element shortName { xsd:string { pattern = "[A-Z0-9/ >\-]{0,6}" } } + +frequencyType = + attribute units { "Hz" | "kHz" | "MHz" | "GHz" }, + xsd:decimal + +longName = element longName { xsd:string { pattern = "[.A-Za-z0-9/ >\-]{0,16}" } } + +frequency = element frequency { frequencyType } + +SquelchList = + element squelch { squelchType }?, + element squelch { squelchType }?, + element squelch { squelchType }? + +squelchType = + element tone { xsd:decimal { minInclusive = "67.0" maxInclusive = "254.1" } }?, # could also use enumeration + element code { xsd:positiveInteger }?, + element polarity { xsd:string { pattern = "[RN]{2}" } }?, + attribute id { text }?, + attribute type { text }? + +offset = element offset { frequencyType } + +tuningStep = element tuningStep { frequencyType } + +squelchSetting = element squelchSetting { xsd:string } + +duplex = element duplex { "positive" | "negative" | "none" } + +mode = element mode { "FM" | "NFM" | "WFM" | "AM" | "NAM" | "DV" } + +dv = element dv { + element urcall { callsignType }, + element rpt1call { callsignType }, + element rpt2call { callsignType }, + element digitalCode { digitalCodeType }? +} + +callsignType = xsd:string { pattern = "[A-Z0-9/ ]*" } + +digitalCodeType = xsd:integer { minInclusive = "0" } + +skip = element skip { "S" | "P" | "" } + +bank = element bank { + attribute bankId { xsd:nonNegativeInteger }, + attribute bankIndex { xsd:nonNegativeInteger }? +} diff --git a/share/contrib/chirp_memory.rng b/share/contrib/chirp_memory.rng new file mode 100644 index 0000000..0d88eac --- /dev/null +++ b/share/contrib/chirp_memory.rng @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [A-Z0-9/ >\-]{0,6} + + + + + + + Hz + kHz + MHz + GHz + + + + + + + + [.A-Za-z0-9/ >\-]{0,16} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 67.0 + 254.1 + + + + + + + + + + + + + [RN]{2} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + positive + negative + none + + + + + + + FM + NFM + WFM + AM + NAM + DV + + + + + + + + + + + + + + + + + + + + + + + + [A-Z0-9/ ]* + + + + + 0 + + + + + + S + P + + + + + + + + + + + + + + + + + diff --git a/share/make_supported.py b/share/make_supported.py new file mode 100755 index 0000000..67ecebd --- /dev/null +++ b/share/make_supported.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python + +import sys +import serial + +sys.path.insert(0, ".") +sys.path.insert(0, "..") + +tmp = sys.stdout +sys.stdout = sys.stderr +from chirp import * +from chirp.drivers import * +sys.stdout = tmp + +RF = chirp_common.RadioFeatures() +KEYS = [x for x in sorted(RF.__dict__.keys()) + if "_" in x and not x.startswith("_")] + +RADIO_TYPES = { + 'Clone': chirp_common.CloneModeRadio, + 'File': chirp_common.FileBackedRadio, + 'Live': chirp_common.LiveRadio, +} + + +counter = 0 + +def radio_type(radio): + for k, v in RADIO_TYPES.items(): + if isinstance(radio, v): + return k + return "" + + +def supported_row(radio): + global counter + counter += 1 + odd = counter % 2 + + row = '' % (odd and "odd" or "even", + radio.VENDOR, + radio.MODEL, + radio.VARIANT) + row += "%s %s %s\n" % ( + 'row%04i' % counter, + 'row%04i' % counter, + radio.VENDOR, radio.MODEL, radio.VARIANT) + rf = radio.get_features() + for key in KEYS: + value = rf.__dict__[key] + if key == "valid_bands": + value = ["%s-%s MHz" % (chirp_common.format_freq(x), + chirp_common.format_freq(y)) + for x, y in value] + + if key in ["valid_bands", "valid_modes", "valid_power_levels", + "valid_tuning_steps"]: + try: + value = ", ".join([str(x) for x in value + if not str(x).startswith("?")]) + except Exception, e: + raise + + if key == "memory_bounds": + value = "%i-%i" % value + + if key == "requires_call_lists": + if "DV" not in rf.valid_modes: + value = None + elif value: + value = "Required" + else: + value = "Optional" + + if value is None: + row += 'N/A' % key + elif isinstance(value, bool): + row += '%s' % \ + (key, + value, + value and "Yes" or "No") + else: + row += '%s' % (key, value) + row += '%s' % radio_type(radio) + row += "\n" + return row + + +def header_row(): + row = "" + row += "Radio\n" + for key in KEYS: + Key = key.split("_", 1)[1].title().replace("_", " ") + row += '%s' % (RF.get_doc(key), Key) + row += 'Type\n' + row += "\n" + return row + + +dest = sys.stdout +if len(sys.argv) > 1: + dest = open(sys.argv[1], 'w') + + +def output(string): + dest.write(string + '\n') + + +output(""" + + +""") + +models = {"Icom": [], + "Kenwood": [], + "Yaesu": [], + "Alinco": [], + "Baofeng": [], + "z_Other": [], + } + +models = [] + +exclude = [directory.DRV_TO_RADIO["Icom_7200"]] + +for radio in directory.DRV_TO_RADIO.values(): + if radio in exclude: + continue + + models.append(radio) + for alias in radio.ALIASES: + class DynamicRadioAlias(radio): + VENDOR = alias.VENDOR + MODEL = alias.MODEL + VARIANT = alias.VARIANT + models.append(DynamicRadioAlias) + + +def get_key(rc): + return '%s %s %s' % (rc.VENDOR, rc.MODEL, rc.VARIANT) + +for radio in sorted(models, cmp=lambda a, b: get_key(a) < get_key(b) and -1 or 1): + if counter % 10 == 0: + output(header_row()) + _radio = radio(None) + if _radio.get_features().has_sub_devices: + for __radio in _radio.get_sub_devices(): + output(supported_row(__radio)) + else: + output(supported_row(_radio)) diff --git a/stock_configs/DE Freenet Frequencies.csv b/stock_configs/DE Freenet Frequencies.csv new file mode 100644 index 0000000..e9ec9bc --- /dev/null +++ b/stock_configs/DE Freenet Frequencies.csv @@ -0,0 +1,7 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,FRNET1,149.025000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +2,FRNET2,149.037500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +3,FRNET3,149.050000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +4,FRNET4,149.087500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +5,FRNET5,149.100000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +6,FRNET6,149.112500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, diff --git a/stock_configs/EU LPD and PMR Channels.csv b/stock_configs/EU LPD and PMR Channels.csv new file mode 100644 index 0000000..19d9e14 --- /dev/null +++ b/stock_configs/EU LPD and PMR Channels.csv @@ -0,0 +1,86 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,LPD 01,433.075000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +2,LPD 02,433.100000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +3,LPD 03,433.125000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +4,LPD 04,433.150000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +5,LPD 05,433.175000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +6,LPD 06,433.200000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +7,LPD 07,433.225000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +8,LPD 08,433.250000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +9,LPD 09,433.275000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +10,LPD 10,433.300000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +11,LPD 11,433.325000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +12,LPD 12,433.350000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +13,LPD 13,433.375000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +14,LPD 14,433.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +15,LPD 15,433.425000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +16,LPD 16,433.450000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +17,LPD 17,433.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +18,LPD 18,433.500000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +19,LPD 19,433.525000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +20,LPD 20,433.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +21,LPD 21,433.575000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +22,LPD 22,433.600000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +23,LPD 23,433.625000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +24,LPD 24,433.650000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +25,LPD 25,433.675000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +26,LPD 26,433.700000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +27,LPD 27,433.725000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +28,LPD 28,433.750000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +29,LPD 29,433.775000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +30,LPD 30,433.800000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +31,LPD 31,433.825000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +32,LPD 32,433.850000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +33,LPD 33,433.875000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +34,LPD 34,433.900000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +35,LPD 35,433.925000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +36,LPD 36,433.950000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +37,LPD 37,433.975000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +38,LPD 38,434.000000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +39,LPD 39,434.025000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +40,LPD 40,434.050000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +41,LPD 41,434.075000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +42,LPD 42,434.100000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +43,LPD 43,434.125000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +44,LPD 44,434.150000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +45,LPD 45,434.175000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +46,LPD 46,434.200000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +47,LPD 47,434.225000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +48,LPD 48,434.250000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +49,LPD 49,434.275000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +50,LPD 50,434.300000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +51,LPD 51,434.325000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +52,LPD 52,434.350000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +53,LPD 53,434.375000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +54,LPD 54,434.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +55,LPD 55,434.425000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +56,LPD 56,434.450000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +57,LPD 57,434.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +58,LPD 58,434.500000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +59,LPD 59,434.525000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +60,LPD 60,434.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +61,LPD 61,434.575000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +62,LPD 62,434.600000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +63,LPD 63,434.625000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +64,LPD 64,434.650000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +65,LPD 65,434.675000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +66,LPD 66,434.700000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +67,LPD 67,434.725000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +68,LPD 68,434.750000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +69,LPD 69,434.775000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +71,PMR 01,446.006250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +72,PMR 02,446.018750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +73,PMR 03,446.031250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +74,PMR 04,446.043750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +75,PMR 05,446.056250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +76,PMR 06,446.068750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +77,PMR 07,446.081250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +78,PMR 08,446.093750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +81,PMR 09,446.106250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +82,PMR 10,446.118750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +83,PMR 11,446.131250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +84,PMR 12,446.143750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +85,PMR 13,446.156250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +86,PMR 14,446.168750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +87,PMR 15,446.181250,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +88,PMR 16,446.193750,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, diff --git a/stock_configs/FR Marine VHF Channels.csv b/stock_configs/FR Marine VHF Channels.csv new file mode 100644 index 0000000..da8bac7 --- /dev/null +++ b/stock_configs/FR Marine VHF Channels.csv @@ -0,0 +1,58 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,SEA 01,160.650000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +2,SEA 02,160.700000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +3,SEA 03,160.750000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +4,SEA 04,160.800000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +5,SEA 05,160.850000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +6,SEA 06,156.300000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +7,SEA 07,160.950000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +8,SEA 08,156.400000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +9,SEA 09,156.450000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +10,SEA 10,156.500000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +11,SEA 11,156.550000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +12,SEA 12,156.600000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +13,SEA 13,156.650000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +14,SEA 14,156.700000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +15,SEA 15,156.750000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +16,SEA 16,156.800000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +17,SEA 17,156.850000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +18,SEA 18,161.500000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +19,SEA 19,161.550000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +20,SEA 20,161.600000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +21,SEA 21,161.650000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +22,SEA 22,161.700000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +23,SEA 23,161.750000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +24,SEA 24,161.800000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +25,SEA 25,161.850000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +26,SEA 26,161.900000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +27,SEA 27,161.950000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +28,SEA 28,162.000000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +29,SEA 60,160.625000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +30,SEA 61,160.675000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +31,SEA 62,160.725000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +32,SEA 63,160.775000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +33,SEA 64,160.825000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +34,SEA 65,160.875000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +35,SEA 66,160.925000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +36,SEA 67,156.375000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +37,SEA 68,156.425000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +38,SEA 69,156.475000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +39,SEA 70,156.525000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +40,SEA 71,156.575000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +41,SEA 72,156.625000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +42,SEA 73,156.675000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +43,SEA 74,156.725000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +44,SEA 75,156.775000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +45,SEA 76,156.825000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +46,SEA 77,156.875000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +47,SEA 78,161.525000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +48,SEA 79,161.575000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +49,SEA 80,161.625000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +50,SEA 81,161.675000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +51,SEA 82,161.725000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +52,SEA 83,161.775000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +53,SEA 84,161.825000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +54,SEA 85,161.875000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +55,SEA 86,161.925000,-,4.600000,,88.5,88.5,023,NN,FM,5.00,,,,, +56,SEA 87,157.375000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +57,SEA 88,157.425000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, diff --git a/stock_configs/KDR444.csv b/stock_configs/KDR444.csv new file mode 100644 index 0000000..7ab0e42 --- /dev/null +++ b/stock_configs/KDR444.csv @@ -0,0 +1,9 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,KDR444 1,444.600,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +2,KDR444 2,444.650,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +3,KDR444 3,444.800,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +4,KDR444 4,444.825,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +5,KDR444 5,444.850,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +6,KDR444 6,444.875,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +7,KDR444 7,444.925,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, +8,KDR444 8,444.975,,0.600000,,88.5,88.5,023,NN,NFM,6.25,,,,, diff --git a/stock_configs/NOAA Weather Alert.csv b/stock_configs/NOAA Weather Alert.csv new file mode 100644 index 0000000..9e355b5 --- /dev/null +++ b/stock_configs/NOAA Weather Alert.csv @@ -0,0 +1,11 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,WX1PA7,162.550000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +2,WX2PA1,162.400000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +3,WX3PA4,162.475000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +4,WX4PA2,162.425000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +5,WX5PA3,162.450000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +6,WX6PA5,162.500000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +7,WX7PA6,162.525000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +8,WX8,161.650000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +9,WX9,161.775000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +10,WX10,163.275000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, diff --git a/stock_configs/UK Business Radio Simple Light Frequencies.csv b/stock_configs/UK Business Radio Simple Light Frequencies.csv new file mode 100644 index 0000000..803995b --- /dev/null +++ b/stock_configs/UK Business Radio Simple Light Frequencies.csv @@ -0,0 +1,16 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,BRSL1,77.687500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +2,BRSL2,86.337500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +3,BRSL3,86.350000,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +4,BRSL4,86.362500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +5,BRSL5,86.375000,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +6,BRSL6,164.050000,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +7,BRSL7,164.062500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +8,BRSL8,169.087500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +9,BRSL9,169.312500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +10,BRSL10,173.050000,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +11,BRSL11,173.062500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +12,BRSL12,173.087500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +13,BRSL13,449.312500,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +14,BRSL14,449.400000,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +15,BRSL15,449.475000,,0.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, diff --git a/stock_configs/US 60 meter channels (Center).csv b/stock_configs/US 60 meter channels (Center).csv new file mode 100644 index 0000000..df22f65 --- /dev/null +++ b/stock_configs/US 60 meter channels (Center).csv @@ -0,0 +1,6 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,60m CH1,5.332000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +2,60m CH2,5.348000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +3,60m CH3,5.358500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +4,60m CH4,5.373000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +5,60m CH5,5.405000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, diff --git a/stock_configs/US 60 meter channels (Dial).csv b/stock_configs/US 60 meter channels (Dial).csv new file mode 100644 index 0000000..ed016f3 --- /dev/null +++ b/stock_configs/US 60 meter channels (Dial).csv @@ -0,0 +1,6 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,60m CH1,5.330500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +2,60m CH2,5.346500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +3,60m CH3,5.357000,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +4,60m CH4,5.371500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, +5,60m CH5,5.403500,,0.600000,,88.5,88.5,023,NN,USB,5.00,,,,, diff --git a/stock_configs/US CA Railroad Channels.csv b/stock_configs/US CA Railroad Channels.csv new file mode 100644 index 0000000..fe5f4e6 --- /dev/null +++ b/stock_configs/US CA Railroad Channels.csv @@ -0,0 +1,187 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,Tstep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,AAR002,159.810000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +2,AAR003,159.930000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +3,AAR004,160.050000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +4,AAR005,160.185000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +5,AAR006,160.200000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +6,AAR007,160.215000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +7,AAR008,160.230000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +8,AAR009,160.245000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +9,AAR010,160.260000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +10,AAR011,160.275000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +11,AAR012,160.290000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +12,AAR013,160.305000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +13,AAR014,160.320000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +14,AAR015,160.335000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +15,AAR016,160.350000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +16,AAR017,160.365000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +17,AAR018,160.380000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +18,AAR019,160.395000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +19,AAR020,160.410000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +20,AAR021,160.425000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +21,AAR022,160.440000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +22,AAR023,160.455000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +23,AAR024,160.470000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +24,AAR025,160.485000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +25,AAR026,160.500000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +26,AAR027,160.515000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +27,AAR028,160.530000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +28,AAR029,160.545000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +29,AAR030,160.560000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +30,AAR031,160.575000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +31,AAR032,160.590000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +32,AAR033,160.605000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +33,AAR034,160.620000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +34,AAR035,160.635000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +35,AAR036,160.650000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +36,AAR037,160.665000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +37,AAR038,160.680000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +38,AAR039,160.695000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +39,AAR040,160.710000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +40,AAR041,160.725000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +41,AAR042,160.740000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +42,AAR043,160.755000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +43,AAR044,160.770000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +44,AAR045,160.785000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +45,AAR046,160.800000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +46,AAR047,160.815000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +47,AAR048,160.830000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +48,AAR049,160.845000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +49,AAR050,160.860000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +50,AAR051,160.875000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +51,AAR052,160.890000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +52,AAR053,160.905000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +53,AAR054,160.920000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +54,AAR055,160.935000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +55,AAR056,160.950000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +56,AAR057,160.965000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +57,AAR058,160.980000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +58,AAR059,160.995000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +59,AAR060,161.010000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +60,AAR061,161.025000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +61,AAR062,161.040000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +62,AAR063,161.055000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +63,AAR064,161.070000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +64,AAR065,161.085000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +65,AAR066,161.100000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +66,AAR067,161.115000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +67,AAR068,161.130000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +68,AAR069,161.145000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +69,AAR070,161.160000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +70,AAR071,161.175000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +71,AAR072,161.190000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +72,AAR073,161.205000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +73,AAR074,161.220000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +74,AAR075,161.235000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +75,AAR076,161.250000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +76,AAR077,161.265000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +77,AAR078,161.280000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +78,AAR079,161.295000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +79,AAR080,161.310000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +80,AAR081,161.325000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +81,AAR082,161.340000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +82,AAR083,161.355000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +83,AAR084,161.370000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +84,AAR085,161.385000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +85,AAR086,161.400000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +86,AAR087,161.415000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +87,AAR088,161.430000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +88,AAR089,161.445000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +89,AAR090,161.460000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +90,AAR091,161.475000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +91,AAR092,161.490000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +92,AAR093,161.505000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +93,AAR094,161.520000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +94,AAR095,161.535000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +95,AAR096,161.550000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +96,AAR097,161.565000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +97,AAR107,160.222500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +98,AAR108,160.237500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +99,AAR109,160.252500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +100,AAR110,160.267500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +101,AAR111,160.282500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +102,AAR112,160.297500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +103,AAR113,160.312500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +104,AAR114,160.327500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +105,AAR115,160.342500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +106,AAR116,160.357500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +107,AAR117,160.372500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +108,AAR118,160.387500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +109,AAR119,160.402500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +110,AAR120,160.417500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +111,AAR121,160.432500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +112,AAR122,160.447500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +113,AAR123,160.462500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +114,AAR124,160.477500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +115,AAR125,160.492500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +116,AAR126,160.507500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +117,AAR127,160.522500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +118,AAR128,160.537500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +119,AAR129,160.552500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +120,AAR130,160.567500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +121,AAR131,160.582500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +122,AAR132,160.597500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +123,AAR133,160.612500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +124,AAR134,160.627500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +125,AAR135,160.642500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +126,AAR136,160.657500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +127,AAR137,160.672500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +128,AAR138,160.687500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +129,AAR139,160.702500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +130,AAR140,160.717500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +131,AAR141,160.732500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +132,AAR142,160.747500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +133,AAR143,160.762500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +134,AAR144,160.777500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +135,AAR145,160.792500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +136,AAR146,160.807500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +137,AAR147,160.822500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +138,AAR148,160.837500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +139,AAR149,160.852500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +140,AAR150,160.867500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +141,AAR151,160.882500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +142,AAR152,160.897500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +143,AAR153,160.912500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +144,AAR154,160.927500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +145,AAR155,160.942500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +146,AAR156,160.957500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +147,AAR157,160.972500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +148,AAR158,160.987500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +149,AAR159,161.002500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +150,AAR160,161.017500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +151,AAR161,161.032500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +152,AAR162,161.047500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +153,AAR163,161.062500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +154,AAR164,161.077500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +155,AAR165,161.092500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +156,AAR166,161.107500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +157,AAR167,161.122500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +158,AAR168,161.137500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +159,AAR169,161.152500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +160,AAR170,161.167500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +161,AAR171,161.182500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +162,AAR172,161.197500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +163,AAR173,161.212500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +164,AAR174,161.227500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +165,AAR175,161.242500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +166,AAR176,161.257500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +167,AAR177,161.272500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +168,AAR178,161.287500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +169,AAR179,161.302500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +170,AAR180,161.317500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +171,AAR181,161.332500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +172,AAR182,161.347500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +173,AAR183,161.362500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +174,AAR184,161.377500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +175,AAR185,161.392500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +176,AAR186,161.407500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +177,AAR187,161.422500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +178,AAR188,161.437500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +179,AAR189,161.452500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +180,AAR190,161.467500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +181,AAR191,161.482500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +182,AAR192,161.497500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +183,AAR193,161.512500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +184,AAR194,161.527500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +185,AAR195,161.542500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +186,AAR196,161.557500,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, diff --git a/stock_configs/US Calling Frequencies.csv b/stock_configs/US Calling Frequencies.csv new file mode 100644 index 0000000..d597d86 --- /dev/null +++ b/stock_configs/US Calling Frequencies.csv @@ -0,0 +1,5 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,6m Call,52.525000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +2,2m Call,146.520000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +3,220 Call,223.500000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +4,70cm Call,446.000000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, diff --git a/stock_configs/US FRS and GMRS Channels.csv b/stock_configs/US FRS and GMRS Channels.csv new file mode 100644 index 0000000..76a58ac --- /dev/null +++ b/stock_configs/US FRS and GMRS Channels.csv @@ -0,0 +1,53 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,FRS 1,462.562500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +2,FRS 2,462.587500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +3,FRS 3,462.612500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +4,FRS 4,462.637500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +5,FRS 5,462.662500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +6,FRS 6,462.687500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +7,FRS 7,462.712500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +8,FRS 8,467.562500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +9,FRS 9,467.587500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +10,FRS 10,467.612500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +11,FRS 11,467.637500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +12,FRS 12,467.662500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +13,FRS 13,467.687500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +14,FRS 14,467.712500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +15,FRS 15,462.550000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +16,FRS 16,462.575000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +17,FRS 17,462.600000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +18,FRS 18,462.625000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +19,FRS 19,462.650000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +20,FRS 20,462.675000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +21,FRS 21,462.700000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +22,FRS 22,462.725000,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +23,GMRS 1,462.562500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +24,GMRS 2,462.587500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +25,GMRS 3,462.612500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +26,GMRS 4,462.637500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +27,GMRS 5,462.662500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +28,GMRS 6,462.687500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +29,GMRS 7,462.712500,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +30,GMRS 8,467.562500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +31,GMRS 9,467.587500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +32,GMRS 10,467.612500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +33,GMRS 11,467.637500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +34,GMRS 12,467.662500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +35,GMRS 13,467.687500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +36,GMRS 14,467.712500,,5.000000,,88.5,88.5,023,NN,NFM,12.50,,,,, +37,GMRS 15,462.550000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +38,GMRS 16,462.575000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +39,GMRS 17,462.600000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +40,GMRS 18,462.625000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +41,GMRS 19,462.650000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +42,GMRS 20,462.675000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +43,GMRS 21,462.700000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +44,GMRS 22,462.725000,,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +45,GMRS 550/15R,462.550000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +46,GMRS 575/16R,462.575000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +47,GMRS 600/17R,462.600000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +48,GMRS 625/18R,462.625000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +49,GMRS 650/19R,462.650000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +50,GMRS 675/20R,462.675000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +51,GMRS 700/21R,462.700000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, +52,GMRS 725/22R,462.725000,+,5.000000,,88.5,88.5,023,NN,FM,12.50,,,,, diff --git a/stock_configs/US MURS Channels.csv b/stock_configs/US MURS Channels.csv new file mode 100644 index 0000000..e6fed17 --- /dev/null +++ b/stock_configs/US MURS Channels.csv @@ -0,0 +1,6 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,MURS 1,151.820000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +2,MURS 2,151.880000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +3,MURS 3,151.940000,,0.000000,,88.5,88.5,023,NN,NFM,5.00,,,,, +4,Blue Dot,154.570000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, +5,Green Dot,154.600000,,0.000000,,88.5,88.5,023,NN,FM,5.00,,,,, diff --git a/stock_configs/US Marine VHF Channels.csv b/stock_configs/US Marine VHF Channels.csv new file mode 100644 index 0000000..21a6f79 --- /dev/null +++ b/stock_configs/US Marine VHF Channels.csv @@ -0,0 +1,61 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Comment,URCALL,RPT1CALL,RPT2CALL +1,SEA 01,160.650000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +2,SEA 02,160.700000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +3,SEA 03,160.750000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +4,SEA 04,160.800000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +5,SEA 05,160.850000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +6,SEA 06,156.300000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +7,SEA 07,160.950000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +8,SEA 08,156.400000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +9,SEA 09,156.450000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +10,SEA 10,156.500000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +11,SEA 11,156.550000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +12,SEA 12,156.600000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +13,SEA 13,156.650000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +14,SEA 14,156.700000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +15,SEA 15,156.750000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +16,SEA 16,156.800000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +17,SEA 17,156.850000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +18,SEA 18,161.500000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +19,SEA 19,161.550000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +20,SEA 20,161.600000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +21,SEA 21,161.650000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +22,SEA 22,161.700000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +23,SEA 23,161.750000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +24,SEA 24,161.800000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +25,SEA 25,161.850000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +26,SEA 26,161.900000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +27,SEA 27,161.950000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +28,SEA 28,162.000000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +29,SEA 60,160.625000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +30,SEA 61,160.675000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +31,SEA 62,160.725000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +32,SEA 63,160.775000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +33,SEA 64,160.825000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +34,SEA 65,160.875000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +35,SEA 66,160.925000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +36,SEA 67,156.375000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +37,SEA 68,156.425000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +38,SEA 69,156.475000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +39,DSC 70,156.525000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +40,SEA 71,156.575000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +41,SEA 72,156.625000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +42,SEA 73,156.675000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +43,SEA 74,156.725000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +44,SEA 77,156.875000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +45,SEA 78,161.525000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +46,SEA 79,161.575000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +47,SEA 80,161.625000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +48,SEA 81,161.675000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +49,SEA 82,161.725000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +50,SEA 83,161.775000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +51,SEA 84,161.825000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +52,SEA 85,161.875000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +53,SEA 86,161.925000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +54,AIS 87,161.975000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +55,AIS 88,162.025000,-,4.600000,,88.5,88.5,023,NN,FM,25.00,,,,, +56,SEA F1,155.625000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +57,SEA F2,155.775000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +58,SEA F3,155.825000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +59,SEA L1,155.500000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, +60,SEA L2,155.525000,,0.000000,,88.5,88.5,023,NN,FM,25.00,,,,, diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..1e80eb9 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,4 @@ +mock +mox3 +pytest +pytest-xdist diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d04e516 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,151 @@ +import glob +import logging +import os +import re +import shutil +import sys +import tempfile +import unittest + +import six + +from chirp import directory + +from tests import run_tests + + +LOG = logging.getLogger('testadapter') + + +class TestAdapterMeta(type): + def __new__(cls, name, parents, dct): + return super(TestAdapterMeta, cls).__new__(cls, name, parents, dct) + + +class TestAdapter(unittest.TestCase): + RADIO_CLASS = None + SOURCE_IMAGE = None + RADIO_INST = None + testwrapper = None + + def shortDescription(self): + test = self.id().split('.')[-1].replace('test_', '').replace('_', ' ') + return 'Testing %s %s' % (self.RADIO_CLASS.get_name(), test) + + @classmethod + def setUpClass(cls): + if not cls.testwrapper: + # Initialize the radio once per class invocation to save + # bitwise parse time + # Do this for things like Generic_CSV, that demand it + _base, ext = os.path.splitext(cls.SOURCE_IMAGE) + cls.testimage = tempfile.mktemp(ext) + shutil.copy(cls.SOURCE_IMAGE, cls.testimage) + cls.testwrapper = run_tests.TestWrapper(cls.RADIO_CLASS, + cls.testimage) + + @classmethod + def tearDownClass(cls): + os.remove(cls.testimage) + + def _runtest(self, test): + tw = run_tests.TestWrapper(self.RADIO_CLASS, + self.testimage, + dst=self.RADIO_INST) + testcase = test(tw) + testcase.prepare() + try: + failures = testcase.run() + if failures: + raise failures[0] + except run_tests.TestCrashError as e: + raise e.get_original_exception() + except run_tests.TestSkippedError as e: + raise unittest.SkipTest(str(e)) + finally: + testcase.cleanup() + + def test_copy_all(self): + self._runtest(run_tests.TestCaseCopyAll) + + def test_brute_force(self): + self._runtest(run_tests.TestCaseBruteForce) + + def test_edges(self): + self._runtest(run_tests.TestCaseEdges) + + def test_settings(self): + self._runtest(run_tests.TestCaseSettings) + + def test_banks(self): + self._runtest(run_tests.TestCaseBanks) + + def test_detect(self): + self._runtest(run_tests.TestCaseDetect) + + def test_clone(self): + self._runtest(run_tests.TestCaseClone) + + +def _get_sub_devices(rclass, testimage): + try: + tw = run_tests.TestWrapper(rclass, None) + except Exception as e: + tw = run_tests.TestWrapper(rclass, testimage) + + rf = tw.do("get_features") + if rf.has_sub_devices: + return tw.do("get_sub_devices") + else: + return [rclass] + + +class RadioSkipper(unittest.TestCase): + def test_is_supported_by_environment(self): + raise unittest.SkipTest('Running in py3 and driver is not supported') + + +def load_tests(loader, tests, pattern, suite=None): + if not suite: + suite = unittest.TestSuite() + + base = os.path.dirname(os.path.abspath(__file__)) + base = os.path.join(base, 'images') + images = glob.glob(os.path.join(base, "*")) + tests = {img: os.path.splitext(os.path.basename(img))[0] for img in images} + + if pattern == 'test*.py': + # This default is meaningless for us + pattern = None + + for image, test in tests.items(): + try: + rclass = directory.get_radio(test) + except Exception: + if six.PY3 and 'CHIRP_DEBUG' in os.environ: + LOG.error('Failed to load %s' % test) + continue + raise + for device in _get_sub_devices(rclass, image): + class_name = 'TestCase_%s' % ( + ''.join(filter(lambda c: c.isalnum(), + device.get_name()))) + if isinstance(device, type): + dst = None + else: + dst = device + device = device.__class__ + tc = TestAdapterMeta( + class_name, (TestAdapter,), dict(RADIO_CLASS=device, + SOURCE_IMAGE=image, + RADIO_INST=dst)) + tests = loader.loadTestsFromTestCase(tc) + + if pattern: + tests = [t for t in tests + if re.search(pattern, '%s.%s' % (class_name, + t._testMethodName))] + + suite.addTests(tests) + + return suite diff --git a/tests/icom_clone_simulator.py b/tests/icom_clone_simulator.py new file mode 100644 index 0000000..e13572a --- /dev/null +++ b/tests/icom_clone_simulator.py @@ -0,0 +1,195 @@ +# Copyright 2019 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import struct + +from chirp.drivers import icf + +import logging + +LOG = logging.getLogger(__name__) + + +import sys +l = logging.getLogger() +l.level = logging.ERROR +l.addHandler(logging.StreamHandler(sys.stdout)) + +class FakeIcomRadio(object): + def __init__(self, radio, mapfile=None): + self._buffer = bytes(b'') + self._radio = radio + if not mapfile: + self._memory = bytes(b'\x00') * radio.get_memsize() + else: + self.load_from_file(mapfile) + + def load_from_file(self, filename): + with open(filename, 'rb') as f: + self._memory = bytes(f.read()) + LOG.debug('Initialized %i bytes from %s' % (len(self._memory), + filename)) + + def read(self, count): + """read() from radio, so here we synthesize responses""" + chunk = self._buffer[:count] + self._buffer = self._buffer[count:] + return chunk + + def queue(self, data): + # LOG.debug('Queuing: %r' % data) + self._buffer += data + + def make_response(self, cmd, payload): + return bytes([ + 0xFE, 0xFE, + 0xEF, # Radio + 0xEE, # PC + cmd, + ]) + payload + bytes([0xFD]) + + @property + def address_fmt(self): + if self._radio.get_memsize() > 0x10000: + return 'I' + else: + return 'H' + + def do_clone_out(self): + LOG.debug('Clone from radio started') + size = 16 + for addr in range(0, self._radio.get_memsize(), size): + if len(self._memory[addr:]) < 4: + # IC-W32E has an off-by-one hack for detection, + # which will cause us to send a short one-byte + # block of garbage, unlike the real radio. So, + # if we get to the end and have a few bytes + # left, don't be stupid. + break + header = bytes(struct.pack('>%sB' % self.address_fmt, + addr, size)) + #LOG.debug('Header for %02x@%04x: %r' % ( + # size, addr, header)) + chunk = [] + cs = 0 + for byte in header: + chunk.extend(x for x in bytes(b'%02X' % byte)) + cs += byte + #LOG.debug('Chunk so far: %r' % chunk) + for byte in self._memory[addr:addr + size]: + chunk.extend(x for x in bytes(b'%02X' % byte)) + cs += byte + #LOG.debug('Chunk is %r' % chunk) + + vx = ((cs ^ 0xFFFF) + 1) & 0xFF + chunk.extend(x for x in bytes(b'%02X' % vx)) + self.queue(self.make_response(icf.CMD_CLONE_DAT, bytes(chunk))) + #LOG.debug('Stopping after first frame') + #break + self.queue(self.make_response(icf.CMD_CLONE_END, bytes([]))) + + def do_clone_in(self): + LOG.debug('Clone to radio started') + self._memory = bytes(b'') + + def do_clone_data(self, payload_hex): + if self.address_fmt == 'I': + header_len = 5 + else: + header_len = 3 + + def hex_to_byte(hexchars): + return int('%s%s' % (chr(hexchars[0]), chr(hexchars[1])), 16) + + payload_bytes = bytes([hex_to_byte(payload_hex[i:i+2]) + for i in range(0, len(payload_hex), 2)]) + + addr, size = struct.unpack('>%sB' % self.address_fmt, payload_bytes[:header_len]) + data = payload_bytes[header_len:-1] + csum = payload_bytes[-1] + + #addr_hex = payload[0:size_offset] + #size_hex = payload[size_offset:size_offset + 2] + #data_hex = payload[size_offset + 2:-2] + #csum_hex = payload[-2:] + + + #addr = hex_to_byte(addr_hex[0:2]) << 8 | hex_to_byte(addr_hex[2:4]) + #size = hex_to_byte(size_hex) + #csum = hex_to_byte(csum_hex) + + #data = [] + #for i in range(0, len(data_hex), 2): + # data.append(hex_to_byte(data_hex[i:i+2])) + + if len(data) != size: + LOG.debug('Invalid frame size: expected %i, but got %i' % ( + size, len(data))) + + expected_addr = len(self._memory) + if addr < expected_addr: + LOG.debug('Frame goes back to %04x from %04x' % (addr, + expected_addr)) + if len(self._memory) != addr: + LOG.debug('Filling gap between %04x and %04x' % (expected_addr, + addr)) + self._memory += (bytes(b'\x00') * (addr - expected_addr)) + + # FIXME: Check checksum + + self._memory += data + + def write(self, data): + """write() to radio, so here we process requests""" + + assert isinstance(data, bytes), 'Bytes required, %s received' % data.__class__ + + if data[:12] == (bytes(b'\xFE') * 12): + LOG.debug('Got hispeed kicker') + data = data[12:] + if data[2] == 0xFE: + return + + src = data[2] + dst = data[3] + cmd = data[4] + payload = data[5:-1] + end = data[-1] + + LOG.debug('Received command: %r' % cmd) + LOG.debug(' Full frame: %r' % data) + + model = self._radio.get_model() + bytes(b'\x00' * 20) + + if cmd == 0xE0: # Ident + # FIXME + self.queue(self.make_response(0x01, # Model + model)) + elif cmd == icf.CMD_CLONE_OUT: + self.do_clone_out() + elif cmd == icf.CMD_CLONE_IN: + self.do_clone_in() + elif cmd == icf.CMD_CLONE_DAT: + self.do_clone_data(payload) + else: + LOG.debug('Unknown command %i' % cmd) + self.queue(self.make_response(0x00, bytes([0x01]))) + + return len(data) + + def flush(self): + return diff --git a/tests/images/Alinco_DJ-G7EG.img b/tests/images/Alinco_DJ-G7EG.img new file mode 100644 index 0000000..2286bc1 Binary files /dev/null and b/tests/images/Alinco_DJ-G7EG.img differ diff --git a/tests/images/Alinco_DJ175.img b/tests/images/Alinco_DJ175.img new file mode 100644 index 0000000..0c05432 Binary files /dev/null and b/tests/images/Alinco_DJ175.img differ diff --git a/tests/images/Alinco_DJ596.img b/tests/images/Alinco_DJ596.img new file mode 100644 index 0000000..d37be19 Binary files /dev/null and b/tests/images/Alinco_DJ596.img differ diff --git a/tests/images/Alinco_DR235T.img b/tests/images/Alinco_DR235T.img new file mode 100644 index 0000000..8256271 Binary files /dev/null and b/tests/images/Alinco_DR235T.img differ diff --git a/tests/images/AnyTone_OBLTR-8R.img b/tests/images/AnyTone_OBLTR-8R.img new file mode 100644 index 0000000..2f11032 Binary files /dev/null and b/tests/images/AnyTone_OBLTR-8R.img differ diff --git a/tests/images/AnyTone_TERMN-8R.img b/tests/images/AnyTone_TERMN-8R.img new file mode 100644 index 0000000..0c88931 Binary files /dev/null and b/tests/images/AnyTone_TERMN-8R.img differ diff --git a/tests/images/BTECH_GMRS-50X1.img b/tests/images/BTECH_GMRS-50X1.img new file mode 100644 index 0000000..e84780f Binary files /dev/null and b/tests/images/BTECH_GMRS-50X1.img differ diff --git a/tests/images/BTECH_GMRS-V1.img b/tests/images/BTECH_GMRS-V1.img new file mode 100644 index 0000000..afc0a36 Binary files /dev/null and b/tests/images/BTECH_GMRS-V1.img differ diff --git a/tests/images/BTECH_MURS-V1.img b/tests/images/BTECH_MURS-V1.img new file mode 100644 index 0000000..fac635c Binary files /dev/null and b/tests/images/BTECH_MURS-V1.img differ diff --git a/tests/images/BTECH_UV-2501+220.img b/tests/images/BTECH_UV-2501+220.img new file mode 100755 index 0000000..8bf021c Binary files /dev/null and b/tests/images/BTECH_UV-2501+220.img differ diff --git a/tests/images/BTECH_UV-25X2.img b/tests/images/BTECH_UV-25X2.img new file mode 100755 index 0000000..aec97d8 Binary files /dev/null and b/tests/images/BTECH_UV-25X2.img differ diff --git a/tests/images/BTECH_UV-25X4.img b/tests/images/BTECH_UV-25X4.img new file mode 100755 index 0000000..12ce9f9 Binary files /dev/null and b/tests/images/BTECH_UV-25X4.img differ diff --git a/tests/images/BTECH_UV-5001.img b/tests/images/BTECH_UV-5001.img new file mode 100755 index 0000000..377be8b Binary files /dev/null and b/tests/images/BTECH_UV-5001.img differ diff --git a/tests/images/BTECH_UV-50X2.img b/tests/images/BTECH_UV-50X2.img new file mode 100755 index 0000000..6bac4fb Binary files /dev/null and b/tests/images/BTECH_UV-50X2.img differ diff --git a/tests/images/BTECH_UV-50X3.img b/tests/images/BTECH_UV-50X3.img new file mode 100644 index 0000000..6da4293 Binary files /dev/null and b/tests/images/BTECH_UV-50X3.img differ diff --git a/tests/images/BTECH_UV-5X3.img b/tests/images/BTECH_UV-5X3.img new file mode 100755 index 0000000..aa6ad8a Binary files /dev/null and b/tests/images/BTECH_UV-5X3.img differ diff --git a/tests/images/Baofeng_BF-888.img b/tests/images/Baofeng_BF-888.img new file mode 100644 index 0000000..0304ddf Binary files /dev/null and b/tests/images/Baofeng_BF-888.img differ diff --git a/tests/images/Baofeng_BF-A58S.img b/tests/images/Baofeng_BF-A58S.img new file mode 100644 index 0000000..bbcbffc Binary files /dev/null and b/tests/images/Baofeng_BF-A58S.img differ diff --git a/tests/images/Baofeng_BF-T1.img b/tests/images/Baofeng_BF-T1.img new file mode 100644 index 0000000..849896c Binary files /dev/null and b/tests/images/Baofeng_BF-T1.img differ diff --git a/tests/images/Baofeng_F-11.img b/tests/images/Baofeng_F-11.img new file mode 100644 index 0000000..b13f9b4 Binary files /dev/null and b/tests/images/Baofeng_F-11.img differ diff --git a/tests/images/Baofeng_UV-3R.img b/tests/images/Baofeng_UV-3R.img new file mode 100644 index 0000000..8333f02 Binary files /dev/null and b/tests/images/Baofeng_UV-3R.img differ diff --git a/tests/images/Baofeng_UV-5R.img b/tests/images/Baofeng_UV-5R.img new file mode 100644 index 0000000..dcdc3e8 Binary files /dev/null and b/tests/images/Baofeng_UV-5R.img differ diff --git a/tests/images/Baofeng_UV-6R.img b/tests/images/Baofeng_UV-6R.img new file mode 100755 index 0000000..32f5c15 Binary files /dev/null and b/tests/images/Baofeng_UV-6R.img differ diff --git a/tests/images/Baofeng_UV-B5.img b/tests/images/Baofeng_UV-B5.img new file mode 100644 index 0000000..5d69456 Binary files /dev/null and b/tests/images/Baofeng_UV-B5.img differ diff --git a/tests/images/Baojie_BJ-9900.img b/tests/images/Baojie_BJ-9900.img new file mode 100644 index 0000000..905dda3 Binary files /dev/null and b/tests/images/Baojie_BJ-9900.img differ diff --git a/tests/images/Boblov_X3Plus.img b/tests/images/Boblov_X3Plus.img new file mode 100644 index 0000000..07331e4 Binary files /dev/null and b/tests/images/Boblov_X3Plus.img differ diff --git a/tests/images/Feidaxin_FD-268A.img b/tests/images/Feidaxin_FD-268A.img new file mode 100755 index 0000000..52fc11b Binary files /dev/null and b/tests/images/Feidaxin_FD-268A.img differ diff --git a/tests/images/Feidaxin_FD-268B.img b/tests/images/Feidaxin_FD-268B.img new file mode 100755 index 0000000..8de4949 Binary files /dev/null and b/tests/images/Feidaxin_FD-268B.img differ diff --git a/tests/images/Feidaxin_FD-288B.img b/tests/images/Feidaxin_FD-288B.img new file mode 100755 index 0000000..778ff91 Binary files /dev/null and b/tests/images/Feidaxin_FD-288B.img differ diff --git a/tests/images/Generic_CSV.csv b/tests/images/Generic_CSV.csv new file mode 100644 index 0000000..e50f849 --- /dev/null +++ b/tests/images/Generic_CSV.csv @@ -0,0 +1,104 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Bank,Bank Index,URCALL,RPT1CALL,RPT2CALL +25,H-TAC1,443.100000,+,5.000000,DTCS,88.5,88.5,032,NN,FM,5.00,,,-1,,,, +26,H-TAC2,147.380000,+,0.600000,Tone,100.0,100.0,023,NN,FM,5.00,,,-1,,,, +27,H-TAC3,147.440000,,0.600000,Tone,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +28,H-TAC4,441.550000,+,5.000000,Tone,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +29,H-TAC5,442.925000,+,5.000000,Tone,107.2,107.2,023,NN,FM,5.00,,,-1,,,, +30,H-TAC6,443.350000,+,5.000000,Tone,156.7,156.7,023,NN,FM,5.00,,,-1,,,, +31,H-TAC7,442.825000,+,5.000000,,110.9,110.9,023,NN,FM,5.00,,,-1,,,, +50,ARESD1,147.320000,+,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +51,WAPRIR,146.900000,-,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +52,WAPRIS,147.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +53,WASECR,440.350000,+,5.000000,TSQL,127.3,127.3,023,NN,FM,5.00,,,-1,,,, +54,OEMNCS,145.330000,-,0.600000,Tone,186.2,186.2,023,NN,FM,5.00,,,-1,,,, +55,HEARTN,145.230000,-,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +56,CLACK,147.120000,+,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +57,CLATSP,146.660000,-,0.600000,Tone,118.8,118.8,023,NN,FM,5.00,,,-1,,,, +58,COLUMB,146.880000,-,0.600000,Tone,114.8,114.8,023,NN,FM,5.00,,,-1,,,, +59,TMOOK1,147.220000,+,0.600000,Tone,100.0,100.0,023,NN,FM,5.00,,,-1,,,, +60,TMOOK2,147.160000,+,0.600000,Tone,118.8,118.8,023,NN,FM,5.00,,,-1,,,, +61,TMOOK3,440.175000,+,5.000000,Tone,100.0,100.0,023,NN,FM,5.00,,,-1,,,, +62,TMOOK4,441.250000,+,5.000000,Tone,118.8,118.8,023,NN,FM,5.00,,,-1,,,, +63,MULTNM,146.840000,-,0.600000,Tone,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +64,CLARK,147.240000,+,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +65,ARC,146.980000,-,0.600000,Tone,123.0,123.0,023,NN,FM,5.00,,,-1,,,, +66,VERNIA,145.250000,-,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +80,WX1,162.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +81,WX2,162.425000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +82,WX3,162.450000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +83,WX4,162.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +84,WX5,162.500000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +85,WX6,162.525000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +86,WX7,162.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +88,CTAF,119.300000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +89,ATIS,127.650000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +90,GROUND,121.700000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +91,TOWER,119.300000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +92,UNICOM,122.950000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +93,ACARS,131.550000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +100,ICALL,851.012500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +101,ITAC1,851.512500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +102,ITAC2,852.012500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +103,ITAC3,852.512500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +104,ITAC4,853.012500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +105,OROPS1,851.325000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +106,OROPS2,851.387500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +107,OROPS3,851.750000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +108,OROPS4,851.775000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +109,OROPS5,851.800000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +110,WAOPS1,852.537500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +111,WAOPS2,852.562500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +112,WAOPS3,852.587500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +113,WAOPS4,852.612500,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +114,WAOPS5,852.637500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +115,UCAL40,453.212500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +116,UTAC41,453.462500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +117,UTAC42,453.712500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +118,UTAC43,453.862500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +119,ISIMP1,853.437500,,0.600000,DTCS,88.5,88.5,074,NN,FM,12.50,,,-1,,,, +120,ISIMP2,851.037500,,0.600000,DTCS,88.5,88.5,114,NN,FM,12.50,,,-1,,,, +121,ISIMP3,851.950000,,0.600000,DTCS,88.5,88.5,131,NN,FM,10.00,,,-1,,,, +122,ISIMP4,851.175000,,0.600000,DTCS,88.5,88.5,023,NN,FM,10.00,,,-1,,,, +123,MAYDAY,853.387500,,0.600000,DTCS,88.5,88.5,025,NN,FM,12.50,,,-1,,,, +124,VCALL,155.750000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +125,VTAC11,151.137500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +126,VTAC12,154.452500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +127,VTAC13,158.737500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +128,VTAC14,159.472500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +129,WCCCA1,860.737500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +130,WCCCA2,860.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +131,WCCCA3,859.737500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +132,WCCCA4,859.737500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +133,WCCCA5,858.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +134,WCCCA6,857.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +135,WCCCA7,856.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +136,WCCCA8,855.962500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +137,WCCCA9,855.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +138,WCCCA0,854.987500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +139,OR SAR,155.805000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +140,OPEN,155.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +141,OSPTAC,156.030000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +142,OSPD1A,154.935000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +143,OSPD1B,156.225000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +144,OSPD1C,154.905000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +145,OSPD1D,156.150000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +146,OSPD6A,153.935000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +147,OSPD6B,154.785000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +148,OSPD6C,154.860000,,0.600000,TSQL,88.5,131.8,023,NN,FM,5.00,,,-1,,,, +149,OSPD6D,155.910000,,0.600000,TSQL,88.5,131.8,023,NN,FM,5.00,,,-1,,,, +150,WASP,155.370000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +151,CHP,156.075000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +152,ODFW,158.895000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +153,SARINTOP,158.905000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +160,H-DS1,147.550000,,0.600000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +161,H-DS2,147.580000,,0.600000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +162,H-DS3,446.300000,,5.000000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +163,H-DS4,446.400000,,5.000000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +164,H-DS5,1294.100000,,0.600000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +165,H-DS6,441.637500,+,5.000000,,88.5,88.5,023,NN,DV,12.50,,,-1,CQCQCQ,,,0 +166,H-DS7,440.550000,+,5.000000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +167,H-DS8,444.262500,+,5.000000,,88.5,88.5,023,NN,DV,12.50,,,-1,CQCQCQ,,,0 +168,HPAGE,145.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +169,APRS,144.390000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +170,H-DAT1,145.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +171,H-DAT2,145.070000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, diff --git a/tests/images/Icom_IC-208H.img b/tests/images/Icom_IC-208H.img new file mode 100644 index 0000000..8917475 Binary files /dev/null and b/tests/images/Icom_IC-208H.img differ diff --git a/tests/images/Icom_IC-2100H.img b/tests/images/Icom_IC-2100H.img new file mode 100644 index 0000000..cf6ffd8 Binary files /dev/null and b/tests/images/Icom_IC-2100H.img differ diff --git a/tests/images/Icom_IC-2200H.img b/tests/images/Icom_IC-2200H.img new file mode 100644 index 0000000..56ee266 Binary files /dev/null and b/tests/images/Icom_IC-2200H.img differ diff --git a/tests/images/Icom_IC-2300H.img b/tests/images/Icom_IC-2300H.img new file mode 100644 index 0000000..cd6aad0 Binary files /dev/null and b/tests/images/Icom_IC-2300H.img differ diff --git a/tests/images/Icom_IC-2720H.img b/tests/images/Icom_IC-2720H.img new file mode 100644 index 0000000..e153ce1 Binary files /dev/null and b/tests/images/Icom_IC-2720H.img differ diff --git a/tests/images/Icom_IC-2730A.img b/tests/images/Icom_IC-2730A.img new file mode 100644 index 0000000..73b973a Binary files /dev/null and b/tests/images/Icom_IC-2730A.img differ diff --git a/tests/images/Icom_IC-2820H.img b/tests/images/Icom_IC-2820H.img new file mode 100644 index 0000000..e845f9f Binary files /dev/null and b/tests/images/Icom_IC-2820H.img differ diff --git a/tests/images/Icom_IC-P7.img b/tests/images/Icom_IC-P7.img new file mode 100755 index 0000000..35a0966 Binary files /dev/null and b/tests/images/Icom_IC-P7.img differ diff --git a/tests/images/Icom_IC-Q7A.img b/tests/images/Icom_IC-Q7A.img new file mode 100644 index 0000000..bad5f87 Binary files /dev/null and b/tests/images/Icom_IC-Q7A.img differ diff --git a/tests/images/Icom_IC-T70.img b/tests/images/Icom_IC-T70.img new file mode 100644 index 0000000..5c09019 Binary files /dev/null and b/tests/images/Icom_IC-T70.img differ diff --git a/tests/images/Icom_IC-T7H.img b/tests/images/Icom_IC-T7H.img new file mode 100644 index 0000000..5186620 Binary files /dev/null and b/tests/images/Icom_IC-T7H.img differ diff --git a/tests/images/Icom_IC-T8A.img b/tests/images/Icom_IC-T8A.img new file mode 100644 index 0000000..c5bcc45 Binary files /dev/null and b/tests/images/Icom_IC-T8A.img differ diff --git a/tests/images/Icom_IC-V82_U82.img b/tests/images/Icom_IC-V82_U82.img new file mode 100644 index 0000000..5a1c0a4 Binary files /dev/null and b/tests/images/Icom_IC-V82_U82.img differ diff --git a/tests/images/Icom_IC-W32A.img b/tests/images/Icom_IC-W32A.img new file mode 100644 index 0000000..b5b239c Binary files /dev/null and b/tests/images/Icom_IC-W32A.img differ diff --git a/tests/images/Icom_IC-W32E.img b/tests/images/Icom_IC-W32E.img new file mode 100644 index 0000000..32bee84 Binary files /dev/null and b/tests/images/Icom_IC-W32E.img differ diff --git a/tests/images/Icom_ID-31A.img b/tests/images/Icom_ID-31A.img new file mode 100644 index 0000000..afdebdd Binary files /dev/null and b/tests/images/Icom_ID-31A.img differ diff --git a/tests/images/Icom_ID-51.img b/tests/images/Icom_ID-51.img new file mode 100644 index 0000000..399c857 Binary files /dev/null and b/tests/images/Icom_ID-51.img differ diff --git a/tests/images/Icom_ID-51_Plus.img b/tests/images/Icom_ID-51_Plus.img new file mode 100755 index 0000000..07bdc7e Binary files /dev/null and b/tests/images/Icom_ID-51_Plus.img differ diff --git a/tests/images/Icom_ID-800H_v2.img b/tests/images/Icom_ID-800H_v2.img new file mode 100644 index 0000000..113f450 Binary files /dev/null and b/tests/images/Icom_ID-800H_v2.img differ diff --git a/tests/images/Icom_ID-880H.img b/tests/images/Icom_ID-880H.img new file mode 100644 index 0000000..0a83f24 Binary files /dev/null and b/tests/images/Icom_ID-880H.img differ diff --git a/tests/images/Jetstream_JT220M.img b/tests/images/Jetstream_JT220M.img new file mode 100644 index 0000000..bb0b4a7 Binary files /dev/null and b/tests/images/Jetstream_JT220M.img differ diff --git a/tests/images/Jetstream_JT270M.img b/tests/images/Jetstream_JT270M.img new file mode 100644 index 0000000..acc95d4 Binary files /dev/null and b/tests/images/Jetstream_JT270M.img differ diff --git a/tests/images/Jetstream_JT270MH.img b/tests/images/Jetstream_JT270MH.img new file mode 100644 index 0000000..4027cb0 Binary files /dev/null and b/tests/images/Jetstream_JT270MH.img differ diff --git a/tests/images/KYD_IP-620.img b/tests/images/KYD_IP-620.img new file mode 100755 index 0000000..c088dfe Binary files /dev/null and b/tests/images/KYD_IP-620.img differ diff --git a/tests/images/KYD_NC-630A.img b/tests/images/KYD_NC-630A.img new file mode 100755 index 0000000..2c627fa Binary files /dev/null and b/tests/images/KYD_NC-630A.img differ diff --git a/tests/images/Kenwood_HMK.hmk b/tests/images/Kenwood_HMK.hmk new file mode 100644 index 0000000..e8ee04a --- /dev/null +++ b/tests/images/Kenwood_HMK.hmk @@ -0,0 +1,71 @@ +KENWOOD MCP FOR AMATEUR MOBILE TRANSCEIVER +[Export Software]=MCP-2A Version 3.02 +[Export File Version]=1 +[Type]=E +[Language]=English + +// Comments +!!Comments= + +// Memory Channels +!!Ch,Rx Freq.,Rx Step,Offset,T/CT/DCS,TO Freq.,CT Freq.,DCS Code,Shift/Split,Rev.,L.Out,Mode,Tx Freq.,Tx Step,M.Name +"900","00155,500000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,500000","025,00","MVHF L1" +"901","00155,525000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,525000","025,00","MVHF L2" +"902","00155,625000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,625000","025,00","MVHF F1" +"903","00155,775000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,775000","025,00","MVHF F2" +"904","00155,825000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,825000","025,00","MVHF F3" +"905","00156,300000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,300000","025,00","MVHF K06" +"906","00156,375000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,375000","025,00","MVHF K67" +"907","00156,400000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,400000","025,00","MVHF K08" +"908","00156,425000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,425000","025,00","MVHF K68" +"909","00156,450000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,450000","025,00","MVHF K09" +"910","00156,475000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,475000","025,00","MVHF K69" +"911","00156,500000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,500000","025,00","MVHF K10" +"912","00156,525000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,525000","025,00","MDSC K70" +"913","00156,550000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,550000","025,00","MVHF K11" +"914","00156,575000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,575000","025,00","MVHF K71" +"915","00156,600000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,600000","025,00","MVHF K12" +"916","00156,625000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,625000","025,00","MVHF K72" +"917","00156,650000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,650000","025,00","MVHF K13" +"918","00156,675000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,675000","025,00","MVHF K73" +"919","00156,700000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,700000","025,00","MVHF K14" +"920","00156,725000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,725000","025,00","MVHF K74" +"921","00156,750000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,750000","025,00","MVHF K15" +"922","00156,800000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,800000","025,00","MVHF K16" +"923","00156,850000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,850000","025,00","MVHF K17" +"924","00156,875000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,875000","025,00","MVHF K77" +"925","00160,625000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,625000","025,00","MVHF K60" +"926","00160,650000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,650000","025,00","MVHF K01" +"927","00160,675000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,675000","025,00","MVHF K61" +"928","00160,700000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,700000","025,00","MVHF K02" +"929","00160,725000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,725000","025,00","MVHF K62" +"930","00160,750000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,750000","025,00","MVHF K03" +"931","00160,775000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,775000","025,00","MVHF K63" +"932","00160,800000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,800000","025,00","MVHF K04" +"933","00160,825000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,825000","025,00","MVHF K64" +"934","00160,850000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,850000","025,00","MVHF K05" +"935","00160,875000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,875000","025,00","MVHF K65" +"936","00160,925000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,925000","025,00","MVHF K66" +"937","00160,950000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,950000","025,00","MVHF K07" +"938","00161,500000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,500000","025,00","MVHF K18" +"939","00161,525000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,525000","025,00","MVHF K78" +"940","00161,550000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,550000","025,00","MVHF K19" +"941","00161,575000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,575000","025,00","MVHF K79" +"942","00161,600000","025,00","04,600000","Off","88,5","88,5","023","-","Off","Off","FM","161,600000","025,00","MVHF K20" +"943","00161,625000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,625000","025,00","MVHF K80" +"944","00161,650000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,650000","025,00","MVHF K21" +"945","00161,675000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,675000","025,00","MVHF K81" +"946","00161,700000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,700000","025,00","MVHF K22" +"947","00161,725000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,725000","025,00","MVHF K82" +"948","00161,750000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,750000","025,00","MVHF K23" +"949","00161,775000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,775000","025,00","MVHF K83" +"950","00161,800000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,800000","025,00","MVHF K24" +"951","00161,825000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,825000","025,00","MVHF K84" +"952","00161,850000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,850000","025,00","MVHF K25" +"953","00161,875000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,875000","025,00","MVHF K85" +"954","00161,900000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,900000","025,00","MVHF K26" +"955","00161,925000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,925000","025,00","MVHF K86" +"956","00161,950000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,950000","025,00","MVHF K27" +"957","00161,975000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,975000","025,00","MAIS K87" +"958","00162,000000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","162,000000","025,00","MVHF K28" +"959","00162,025000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","162,025000","025,00","MAIS K88" diff --git a/tests/images/Kenwood_TH-D72_clone_mode.img b/tests/images/Kenwood_TH-D72_clone_mode.img new file mode 100644 index 0000000..257c2a2 Binary files /dev/null and b/tests/images/Kenwood_TH-D72_clone_mode.img differ diff --git a/tests/images/Kenwood_TK-272G.img b/tests/images/Kenwood_TK-272G.img new file mode 100755 index 0000000..bdeed1f Binary files /dev/null and b/tests/images/Kenwood_TK-272G.img differ diff --git a/tests/images/Kenwood_TK-3180K2.img b/tests/images/Kenwood_TK-3180K2.img new file mode 100644 index 0000000..b3524c9 Binary files /dev/null and b/tests/images/Kenwood_TK-3180K2.img differ diff --git a/tests/images/Kenwood_TK-760G.img b/tests/images/Kenwood_TK-760G.img new file mode 100755 index 0000000..23c883a Binary files /dev/null and b/tests/images/Kenwood_TK-760G.img differ diff --git a/tests/images/Kenwood_TK-8102.img b/tests/images/Kenwood_TK-8102.img new file mode 100644 index 0000000..d03fbc4 Binary files /dev/null and b/tests/images/Kenwood_TK-8102.img differ diff --git a/tests/images/Kenwood_TK-8180.img b/tests/images/Kenwood_TK-8180.img new file mode 100644 index 0000000..06a5910 Binary files /dev/null and b/tests/images/Kenwood_TK-8180.img differ diff --git a/tests/images/Kenwood_TS-480_CloneMode.img b/tests/images/Kenwood_TS-480_CloneMode.img new file mode 100644 index 0000000..2e018a0 Binary files /dev/null and b/tests/images/Kenwood_TS-480_CloneMode.img differ diff --git a/tests/images/LUITON_LT-725UV.img b/tests/images/LUITON_LT-725UV.img new file mode 100755 index 0000000..537e13b Binary files /dev/null and b/tests/images/LUITON_LT-725UV.img differ diff --git a/tests/images/Leixen_VV-898.img b/tests/images/Leixen_VV-898.img new file mode 100644 index 0000000..abeec14 Binary files /dev/null and b/tests/images/Leixen_VV-898.img differ diff --git a/tests/images/Leixen_VV-898S.img b/tests/images/Leixen_VV-898S.img new file mode 100755 index 0000000..10a264a Binary files /dev/null and b/tests/images/Leixen_VV-898S.img differ diff --git a/tests/images/Polmar_DB-50M.img b/tests/images/Polmar_DB-50M.img new file mode 100644 index 0000000..04df657 Binary files /dev/null and b/tests/images/Polmar_DB-50M.img differ diff --git a/tests/images/Puxing_PX-2R.img b/tests/images/Puxing_PX-2R.img new file mode 100644 index 0000000..63c615c Binary files /dev/null and b/tests/images/Puxing_PX-2R.img differ diff --git a/tests/images/Puxing_PX-777.img b/tests/images/Puxing_PX-777.img new file mode 100644 index 0000000..3fceb43 Binary files /dev/null and b/tests/images/Puxing_PX-777.img differ diff --git a/tests/images/Puxing_PX-888K.img b/tests/images/Puxing_PX-888K.img new file mode 100644 index 0000000..27d30d0 Binary files /dev/null and b/tests/images/Puxing_PX-888K.img differ diff --git a/tests/images/QYT_KT7900D.img b/tests/images/QYT_KT7900D.img new file mode 100755 index 0000000..73bfc37 Binary files /dev/null and b/tests/images/QYT_KT7900D.img differ diff --git a/tests/images/QYT_KT8900D.img b/tests/images/QYT_KT8900D.img new file mode 100755 index 0000000..e31310f Binary files /dev/null and b/tests/images/QYT_KT8900D.img differ diff --git a/tests/images/Radioddity_R2.img b/tests/images/Radioddity_R2.img new file mode 100644 index 0000000..0bbd846 Binary files /dev/null and b/tests/images/Radioddity_R2.img differ diff --git a/tests/images/Radtel_T18.img b/tests/images/Radtel_T18.img new file mode 100755 index 0000000..f508675 Binary files /dev/null and b/tests/images/Radtel_T18.img differ diff --git a/tests/images/Retevis_RT21.img b/tests/images/Retevis_RT21.img new file mode 100644 index 0000000..030d4f7 Binary files /dev/null and b/tests/images/Retevis_RT21.img differ diff --git a/tests/images/Retevis_RT22.img b/tests/images/Retevis_RT22.img new file mode 100644 index 0000000..ffca933 Binary files /dev/null and b/tests/images/Retevis_RT22.img differ diff --git a/tests/images/Retevis_RT23.img b/tests/images/Retevis_RT23.img new file mode 100755 index 0000000..2bb0252 Binary files /dev/null and b/tests/images/Retevis_RT23.img differ diff --git a/tests/images/Retevis_RT26.img b/tests/images/Retevis_RT26.img new file mode 100644 index 0000000..fdb6141 Binary files /dev/null and b/tests/images/Retevis_RT26.img differ diff --git a/tests/images/TDXone_TD-Q8A.img b/tests/images/TDXone_TD-Q8A.img new file mode 100755 index 0000000..6bd3d07 Binary files /dev/null and b/tests/images/TDXone_TD-Q8A.img differ diff --git a/tests/images/TYT_TH-350.img b/tests/images/TYT_TH-350.img new file mode 100644 index 0000000..7d1fd3c Binary files /dev/null and b/tests/images/TYT_TH-350.img differ diff --git a/tests/images/TYT_TH-7800.img b/tests/images/TYT_TH-7800.img new file mode 100644 index 0000000..5c9240d Binary files /dev/null and b/tests/images/TYT_TH-7800.img differ diff --git a/tests/images/TYT_TH-9800.img b/tests/images/TYT_TH-9800.img new file mode 100644 index 0000000..a2f584b Binary files /dev/null and b/tests/images/TYT_TH-9800.img differ diff --git a/tests/images/TYT_TH-UV3R-25.img b/tests/images/TYT_TH-UV3R-25.img new file mode 100755 index 0000000..d62a459 Binary files /dev/null and b/tests/images/TYT_TH-UV3R-25.img differ diff --git a/tests/images/TYT_TH-UV3R.img b/tests/images/TYT_TH-UV3R.img new file mode 100644 index 0000000..75b2cf6 Binary files /dev/null and b/tests/images/TYT_TH-UV3R.img differ diff --git a/tests/images/TYT_TH-UV8000.img b/tests/images/TYT_TH-UV8000.img new file mode 100644 index 0000000..71b7aa9 Binary files /dev/null and b/tests/images/TYT_TH-UV8000.img differ diff --git a/tests/images/TYT_TH-UVF1.img b/tests/images/TYT_TH-UVF1.img new file mode 100644 index 0000000..f01edd6 Binary files /dev/null and b/tests/images/TYT_TH-UVF1.img differ diff --git a/tests/images/TYT_TH9000_144.img b/tests/images/TYT_TH9000_144.img new file mode 100644 index 0000000..871e986 Binary files /dev/null and b/tests/images/TYT_TH9000_144.img differ diff --git a/tests/images/Vertex_Standard_VXA-700.img b/tests/images/Vertex_Standard_VXA-700.img new file mode 100644 index 0000000..e0dbbc0 Binary files /dev/null and b/tests/images/Vertex_Standard_VXA-700.img differ diff --git a/tests/images/WACCOM_MINI-8900.img b/tests/images/WACCOM_MINI-8900.img new file mode 100755 index 0000000..9b5737a Binary files /dev/null and b/tests/images/WACCOM_MINI-8900.img differ diff --git a/tests/images/Wouxun_KG-816.img b/tests/images/Wouxun_KG-816.img new file mode 100644 index 0000000..090e18d Binary files /dev/null and b/tests/images/Wouxun_KG-816.img differ diff --git a/tests/images/Wouxun_KG-818.img b/tests/images/Wouxun_KG-818.img new file mode 100644 index 0000000..716da73 Binary files /dev/null and b/tests/images/Wouxun_KG-818.img differ diff --git a/tests/images/Wouxun_KG-UV6.img b/tests/images/Wouxun_KG-UV6.img new file mode 100644 index 0000000..b1a7795 Binary files /dev/null and b/tests/images/Wouxun_KG-UV6.img differ diff --git a/tests/images/Wouxun_KG-UV8D.img b/tests/images/Wouxun_KG-UV8D.img new file mode 100644 index 0000000..1c66447 Binary files /dev/null and b/tests/images/Wouxun_KG-UV8D.img differ diff --git a/tests/images/Wouxun_KG-UV8D_Plus.img b/tests/images/Wouxun_KG-UV8D_Plus.img new file mode 100644 index 0000000..23e6f58 Binary files /dev/null and b/tests/images/Wouxun_KG-UV8D_Plus.img differ diff --git a/tests/images/Wouxun_KG-UV8E.img b/tests/images/Wouxun_KG-UV8E.img new file mode 100644 index 0000000..89e5e1f Binary files /dev/null and b/tests/images/Wouxun_KG-UV8E.img differ diff --git a/tests/images/Wouxun_KG-UV9D_Plus.img b/tests/images/Wouxun_KG-UV9D_Plus.img new file mode 100644 index 0000000..410e498 Binary files /dev/null and b/tests/images/Wouxun_KG-UV9D_Plus.img differ diff --git a/tests/images/Wouxun_KG-UVD1P.img b/tests/images/Wouxun_KG-UVD1P.img new file mode 100644 index 0000000..93df646 Binary files /dev/null and b/tests/images/Wouxun_KG-UVD1P.img differ diff --git a/tests/images/Yaesu_FT-1500M.img b/tests/images/Yaesu_FT-1500M.img new file mode 100644 index 0000000..581e353 Binary files /dev/null and b/tests/images/Yaesu_FT-1500M.img differ diff --git a/tests/images/Yaesu_FT-1802M.img b/tests/images/Yaesu_FT-1802M.img new file mode 100644 index 0000000..f13e34d Binary files /dev/null and b/tests/images/Yaesu_FT-1802M.img differ diff --git a/tests/images/Yaesu_FT-1D_R.img b/tests/images/Yaesu_FT-1D_R.img new file mode 100644 index 0000000..70190c8 Binary files /dev/null and b/tests/images/Yaesu_FT-1D_R.img differ diff --git a/tests/images/Yaesu_FT-25R.img b/tests/images/Yaesu_FT-25R.img new file mode 100644 index 0000000..fef4b0e Binary files /dev/null and b/tests/images/Yaesu_FT-25R.img differ diff --git a/tests/images/Yaesu_FT-2800M.img b/tests/images/Yaesu_FT-2800M.img new file mode 100644 index 0000000..35ccf18 Binary files /dev/null and b/tests/images/Yaesu_FT-2800M.img differ diff --git a/tests/images/Yaesu_FT-2900R_1900R.img b/tests/images/Yaesu_FT-2900R_1900R.img new file mode 100755 index 0000000..ef17a61 Binary files /dev/null and b/tests/images/Yaesu_FT-2900R_1900R.img differ diff --git a/tests/images/Yaesu_FT-450D.img b/tests/images/Yaesu_FT-450D.img new file mode 100644 index 0000000..09873ea Binary files /dev/null and b/tests/images/Yaesu_FT-450D.img differ diff --git a/tests/images/Yaesu_FT-4VR.img b/tests/images/Yaesu_FT-4VR.img new file mode 100644 index 0000000..ffa6cfe Binary files /dev/null and b/tests/images/Yaesu_FT-4VR.img differ diff --git a/tests/images/Yaesu_FT-4XE.img b/tests/images/Yaesu_FT-4XE.img new file mode 100644 index 0000000..fb8087a Binary files /dev/null and b/tests/images/Yaesu_FT-4XE.img differ diff --git a/tests/images/Yaesu_FT-4XR.img b/tests/images/Yaesu_FT-4XR.img new file mode 100644 index 0000000..b45d1ae Binary files /dev/null and b/tests/images/Yaesu_FT-4XR.img differ diff --git a/tests/images/Yaesu_FT-50.img b/tests/images/Yaesu_FT-50.img new file mode 100755 index 0000000..77cffd7 Binary files /dev/null and b/tests/images/Yaesu_FT-50.img differ diff --git a/tests/images/Yaesu_FT-60.img b/tests/images/Yaesu_FT-60.img new file mode 100644 index 0000000..f17d470 Binary files /dev/null and b/tests/images/Yaesu_FT-60.img differ diff --git a/tests/images/Yaesu_FT-65E.img b/tests/images/Yaesu_FT-65E.img new file mode 100644 index 0000000..94cda1a Binary files /dev/null and b/tests/images/Yaesu_FT-65E.img differ diff --git a/tests/images/Yaesu_FT-65R.img b/tests/images/Yaesu_FT-65R.img new file mode 100644 index 0000000..c62c3df Binary files /dev/null and b/tests/images/Yaesu_FT-65R.img differ diff --git a/tests/images/Yaesu_FT-70D.img b/tests/images/Yaesu_FT-70D.img new file mode 100644 index 0000000..b557586 Binary files /dev/null and b/tests/images/Yaesu_FT-70D.img differ diff --git a/tests/images/Yaesu_FT-7100M.img b/tests/images/Yaesu_FT-7100M.img new file mode 100644 index 0000000..30e18ce Binary files /dev/null and b/tests/images/Yaesu_FT-7100M.img differ diff --git a/tests/images/Yaesu_FT-7800_7900.img b/tests/images/Yaesu_FT-7800_7900.img new file mode 100644 index 0000000..c10a4fb Binary files /dev/null and b/tests/images/Yaesu_FT-7800_7900.img differ diff --git a/tests/images/Yaesu_FT-817.img b/tests/images/Yaesu_FT-817.img new file mode 100644 index 0000000..00e624d Binary files /dev/null and b/tests/images/Yaesu_FT-817.img differ diff --git a/tests/images/Yaesu_FT-817ND.img b/tests/images/Yaesu_FT-817ND.img new file mode 100644 index 0000000..c0a2bb4 Binary files /dev/null and b/tests/images/Yaesu_FT-817ND.img differ diff --git a/tests/images/Yaesu_FT-817ND_US.img b/tests/images/Yaesu_FT-817ND_US.img new file mode 100644 index 0000000..210c5a8 Binary files /dev/null and b/tests/images/Yaesu_FT-817ND_US.img differ diff --git a/tests/images/Yaesu_FT-818.img b/tests/images/Yaesu_FT-818.img new file mode 100644 index 0000000..59b6e62 Binary files /dev/null and b/tests/images/Yaesu_FT-818.img differ diff --git a/tests/images/Yaesu_FT-857_897.img b/tests/images/Yaesu_FT-857_897.img new file mode 100644 index 0000000..ac66f73 Binary files /dev/null and b/tests/images/Yaesu_FT-857_897.img differ diff --git a/tests/images/Yaesu_FT-857_897_US.img b/tests/images/Yaesu_FT-857_897_US.img new file mode 100644 index 0000000..20a1c24 Binary files /dev/null and b/tests/images/Yaesu_FT-857_897_US.img differ diff --git a/tests/images/Yaesu_FT-8800.img b/tests/images/Yaesu_FT-8800.img new file mode 100644 index 0000000..4cbb883 Binary files /dev/null and b/tests/images/Yaesu_FT-8800.img differ diff --git a/tests/images/Yaesu_FT-8900.img b/tests/images/Yaesu_FT-8900.img new file mode 100644 index 0000000..0445467 Binary files /dev/null and b/tests/images/Yaesu_FT-8900.img differ diff --git a/tests/images/Yaesu_FT2D_R.img b/tests/images/Yaesu_FT2D_R.img new file mode 100644 index 0000000..459b0ce Binary files /dev/null and b/tests/images/Yaesu_FT2D_R.img differ diff --git a/tests/images/Yaesu_FT3D_R.img b/tests/images/Yaesu_FT3D_R.img new file mode 100644 index 0000000..179d69e Binary files /dev/null and b/tests/images/Yaesu_FT3D_R.img differ diff --git a/tests/images/Yaesu_FTM-3200D_R.img b/tests/images/Yaesu_FTM-3200D_R.img new file mode 100644 index 0000000..d3f3f66 Binary files /dev/null and b/tests/images/Yaesu_FTM-3200D_R.img differ diff --git a/tests/images/Yaesu_FTM-350.img b/tests/images/Yaesu_FTM-350.img new file mode 100644 index 0000000..1e19c47 Binary files /dev/null and b/tests/images/Yaesu_FTM-350.img differ diff --git a/tests/images/Yaesu_VX-2.img b/tests/images/Yaesu_VX-2.img new file mode 100644 index 0000000..bc237fd Binary files /dev/null and b/tests/images/Yaesu_VX-2.img differ diff --git a/tests/images/Yaesu_VX-3.img b/tests/images/Yaesu_VX-3.img new file mode 100644 index 0000000..eedefd3 Binary files /dev/null and b/tests/images/Yaesu_VX-3.img differ diff --git a/tests/images/Yaesu_VX-5.img b/tests/images/Yaesu_VX-5.img new file mode 100644 index 0000000..522c0f3 Binary files /dev/null and b/tests/images/Yaesu_VX-5.img differ diff --git a/tests/images/Yaesu_VX-6.img b/tests/images/Yaesu_VX-6.img new file mode 100644 index 0000000..509099d Binary files /dev/null and b/tests/images/Yaesu_VX-6.img differ diff --git a/tests/images/Yaesu_VX-7.img b/tests/images/Yaesu_VX-7.img new file mode 100644 index 0000000..2542f8e Binary files /dev/null and b/tests/images/Yaesu_VX-7.img differ diff --git a/tests/images/Yaesu_VX-8DR.img b/tests/images/Yaesu_VX-8DR.img new file mode 100644 index 0000000..299fdc1 Binary files /dev/null and b/tests/images/Yaesu_VX-8DR.img differ diff --git a/tests/images/Yaesu_VX-8GE.img b/tests/images/Yaesu_VX-8GE.img new file mode 100644 index 0000000..a140121 Binary files /dev/null and b/tests/images/Yaesu_VX-8GE.img differ diff --git a/tests/images/Yaesu_VX-8R.img b/tests/images/Yaesu_VX-8R.img new file mode 100644 index 0000000..0ae38f7 Binary files /dev/null and b/tests/images/Yaesu_VX-8R.img differ diff --git a/tests/run_tests b/tests/run_tests new file mode 100755 index 0000000..226de0c --- /dev/null +++ b/tests/run_tests @@ -0,0 +1,3 @@ +#!/bin/bash + +exec python $(readlink -f $0).py $* diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100755 index 0000000..e13fd8c --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,1372 @@ +#!/usr/bin/env python +# +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +from builtins import bytes +import copy +import traceback +import sys +import os +import shutil +import glob +import tempfile +import time +from optparse import OptionParser +from serial import Serial + +# change to the tests directory +scriptdir = os.path.dirname(sys.argv[0]) +if scriptdir: + os.chdir(scriptdir) + +sys.path.insert(0, "../") + +os.environ['CHIRP_TESTENV'] = 'sigh' +import logging +from chirp import logger + +class LoggerOpts(object): + quiet = 2 + verbose = 0 + log_file = os.path.join('logs', 'debug.log') + log_level = logging.DEBUG + +if not os.path.exists("logs"): + os.mkdir("logs") +logger.handle_options(LoggerOpts()) + +from chirp import CHIRP_VERSION +# FIXME: Not all drivers are py3 compatible in syntax, so punt on this +# until that time, and defer to the safe import loop below. +# from chirp.drivers import * +from chirp import chirp_common, directory +from chirp import import_logic, memmap, settings, errors +from chirp import settings + +directory.safe_import_drivers() + +from chirp.drivers import generic_csv + +TESTS = {} + +time.sleep = lambda s: None + + +class TestError(Exception): + def get_detail(self): + return str(self) + + +class TestInternalError(TestError): + pass + + +class TestCrashError(TestError): + def __init__(self, tb, exc, args): + Exception.__init__(self, str(exc)) + self.__tb = tb + self.__exc = exc + self.__args = args + self.__mytb = "".join(traceback.format_stack()) + + def __str__(self): + return str(self.__exc) + + def get_detail(self): + return str(self.__exc) + os.linesep + \ + ("Args were: %s" % self.__args) + os.linesep + \ + self.__tb + os.linesep + \ + "Called from:" + os.linesep + self.__mytb + + def get_original_exception(self): + return self.__exc + + +class TestFailedError(TestError): + def __init__(self, msg, detail=""): + TestError.__init__(self, msg) + self._detail = detail + + def get_detail(self): + return self._detail + + +class TestSkippedError(TestError): + pass + + +def get_tb(): + return traceback.format_exc() + + +class TestWrapper: + def __init__(self, dstclass, filename, dst=None): + self._ignored_exceptions = [] + self._dstclass = dstclass + self._filename = filename + self._make_reload = False + self._dst = dst + self.open() + + def pass_exception_type(self, et): + self._ignored_exceptions.append(et) + + def nopass_exception_type(self, et): + self._ignored_exceptions.remove(et) + + def make_reload(self): + self._make_reload = True + + def open(self): + if self._dst: + self._dst.load_mmap(self._filename) + else: + self._dst = self._dstclass(self._filename) + + def close(self): + self._dst.save_mmap(self._filename) + + def do(self, function, *args, **kwargs): + if self._make_reload: + try: + self.open() + except Exception as e: + raise TestCrashError(get_tb(), e, "[Loading]") + + try: + fn = getattr(self._dst, function) + except KeyError: + raise TestInternalError("Model lacks function `%s'" % function) + + try: + ret = fn(*args, **kwargs) + except Exception as e: + if type(e) in self._ignored_exceptions: + raise e + details = str(args) + str(kwargs) + for arg in args: + if isinstance(arg, chirp_common.Memory): + details += os.linesep + \ + os.linesep.join(["%s:%s" % (k, v) for k, v + in list(arg.__dict__.items())]) + raise TestCrashError(get_tb(), e, details) + + if self._make_reload: + try: + self.close() + except Exception as e: + raise TestCrashError(get_tb(), e, "[Saving]") + + return ret + + def get_id(self): + return "%s %s %s" % (self._dst.VENDOR, + self._dst.MODEL, + self._dst.VARIANT) + + def get_radio(self): + return self._dst + + +class TestCase: + def __init__(self, wrapper): + self._wrapper = wrapper + + def prepare(self): + pass + + def run(self): + "Return True or False for Pass/Fail" + pass + + def cleanup(self): + pass + + def compare_mem(self, a, b, ignore=None): + rf = self._wrapper.do("get_features") + + if a.tmode == "Cross": + tx_mode, rx_mode = a.cross_mode.split("->") + + for k, v in list(a.__dict__.items()): + if ignore and k in ignore: + continue + if k == "power": + continue # FIXME + elif k == "immutable": + continue + elif k == "name": + if not rf.has_name: + continue # Don't complain about name, if not supported + else: + # Name mismatch fair if filter_name() is right + v = self._wrapper.do("filter_name", v).rstrip() + elif k == "tuning_step" and not rf.has_tuning_step: + continue + elif k == "rtone" and not ( + a.tmode == "Tone" or + (a.tmode == "TSQL" and not rf.has_ctone) or + (a.tmode == "Cross" and tx_mode == "Tone") or + (a.tmode == "Cross" and rx_mode == "Tone" and + not rf.has_ctone) + ): + continue + elif k == "ctone" and (not rf.has_ctone or + not (a.tmode == "TSQL" or + (a.tmode == "Cross" and + rx_mode == "Tone"))): + continue + elif k == "dtcs" and not ( + (a.tmode == "DTCS" and not rf.has_rx_dtcs) or + (a.tmode == "Cross" and tx_mode == "DTCS") or + (a.tmode == "Cross" and rx_mode == "DTCS" and + not rf.has_rx_dtcs)): + continue + elif k == "rx_dtcs" and (not rf.has_rx_dtcs or + not (a.tmode == "Cross" and + rx_mode == "DTCS")): + continue + elif k == "offset" and not a.duplex: + continue + elif k == "cross_mode" and a.tmode != "Cross": + continue + + try: + if b.__dict__[k] != v: + msg = "Field `%s' " % k + \ + "is `%s', " % b.__dict__[k] + \ + "expected `%s' " % v + # If we set a channel that came back with a duplex + # of 'off', we may have been outside the transmit range of + # the radio, so we should not fail. + if k == "duplex" and b.__dict__[k] == "off": + continue + details = msg + details += os.linesep + "### Wanted:" + os.linesep + details += os.linesep.join(["%s:%s" % (k, v) for k, v + in list(a.__dict__.items())]) + details += os.linesep + "### Got:" + os.linesep + details += os.linesep.join(["%s:%s" % (k, v) for k, v + in list(b.__dict__.items())]) + raise TestFailedError(msg, details) + except KeyError as e: + print(sorted(a.__dict__.keys())) + print(sorted(b.__dict__.keys())) + raise + + +class TestCaseCopyAll(TestCase): + "Copy Memories From CSV" + + def __str__(self): + return "CopyAll" + + def prepare(self): + testbase = os.path.dirname(os.path.abspath(__file__)) + source = os.path.join(testbase, 'images', 'Generic_CSV.csv') + self._src = generic_csv.CSVRadio(source) + + def run(self): + src_rf = self._src.get_features() + bounds = src_rf.memory_bounds + + dst_rf = self._wrapper.do("get_features") + dst_number = dst_rf.memory_bounds[0] + + failures = [] + + for number in range(bounds[0], bounds[1]): + src_mem = self._src.get_memory(number) + if src_mem.empty: + continue + + try: + dst_mem = import_logic.import_mem(self._wrapper.get_radio(), + src_rf, src_mem, + overrides={ + "number": dst_number}) + import_logic.import_bank(self._wrapper.get_radio(), + self._src, + dst_mem, + src_mem) + except import_logic.DestNotCompatible: + continue + except import_logic.ImportError as e: + failures.append(TestFailedError("<%i>: Import Failed: %s" % + (dst_number, e))) + continue + except Exception as e: + raise TestCrashError(get_tb(), e, "[Import]") + + self._wrapper.do("set_memory", dst_mem) + ret_mem = self._wrapper.do("get_memory", dst_number) + + try: + self.compare_mem(dst_mem, ret_mem) + except TestFailedError as e: + failures.append( + TestFailedError("<%i>: %s" % (number, e), e.get_detail())) + + return failures +TESTS["CopyAll"] = TestCaseCopyAll + + +class TestCaseBruteForce(TestCase): + def __str__(self): + return "BruteForce" + + def set_and_compare(self, m): + msgs = self._wrapper.do("validate_memory", m) + if msgs: + # If the radio correctly refuses memories it can't + # store, don't fail + return + + self._wrapper.do("set_memory", m) + ret_m = self._wrapper.do("get_memory", m.number) + + # Damned Baofeng radios don't seem to properly store + # shift and direction, so be gracious here + if m.duplex == "split" and ret_m.duplex in ["-", "+"]: + ret_m.offset = ret_m.freq + \ + (ret_m.offset * int(ret_m.duplex + "1")) + ret_m.duplex = "split" + + self.compare_mem(m, ret_m) + + def do_tone(self, m, rf): + self._wrapper.pass_exception_type(errors.UnsupportedToneError) + for tone in chirp_common.TONES: + for tmode in rf.valid_tmodes: + if tmode not in chirp_common.TONE_MODES: + continue + elif tmode in ["DTCS", "DTCS-R", "Cross"]: + continue # We'll test DCS and Cross tones separately + + m.tmode = tmode + if tmode == "": + pass + elif tmode == "Tone": + m.rtone = tone + elif tmode in ["TSQL", "TSQL-R"]: + if rf.has_ctone: + m.ctone = tone + else: + m.rtone = tone + else: + raise TestInternalError("Unknown tone mode `%s'" % tmode) + + try: + self.set_and_compare(m) + except errors.UnsupportedToneError as e: + # If a radio doesn't support a particular tone value, + # don't punish it + pass + self._wrapper.nopass_exception_type(errors.UnsupportedToneError) + + def do_dtcs(self, m, rf): + if not rf.has_dtcs: + return + + m.tmode = "DTCS" + for code in rf.valid_dtcs_codes: + m.dtcs = code + self.set_and_compare(m) + + if not rf.has_dtcs_polarity: + return + + for pol in rf.valid_dtcs_pols: + m.dtcs_polarity = pol + self.set_and_compare(m) + + def do_cross(self, m, rf): + if not rf.has_cross: + return + + m.tmode = "Cross" + # No fair asking a radio to detect two identical tones as Cross instead + # of TSQL + m.rtone = 100.0 + m.ctone = 107.2 + m.dtcs = 506 + m.rx_dtcs = 516 + for cross_mode in rf.valid_cross_modes: + m.cross_mode = cross_mode + self.set_and_compare(m) + + def do_duplex(self, m, rf): + for duplex in rf.valid_duplexes: + if duplex not in ["", "-", "+", "split"]: + continue + if duplex == "split" and not rf.can_odd_split: + raise TestFailedError("Forgot to set rf.can_odd_split!") + if duplex == "split": + m.offset = rf.valid_bands[0][1] - 100000 + m.duplex = duplex + self.set_and_compare(m) + + if rf.can_odd_split and "split" not in rf.valid_duplexes: + raise TestFailedError("Paste error: rf.can_odd_split defined, but " + "split duplex not supported.") + + def do_skip(self, m, rf): + for skip in rf.valid_skips: + m.skip = skip + self.set_and_compare(m) + + def do_mode(self, m, rf): + def ensure_urcall(call): + l = self._wrapper.do("get_urcall_list") + l[0] = call + self._wrapper.do("set_urcall_list", l) + + def ensure_rptcall(call): + l = self._wrapper.do("get_repeater_call_list") + l[0] = call + self._wrapper.do("set_repeater_call_list", l) + + def freq_is_ok(freq): + for lo, hi in rf.valid_bands: + if freq > lo and freq < hi: + return True + return False + + successes = 0 + for mode in rf.valid_modes: + tmp = copy.deepcopy(m) + if mode not in chirp_common.MODES: + continue + if mode == "DV": + tmp = chirp_common.DVMemory() + try: + ensure_urcall(tmp.dv_urcall) + ensure_rptcall(tmp.dv_rpt1call) + ensure_rptcall(tmp.dv_rpt2call) + except IndexError: + if rf.requires_call_lists: + raise + else: + # This radio may not do call lists at all, + # so let it slide + pass + if mode == "FM" and freq_is_ok(tmp.freq + 100000000): + # Some radios don't support FM below approximately 30MHz, + # so jump up by 100MHz, if they support that + tmp.freq += 100000000 + + tmp.mode = mode + + if rf.validate_memory(tmp): + # A result (of error messages) from validate means the radio + # thinks this is invalid, so don't fail the test + print('Failed to validate %s: %s' % (tmp, rf.validate_memory(tmp))) + continue + + self.set_and_compare(tmp) + successes += 1 + + if (not successes) and rf.valid_modes: + raise TestFailedError("All modes were skipped, " + "something went wrong") + + def run(self): + rf = self._wrapper.do("get_features") + + def clean_mem(): + m = chirp_common.Memory() + m.number = rf.memory_bounds[0] + try: + m.mode = rf.valid_modes[0] + except IndexError: + pass + if rf.valid_bands: + m.freq = rf.valid_bands[0][0] + 600000 + else: + m.freq = 146520000 + if m.freq < 30000000 and "AM" in rf.valid_modes: + m.mode = "AM" + return m + + tests = [ + self.do_tone, + self.do_dtcs, + self.do_cross, + self.do_duplex, + self.do_skip, + self.do_mode, + ] + for test in tests: + test(clean_mem(), rf) + + if 12.5 in rf.valid_tuning_steps and \ + "split" in rf.valid_duplexes: + m = clean_mem() + if rf.valid_bands: + m.offset = rf.valid_bands[0][1] - 12500 + else: + m.offset = 151137500 + m.duplex = "split" + self.set_and_compare(m) + + return [] +TESTS["BruteForce"] = TestCaseBruteForce + + +class TestCaseEdges(TestCase): + def __str__(self): + return "Edges" + + def _mem(self, rf): + m = chirp_common.Memory() + m.freq = rf.valid_bands[0][0] + 1000000 + if m.freq < 30000000 and "AM" in rf.valid_modes: + m.mode = "AM" + else: + try: + m.mode = rf.valid_modes[0] + except IndexError: + pass + + for i in range(*rf.memory_bounds): + m.number = i + if not self._wrapper.do("validate_memory", m): + return m + + raise TestSkippedError("No mutable memory locations found") + + def do_longname(self, rf): + m = self._mem(rf) + m.name = ("X" * 256) # Should be longer than any radio can handle + m.name = self._wrapper.do("filter_name", m.name) + + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + + self.compare_mem(m, n) + + def do_badname(self, rf): + m = self._mem(rf) + + ascii = "".join([chr(x) for x in range(ord(" "), ord("~")+1)]) + for i in range(0, len(ascii), 4): + m.name = self._wrapper.do("filter_name", ascii[i:i+4]) + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + self.compare_mem(m, n) + + def do_bandedges(self, rf): + m = self._mem(rf) + min_step = min(rf.has_tuning_step and rf.valid_tuning_steps or [10]) + + for low, high in rf.valid_bands: + for freq in (low, high - int(min_step * 1000)): + m.freq = freq + if self._wrapper.do("validate_memory", m): + # Radio doesn't like it, so skip + continue + + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + self.compare_mem(m, n) + + def do_oddsteps(self, rf): + odd_steps = { + 145000000: [145856250, 145862500], + 445000000: [445856250, 445862500], + 862000000: [862731250, 862737500], + } + + m = self._mem(rf) + + for low, high in rf.valid_bands: + for band, totest in list(odd_steps.items()): + if band < low or band > high: + continue + for testfreq in totest: + step = chirp_common.required_step(testfreq) + if step not in rf.valid_tuning_steps: + continue + + m.freq = testfreq + m.tuning_step = step + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + self.compare_mem(m, n, ignore=['tuning_step']) + + def do_empty_to_not(self, rf): + firstband = rf.valid_bands[0] + testfreq = firstband[0] + for loc in range(*rf.memory_bounds): + m = self._wrapper.do('get_memory', loc) + if m.empty: + m.empty = False + m.freq = testfreq + self._wrapper.do('set_memory', m) + m = self._wrapper.do('get_memory', loc) + if m.freq == testfreq: + return + else: + raise TestFailedError('Radio failed to set an empty ' + 'location (%i)' % loc) + + def do_delete_memory(self, rf): + firstband = rf.valid_bands[0] + testfreq = firstband[0] + for loc in range(*rf.memory_bounds): + if loc == rf.memory_bounds[0]: + # Some radios will not allow you to delete the first memory + # /me glares at yaesu + continue + m = self._wrapper.do('get_memory', loc) + if not m.empty: + m.empty = True + self._wrapper.do('set_memory', m) + m = self._wrapper.do('get_memory', loc) + if not m.empty: + raise TestFailedError('Radio refused to delete a memory ' + 'location (%i)' % loc) + else: + return + + def run(self): + rf = self._wrapper.do("get_features") + + if not rf.valid_bands: + raise TestFailedError("Radio does not provide valid bands!") + + self.do_longname(rf) + self.do_bandedges(rf) + self.do_oddsteps(rf) + self.do_badname(rf) + if rf.can_delete: + self.do_empty_to_not(rf) + self.do_delete_memory(rf) + + return [] + +TESTS["Edges"] = TestCaseEdges + + +class TestCaseSettings(TestCase): + def __str__(self): + return "Settings" + + def do_get_settings(self, rf): + lst = self._wrapper.do("get_settings") + if not isinstance(lst, list): + raise TestFailedError("Invalid Radio Settings") + + def do_same_settings(self, rf): + o = self._wrapper.do("get_settings") + self._wrapper.do("set_settings", o) + n = self._wrapper.do("get_settings") + list(map(self.compare_settings, o, n)) + + @staticmethod + def compare_settings(a, b): + try: + if isinstance(a, settings.RadioSettingValue): + raise TypeError('Hit bottom') + list(map(TestCaseSettings.compare_settings, a, b)) + except TypeError: + if a.get_value() != b.get_value(): + msg = "Field is `%s', " % b + \ + "expected `%s' " % a + details = msg + raise TestFailedError(msg, details) + + def run(self): + rf = self._wrapper.do("get_features") + + if not rf.has_settings: + raise TestSkippedError("Settings not supported") + + self.do_get_settings(rf) + self.do_same_settings(rf) + + return [] + +TESTS["Settings"] = TestCaseSettings + + +class TestCaseBanks(TestCase): + def __str__(self): + return "Banks" + + def _do_bank_names(self, rf, testname): + bm = self._wrapper.do("get_bank_model") + banks = bm.get_mappings() + + for bank in banks: + name = bank.get_name() + try: + bank.set_name(testname) + except AttributeError: + return [], [] + except Exception as e: + if str(e) == "Not implemented": + return [], [] + else: + raise e + + return banks, bm.get_mappings() + + def do_bank_names(self, rf): + banks, newbanks = self._do_bank_names(rf, "T") + + for i in range(0, len(banks)): + if banks[i].get_name() != newbanks[i].get_name(): + raise TestFailedError("Bank names not preserved", + "Tried %s on %i\nGot %s" % (banks[i], + i, + newbanks[i])) + + def do_bank_names_toolong(self, rf): + testname = "Not possibly this long" + banks, newbanks = self._do_bank_names(rf, testname) + + for i in range(0, len(newbanks)): + # Truncation is allowed, but not failure + if not testname.lower().startswith(str(newbanks[i]).lower()): + raise TestFailedError("Bank names not properly truncated", + "Tried: %s on %i\nGot: %s" % + (testname, i, newbanks[i])) + + def do_bank_names_no_trailing_whitespace(self, rf): + banks, newbanks = self._do_bank_names(rf, "foo ") + + for bank in newbanks: + if str(bank) != str(bank).rstrip(): + raise TestFailedError("Bank names stored with " + + "trailing whitespace") + + def do_bank_store(self, rf): + loc = rf.memory_bounds[0] + mem = chirp_common.Memory() + mem.number = loc + mem.freq = rf.valid_bands[0][0] + 100000 + + # Make sure the memory is empty and we create it from scratch + mem.empty = True + self._wrapper.do("set_memory", mem) + + mem.empty = False + self._wrapper.do("set_memory", mem) + + model = self._wrapper.do("get_bank_model") + + # If in your bank model every channel has to be tied to a bank, just + # add a variable named channelAlwaysHasBank to it and make it True + try: + channelAlwaysHasBank = model.channelAlwaysHasBank + except: + channelAlwaysHasBank = False + + mem_banks = model.get_memory_mappings(mem) + if channelAlwaysHasBank: + if len(mem_banks) == 0: + raise TestFailedError("Freshly-created memory has no banks " + + "and it should", "Bank: %s" % + str(mem_banks)) + else: + if len(mem_banks) != 0: + raise TestFailedError("Freshly-created memory has banks " + + "and should not", "Bank: %s" % + str(mem_banks)) + + banks = model.get_mappings() + + def verify(bank): + if bank not in model.get_memory_mappings(mem): + return "Memory does not claim bank" + + if loc not in [x.number for x in model.get_mapping_memories(bank)]: + return "Bank does not claim memory" + + return None + + model.add_memory_to_mapping(mem, banks[0]) + reason = verify(banks[0]) + if reason is not None: + raise TestFailedError("Setting memory bank does not persist", + "%s\nMemory banks:%s\nBank memories:%s" % + (reason, + model.get_memory_mappings(mem), + model.get_mapping_memories(banks[0]))) + + model.remove_memory_from_mapping(mem, banks[0]) + reason = verify(banks[0]) + if reason is None and not channelAlwaysHasBank: + raise TestFailedError("Memory remains in bank after remove", + reason) + + try: + model.remove_memory_from_mapping(mem, banks[0]) + did_error = False + except Exception: + did_error = True + + if not did_error and not channelAlwaysHasBank: + raise TestFailedError("Removing memory from non-member bank " + + "did not raise Exception") + + def do_bank_index(self, rf): + if not rf.has_bank_index: + return + + loc = rf.memory_bounds[0] + mem = chirp_common.Memory() + mem.number = loc + mem.freq = rf.valid_bands[0][0] + 100000 + + self._wrapper.do("set_memory", mem) + + model = self._wrapper.do("get_bank_model") + banks = model.get_mappings() + index_bounds = model.get_index_bounds() + + model.add_memory_to_mapping(mem, banks[0]) + for i in range(0, *index_bounds): + model.set_memory_index(mem, banks[0], i) + if model.get_memory_index(mem, banks[0]) != i: + raise TestFailedError("Bank index not persisted") + + suggested_index = model.get_next_mapping_index(banks[0]) + if suggested_index not in list(range(*index_bounds)): + raise TestFailedError("Suggested bank index not in valid range", + "Got %i, range is %s" % (suggested_index, + index_bounds)) + + def run(self): + rf = self._wrapper.do("get_features") + + if not rf.has_bank: + raise TestSkippedError("Banks not supported") + + self.do_bank_names(rf) + self.do_bank_names_toolong(rf) + self.do_bank_names_no_trailing_whitespace(rf) + self.do_bank_store(rf) + # Again to make sure we clear bank info on delete + self.do_bank_store(rf) + self.do_bank_index(rf) + + return [] + +TESTS["Banks"] = TestCaseBanks + + +class TestCaseDetect(TestCase): + def __str__(self): + return "Detect" + + def run(self): + if isinstance(self._wrapper._dst, chirp_common.LiveRadio): + raise TestSkippedError("This is a live radio") + + filename = self._wrapper._filename + + try: + radio = directory.get_radio_by_image(filename) + except Exception as e: + raise TestFailedError("Failed to detect", str(e)) + + if radio.__class__.__name__ == 'DynamicRadioAlias': + # This was detected via metadata and wrapped, which means + # we found the appropriate class. + pass + elif issubclass(self._wrapper._dstclass, radio.__class__): + pass + elif issubclass(radio.__class__, self._wrapper._dstclass): + pass + elif radio.__class__ != self._wrapper._dstclass: + raise TestFailedError("%s detected as %s" % + (self._wrapper._dstclass, radio.__class__)) + return [] + +TESTS["Detect"] = TestCaseDetect + + +class TestCaseClone(TestCase): + class SerialNone: + def __init__(self, isbytes): + self.isbytes = isbytes + self.mismatch = False + self.mismatch_at = None + + def read(self, size): + if self.isbytes: + return b"" + else: + return "" + + def write(self, data): + expected = self.isbytes and bytes or str + if six.PY2: + # One driver uses bytearray() which will trigger this + # check even though it works fine on py2. So, only + # do this check for PY3 which is where it matters + # anyway. + pass + elif not self.mismatch and not isinstance(data, expected): + self.mismatch = True + self.mismatch_at = ''.join(traceback.format_stack()) + pass + + def setBaudrate(self, rate): + pass + + def setTimeout(self, timeout): + pass + + def setParity(self, parity): + pass + + def __str__(self): + return self.__class__.__name__.replace("Serial", "") + + class SerialError(SerialNone): + def read(self, size): + raise Exception("Foo") + + def write(self, data): + raise Exception("Bar") + + class SerialGarbage(SerialNone): + def read(self, size): + if self.isbytes: + buf = [] + for i in range(0, size): + buf += i % 256 + return bytes(buf) + else: + buf = "" + for i in range(0, size): + buf += chr(i % 256) + return buf + + class SerialShortGarbage(SerialNone): + def read(self, size): + if self.isbytes: + return b'\x00' * (size - 1) + else: + return "\x00" * (size - 1) + + def __str__(self): + return "Clone" + + def _run(self, serial): + error = None + live = isinstance(self._wrapper._dst, chirp_common.LiveRadio) + clone = isinstance(self._wrapper._dst, chirp_common.CloneModeRadio) + + if not clone and not live: + raise TestSkippedError('Does not support clone') + + try: + radio = self._wrapper._dst.__class__(serial) + radio.status_fn = lambda s: True + except Exception as e: + error = e + + if not live: + if error is not None: + raise TestFailedError("Clone radio tried to read from " + + "serial on init") + else: + if not isinstance(error, errors.RadioError): + raise TestFailedError("Live radio didn't notice serial " + + "was dead on init") + return [] # Nothing more to test on an error'd live radio + + error = None + try: + radio.sync_in() + except Exception as e: + error = e + + if error is None: + raise TestFailedError("Radio did not raise exception " + + "with %s data" % serial, + "On sync_in()") + elif not isinstance(error, errors.RadioError): + raise TestFailedError("Radio did not raise RadioError " + + "with %s data" % serial, + "sync_in() Got: %s (%s)\n%s" % + (error.__class__.__name__, + error, get_tb())) + + if radio.NEEDS_COMPAT_SERIAL: + radio._mmap = memmap.MemoryMap("\x00" * (1024 * 128)) + else: + radio._mmap = memmap.MemoryMapBytes(bytes(b"\x00") * (1024 * 128)) + + error = None + try: + radio.sync_out() + except Exception as e: + error = e + + if error is None: + raise TestFailedError("Radio did not raise exception " + + "with %s data" % serial, + "On sync_out()") + elif not isinstance(error, errors.RadioError): + raise TestFailedError("Radio did not raise RadioError " + + "with %s data" % serial, + "sync_out(): Got: %s (%s)" % + (error.__class__.__name__, error)) + + if serial.mismatch: + raise TestFailedError("Radio tried to write the wrong " + "type of data to the %s pipe." % ( + serial.__class__.__name__), + "TestClone:%s\n%s" % ( + serial.__class__.__name__, + serial.mismatch_at)) + + return [] + + def run(self): + isbytes = not self._wrapper._dst.NEEDS_COMPAT_SERIAL + self._run(self.SerialError(isbytes)) + self._run(self.SerialNone(isbytes)) + self._run(self.SerialGarbage(isbytes)) + self._run(self.SerialShortGarbage(isbytes)) + return [] + +TESTS["Clone"] = TestCaseClone + + +class TestOutput: + def __init__(self, output=None): + if not output: + output = sys.stdout + self._out = output + + def prepare(self): + pass + + def cleanup(self): + pass + + def _print(self, string): + print(string, file=self._out) + + def report(self, rclass, tc, msg, e): + name = ("%s %s" % (rclass.MODEL, rclass.VARIANT))[:13] + self._print("%9s %-13s %-10s %s %s" % (rclass.VENDOR.split(" ")[0], + name, + tc, + msg, e)) + + +class TestOutputANSI(TestOutput): + def __init__(self, output=None): + TestOutput.__init__(self, output) + self.__counts = { + "PASSED": 0, + "FAILED": 0, + "CRASHED": 0, + "SKIPPED": 0, + } + self.__total = 0 + + def report(self, rclass, tc, msg, e): + self.__total += 1 + self.__counts[msg] += 1 + msg += ":" + if os.isatty(1): + if msg == "PASSED:": + msg = "\033[1;32m%8s\033[0m" % msg + elif msg == "FAILED:": + msg = "\033[1;41m%8s\033[0m" % msg + elif msg == "CRASHED:": + msg = "\033[1;45m%8s\033[0m" % msg + elif msg == "SKIPPED:": + msg = "\033[1;32m%8s\033[0m" % msg + else: + msg = "%8s" % msg + + TestOutput.report(self, rclass, tc, msg, e) + + def cleanup(self): + self._print("-" * 70) + self._print("Results:") + self._print(" %-7s: %i" % ("TOTAL", self.__total)) + for t, c in list(self.__counts.items()): + self._print(" %-7s: %i" % (t, c)) + + +class TestOutputHTML(TestOutput): + def __init__(self, filename): + self._filename = filename + + def prepare(self): + print("Writing to %s" % self._filename, end=' ') + sys.stdout.flush() + self._out = file(self._filename, "w") + s = """ + + +Test report for CHIRP version %s + + + +

Test report for CHIRP version %s

+

Generated on %s (%s)

+
+ + + + +""" % (CHIRP_VERSION, CHIRP_VERSION, time.strftime("%x at %X"), os.name) + print(s, file=self._out) + + def cleanup(self): + print("
VendorModelTest CaseStatusMessage
", file=self._out) + self._out.close() + print("Done") + + def report(self, rclass, tc, msg, e): + s = ("" % msg) + \ + ("%s" % rclass.VENDOR) + \ + ("%s %s" % + (rclass.MODEL, rclass.VARIANT)) + \ + ("%s" % tc) + \ + ("%s" % (msg, msg)) + \ + ("%s" % e) + \ + "" + print(s, file=self._out) + sys.stdout.write(".") + sys.stdout.flush() + + +class TestRunner: + def __init__(self, images_dir, test_list, test_out): + self._images_dir = images_dir + self._test_list = test_list + self._test_out = test_out + if not os.path.exists("tmp"): + os.mkdir("tmp") + + def _make_list(self): + run_list = [] + images = glob.glob(os.path.join(self._images_dir, "*.img")) + for image in sorted(images): + drv_name, _ = os.path.splitext(os.path.basename(image)) + run_list.append((directory.get_radio(drv_name), image)) + return run_list + + def report(self, rclass, tc, msg, e): + self._test_out.report(rclass, tc, msg, e) + + def log(self, rclass, tc, e): + fn = "logs/%s_%s.log" % (directory.radio_class_id(rclass), tc) + log = file(fn, "a") + print("---- Begin test %s ----" % tc, file=log) + log.write(e.get_detail()) + print(file=log) + print("---- End test %s ----" % tc, file=log) + log.close() + + def nuke_log(self, rclass, tc): + fn = "logs/%s_%s.log" % (directory.radio_class_id(rclass), tc) + if os.path.exists(fn): + os.remove(fn) + + def _run_one(self, rclass, parm, dst=None): + nfailed = 0 + for tcclass in self._test_list: + nprinted = 0 + tw = TestWrapper(rclass, parm, dst=dst) + tc = tcclass(tw) + + self.nuke_log(rclass, tc) + + tc.prepare() + + try: + failures = tc.run() + for e in failures: + self.report(rclass, tc, "FAILED", e) + if e.get_detail(): + self.log(rclass, tc, e) + nfailed += 1 + nprinted += 1 + except TestFailedError as e: + self.report(rclass, tc, "FAILED", e) + if e.get_detail(): + self.log(rclass, tc, e) + nfailed += 1 + nprinted += 1 + except TestCrashError as e: + self.report(rclass, tc, "CRASHED", e) + self.log(rclass, tc, e) + nfailed += 1 + nprinted += 1 + except TestSkippedError as e: + self.report(rclass, tc, "SKIPPED", e) + self.log(rclass, tc, e) + nprinted += 1 + + tc.cleanup() + + if not nprinted: + self.report(rclass, tc, "PASSED", "All tests") + + return nfailed + + def run_rclass_image(self, rclass, image, dst=None): + rid = "%s_%s_" % (rclass.VENDOR, rclass.MODEL) + rid = rid.replace("/", "_") + # Do this for things like Generic_CSV, that demand it + _base, ext = os.path.splitext(image) + testimage = tempfile.mktemp(ext, rid) + shutil.copy(image, testimage) + + try: + tw = TestWrapper(rclass, testimage, dst=dst) + rf = tw.do("get_features") + if rf.has_sub_devices: + devices = tw.do("get_sub_devices") + failed = 0 + for dev in devices: + failed += self.run_rclass_image(dev.__class__, image, dst=dev) + return failed + else: + return self._run_one(rclass, image, dst=dst) + finally: + os.remove(testimage) + + def run_list(self, run_list): + def _key(pair): + return pair[0].VENDOR + pair[0].MODEL + pair[0].VARIANT + failed = 0 + for rclass, image in sorted(run_list, key=_key): + failed += self.run_rclass_image(rclass, image) + return failed + + def run_all(self): + run_list = self._make_list() + return self.run_list(run_list) + + def run_one(self, drv_name): + return self.run_rclass_image(directory.get_radio(drv_name), + os.path.join("images", + "%s.img" % drv_name)) + + def run_one_live(self, drv_name, port): + rclass = directory.get_radio(drv_name) + pipe = Serial(port=port, baudrate=rclass.BAUD_RATE, timeout=0.5) + tw = TestWrapper(rclass, pipe) + rf = tw.do("get_features") + if rf.has_sub_devices: + devices = tw.do("get_sub_devices") + failed = 0 + for device in devices: + failed += self._run_one(device.__class__, pipe) + return failed + else: + return self._run_one(rclass, pipe) + +if __name__ == "__main__": + import sys + + images = glob.glob("images/*.img") + tests = [os.path.splitext(os.path.basename(img))[0] for img in images] + + op = OptionParser() + op.add_option("-d", "--driver", dest="driver", default=None, + help="Driver to test (omit for all)") + op.add_option("-t", "--test", dest="test", default=None, + help="Test to run (omit for all)") + op.add_option("-e", "--exclude", dest="exclude", default=None, + help="Test to exclude") + op.add_option("", "--html", dest="html", default=None, + help="Output to HTML file") + op.add_option("-l", "--live", dest="live", default=None, + help="Live radio on this port (requires -d)") + op.usage = """ +Available drivers: +%s +Available tests: +%s +""" % ("\n".join([" %s" % x for x in sorted(tests)]), + "\n".join([" %s" % x for x in sorted(TESTS.keys())])) + + (options, args) = op.parse_args() + + if options.html: + test_out = TestOutputHTML(options.html) + else: + stdout = sys.stdout + if not os.path.exists("logs"): + os.mkdir("logs") + sys.stdout = file("logs/verbose", "w") + test_out = TestOutputANSI(stdout) + + test_out.prepare() + + if options.exclude: + del TESTS[options.exclude] + + if options.test: + tr = TestRunner("images", [TESTS[options.test]], test_out) + else: + tr = TestRunner("images", list(TESTS.values()), test_out) + + if options.live: + if not options.driver: + print("Live mode requires a driver to be specified") + sys.exit(1) + failed = tr.run_one_live(options.driver, options.live) + elif options.driver: + failed = tr.run_one(options.driver) + else: + failed = tr.run_all() + + test_out.cleanup() + + sys.exit(failed) diff --git a/tests/test_drivers.py b/tests/test_drivers.py new file mode 100644 index 0000000..ba8aaf9 --- /dev/null +++ b/tests/test_drivers.py @@ -0,0 +1,24 @@ +import sys +import unittest + +from chirp import directory +from tests import load_tests + + +class TestSuiteAdapter(object): + """Adapter for pytest since it doesn't support the loadTests() protocol""" + + def __init__(self, locals): + self.locals = locals + + def loadTestsFromTestCase(self, test_cls): + self.locals[test_cls.__name__] = test_cls + + @staticmethod + def addTests(tests): + pass + + +directory.safe_import_drivers() +adapter = TestSuiteAdapter(locals()) +load_tests(adapter, None, None, suite=adapter) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/base.py b/tests/unit/base.py new file mode 100644 index 0000000..1e112ce --- /dev/null +++ b/tests/unit/base.py @@ -0,0 +1,50 @@ +import sys +import unittest + +import mock + +try: + import mox +except ImportError: + from mox3 import mox + +import warnings +warnings.simplefilter('ignore', Warning) + + +class BaseTest(unittest.TestCase): + def setUp(self): + __builtins__['_'] = lambda s: s + self.mox = mox.Mox() + + def tearDown(self): + self.mox.UnsetStubs() + self.mox.VerifyAll() + + +pygtk_mocks = ('gtk', 'pango', 'gobject') +pygtk_base_classes = ('gobject.GObject', 'gtk.HBox', 'gtk.Dialog') + + +class DummyBase(object): + def __init__(self, *a, **k): + # gtk.Dialog + self.vbox = mock.MagicMock() + + # gtk.Dialog + def set_position(self, pos): + pass + + +def mock_gtk(): + for module in pygtk_mocks: + sys.modules[module] = mock.MagicMock() + + for path in pygtk_base_classes: + module, base_class = path.split('.') + setattr(sys.modules[module], base_class, DummyBase) + + +def unmock_gtk(): + for module in pygtk_mocks: + del sys.modules[module] diff --git a/tests/unit/test_bitwise.py b/tests/unit/test_bitwise.py new file mode 100644 index 0000000..2b674e8 --- /dev/null +++ b/tests/unit/test_bitwise.py @@ -0,0 +1,341 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import struct +import unittest + +import six + +from chirp import bitwise +from chirp import memmap + + +class BaseTest(unittest.TestCase): + def _compare_structure(self, obj, primitive): + for key, value in primitive.items(): + if isinstance(value, dict): + self._compare_structure(getattr(obj, key), value) + else: + self.assertEqual(type(value)(getattr(obj, key)), value) + + +class TestMemoryMapCoherence(BaseTest): + def test_byte_char_coherence(self): + charmmap = memmap.MemoryMap('00') + # This will to a get_byte_compatible() from chars + obj = bitwise.parse('char foo[2];', charmmap) + self.assertEqual('00', str(obj.foo)) + obj.foo = '11' + # The above assignment happens on the byte-compatible mmap, + # make sure it is still visible in the charmmap we know about. + # This confirms that get_byte_compatible() links the backing + # store of the original mmap to the new one. + self.assertEqual('11', charmmap.get_packed()) + + +class TestBitwiseBaseIntTypes(BaseTest): + def _test_type(self, datatype, _data, value): + data = memmap.MemoryMapBytes(bytes(_data)) + obj = bitwise.parse("%s foo;" % datatype, data) + self.assertEqual(int(obj.foo), value) + self.assertEqual(obj.foo.size(), len(data) * 8) + + obj.foo = 0 + self.assertEqual(int(obj.foo), 0) + self.assertEqual(data.get_packed(), (b"\x00" * (obj.size() // 8))) + + obj.foo = value + self.assertEqual(int(obj.foo), value) + self.assertEqual(data.get_packed(), _data) + + obj.foo = 7 + # Compare against the equivalent real division so we get consistent + # results on py2 and py3 + self.assertEqual(7 // 2, obj.foo // 2) + self.assertEqual(7 / 2, obj.foo / 2) + self.assertEqual(7 / 2.0, obj.foo / 2.0) + + def test_type_u8(self): + self._test_type("u8", b"\x80", 128) + + def test_type_u16(self): + self._test_type("u16", b"\x01\x00", 256) + + def test_type_u24(self): + self._test_type("u24", b"\x80\x00\x00", 2**23) + + def test_type_u32(self): + self._test_type("u32", b"\x80\x00\x00\x00", 2**31) + + def test_type_ul16(self): + self._test_type("ul16", b"\x00\x01", 256) + + def test_type_ul24(self): + self._test_type("ul24", b"\x00\x00\x80", 2**23) + + def test_type_ul32(self): + self._test_type("ul32", b"\x00\x00\x00\x80", 2**31) + + def test_int_array(self): + data = memmap.MemoryMapBytes(bytes(b'\x00\x01\x02\x03')) + obj = bitwise.parse('u8 foo[4];', data) + for i in range(4): + self.assertEqual(i, obj.foo[i]) + obj.foo[i] = i * 2 + self.assertEqual(b'\x00\x02\x04\x06', data.get_packed()) + + def test_int_array(self): + data = memmap.MemoryMapBytes(bytes(b'\x00\x01\x02\x03')) + obj = bitwise.parse('u8 foo[4];', data) + for i in range(4): + self.assertEqual(i, obj.foo[i]) + obj.foo[i] = i * 2 + self.assertEqual(b'\x00\x02\x04\x06', data.get_packed()) + + +class TestBitfieldTypes(BaseTest): + def test_bitfield_u8(self): + defn = "u8 foo:4, bar:4;" + data = memmap.MemoryMapBytes(bytes(b"\x12")) + obj = bitwise.parse(defn, data) + self.assertEqual(obj.foo, 1) + self.assertEqual(obj.bar, 2) + self.assertEqual(obj.foo.size(), 4) + self.assertEqual(obj.bar.size(), 4) + obj.foo = 0x8 + obj.bar = 0x1 + self.assertEqual(data.get_packed(), b"\x81") + + def _test_bitfield_16(self, variant, data): + defn = "u%s16 foo:4, bar:8, baz:4;" % variant + data = memmap.MemoryMapBytes(bytes(data)) + obj = bitwise.parse(defn, data) + self.assertEqual(int(obj.foo), 1) + self.assertEqual(int(obj.bar), 0x23) + self.assertEqual(int(obj.baz), 4) + self.assertEqual(obj.foo.size(), 4) + self.assertEqual(obj.bar.size(), 8) + self.assertEqual(obj.baz.size(), 4) + obj.foo = 0x2 + obj.bar = 0x11 + obj.baz = 0x3 + if variant == "l": + self.assertEqual(data.get_packed(), b"\x13\x21") + else: + self.assertEqual(data.get_packed(), b"\x21\x13") + + def test_bitfield_u16(self): + self._test_bitfield_16("", b"\x12\x34") + + def test_bitfield_ul16(self): + self._test_bitfield_16('l', b"\x34\x12") + + def _test_bitfield_24(self, variant, data): + defn = "u%s24 foo:12, bar:6, baz:6;" % variant + data = memmap.MemoryMapBytes(bytes(data)) + obj = bitwise.parse(defn, data) + self.assertEqual(int(obj.foo), 4) + self.assertEqual(int(obj.bar), 3) + self.assertEqual(int(obj.baz), 2) + self.assertEqual(obj.foo.size(), 12) + self.assertEqual(obj.bar.size(), 6) + self.assertEqual(obj.baz.size(), 6) + obj.foo = 1 + obj.bar = 2 + obj.baz = 3 + if variant == 'l': + self.assertEqual(data.get_packed(), b"\x83\x10\x00") + else: + self.assertEqual(data.get_packed(), b"\x00\x10\x83") + + def test_bitfield_u24(self): + self._test_bitfield_24("", b"\x00\x40\xC2") + + def test_bitfield_ul24(self): + self._test_bitfield_24("l", b"\xC2\x40\x00") + + +class TestBitType(BaseTest): + def test_bit_array(self): + defn = "bit foo[24];" + data = memmap.MemoryMapBytes(bytes(b"\x00\x80\x01")) + obj = bitwise.parse(defn, data) + for i, v in [(0, False), (8, True), (23, True)]: + self.assertEqual(bool(obj.foo[i]), v) + for i in range(0, 24): + obj.foo[i] = i % 2 + self.assertEqual(data.get_packed(), b"\x55\x55\x55") + + def test_bit_array_fail(self): + self.assertRaises(ValueError, bitwise.parse, "bit foo[23];", b"000") + + +class TestBitwiseBCDTypes(BaseTest): + def _test_def(self, definition, name, _data, value): + data = memmap.MemoryMapBytes(bytes(_data)) + obj = bitwise.parse(definition, data) + self.assertEqual(int(getattr(obj, name)), value) + self.assertEqual(getattr(obj, name).size(), len(_data) * 8) + setattr(obj, name, 0) + self.assertEqual(data.get_packed(), (b"\x00" * len(_data))) + setattr(obj, name, 42) + if definition.startswith("b"): + expected = (len(_data) == 2 and b"\x00" or b"") + b"\x42" + else: + expected = b"\x42" + (len(_data) == 2 and b"\x00" or b"") + raw = data.get_packed() + self.assertEqual(raw, expected) + + def test_bbcd(self): + self._test_def("bbcd foo;", "foo", b"\x12", 12) + + def test_lbcd(self): + self._test_def("lbcd foo;", "foo", b"\x12", 12) + + def test_bbcd_array(self): + self._test_def("bbcd foo[2];", "foo", b"\x12\x34", 1234) + + def test_lbcd_array(self): + self._test_def("lbcd foo[2];", "foo", b"\x12\x34", 3412) + + +class TestBitwiseCharTypes(BaseTest): + def test_char(self): + data = memmap.MemoryMapBytes(bytes(b"c")) + obj = bitwise.parse("char foo;", data) + self.assertEqual(str(obj.foo), "c") + self.assertEqual(obj.foo.size(), 8) + obj.foo = "d" + self.assertEqual(data.get_packed(), b"d") + + def test_string(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertEqual(str(obj.foo), "foobar") + self.assertEqual(obj.foo.size(), 8 * 6) + obj.foo = "bazfoo" + self.assertEqual(data.get_packed(), b"bazfoo") + + def test_string_invalid_chars(self): + data = memmap.MemoryMapBytes(bytes(b"\xFFoobar1")) + obj = bitwise.parse("struct {char foo[7];} bar;", data) + + if six.PY3: + expected = '\xffoobar1' + else: + expected = '\\xffoobar1' + + self.assertIn(expected, repr(obj.bar)) + + def test_string_wrong_length(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertRaises(ValueError, setattr, obj, "foo", "bazfo") + self.assertRaises(ValueError, setattr, obj, "foo", "bazfooo") + + def test_string_with_various_input_types(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertEqual('foobar', str(obj.foo)) + self.assertEqual(6, len(b'barfoo')) + obj.foo = b'barfoo' + self.assertEqual('barfoo', str(obj.foo)) + obj.foo = [ord(c) for c in 'fffbbb'] + self.assertEqual('fffbbb', str(obj.foo)) + + def test_string_get_raw(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertEqual('foobar', obj.foo.get_raw()) + self.assertEqual(b'foobar', obj.foo.get_raw(asbytes=True)) + + +class TestBitwiseStructTypes(BaseTest): + def _test_def(self, definition, data, primitive): + obj = bitwise.parse(definition, data) + self._compare_structure(obj, primitive) + self.assertEqual(obj.size(), len(data) * 8) + + def test_struct_one_element(self): + defn = "struct { u8 bar; } foo;" + value = {"foo": {"bar": 128}} + self._test_def(defn, b"\x80", value) + + def test_struct_two_elements(self): + defn = "struct { u8 bar; u16 baz; } foo;" + value = {"foo": {"bar": 128, "baz": 256}} + self._test_def(defn, b"\x80\x01\x00", value) + + def test_struct_writes(self): + data = memmap.MemoryMapBytes(bytes(b"..")) + defn = "struct { u8 bar; u8 baz; } foo;" + obj = bitwise.parse(defn, data) + obj.foo.bar = 0x12 + obj.foo.baz = 0x34 + self.assertEqual(data.get_packed(), b"\x12\x34") + + def test_struct_get_raw(self): + data = memmap.MemoryMapBytes(bytes(b"..")) + defn = "struct { u8 bar; u8 baz; } foo;" + obj = bitwise.parse(defn, data) + self.assertEqual('..', obj.get_raw()) + self.assertEqual(b'..', obj.get_raw(asbytes=True)) + + def test_struct_get_raw_small(self): + data = memmap.MemoryMapBytes(bytes(b".")) + defn = "struct { u8 bar; } foo;" + obj = bitwise.parse(defn, data) + self.assertEqual('.', obj.get_raw()) + self.assertEqual(b'.', obj.get_raw(asbytes=True)) + + +class TestBitwiseSeek(BaseTest): + def test_seekto(self): + defn = "#seekto 4; char foo;" + obj = bitwise.parse(defn, b"abcdZ") + self.assertEqual(str(obj.foo), "Z") + + def test_seek(self): + defn = "char foo; #seek 3; char bar;" + obj = bitwise.parse(defn, b"AbcdZ") + self.assertEqual(str(obj.foo), "A") + self.assertEqual(str(obj.bar), "Z") + + +class TestBitwiseErrors(BaseTest): + def test_missing_semicolon(self): + self.assertRaises(SyntaxError, bitwise.parse, "u8 foo", "") + + +class TestBitwiseComments(BaseTest): + def test_comment_inline_cppstyle(self): + obj = bitwise.parse('u8 foo; // test', b'\x10') + self.assertEqual(16, obj.foo) + + def test_comment_cppstyle(self): + obj = bitwise.parse('// Test this\nu8 foo;', b'\x10') + self.assertEqual(16, obj.foo) + + +class TestBitwiseStringEncoders(BaseTest): + def test_encode_bytes(self): + self.assertEqual(b'foobar\x00', + bitwise.string_straight_encode('foobar\x00')) + + def test_decode_bytes(self): + self.assertEqual('foobar\x00', + bitwise.string_straight_decode(b'foobar\x00')) diff --git a/tests/unit/test_chirp_common.py b/tests/unit/test_chirp_common.py new file mode 100644 index 0000000..400c469 --- /dev/null +++ b/tests/unit/test_chirp_common.py @@ -0,0 +1,415 @@ +import base64 +import json +import os +import tempfile + +import mock + +from tests.unit import base +from chirp import CHIRP_VERSION +from chirp import chirp_common +from chirp import errors + + +class TestUtilityFunctions(base.BaseTest): + def test_parse_freq_whole(self): + self.assertEqual(chirp_common.parse_freq("146.520000"), 146520000) + self.assertEqual(chirp_common.parse_freq("146.5200"), 146520000) + self.assertEqual(chirp_common.parse_freq("146.52"), 146520000) + self.assertEqual(chirp_common.parse_freq("146"), 146000000) + self.assertEqual(chirp_common.parse_freq("1250"), 1250000000) + self.assertEqual(chirp_common.parse_freq("123456789"), + 123456789000000) + + def test_parse_freq_decimal(self): + self.assertEqual(chirp_common.parse_freq("1.0"), 1000000) + self.assertEqual(chirp_common.parse_freq("1.000000"), 1000000) + self.assertEqual(chirp_common.parse_freq("1.1"), 1100000) + self.assertEqual(chirp_common.parse_freq("1.100"), 1100000) + self.assertEqual(chirp_common.parse_freq("0.6"), 600000) + self.assertEqual(chirp_common.parse_freq("0.600"), 600000) + self.assertEqual(chirp_common.parse_freq("0.060"), 60000) + self.assertEqual(chirp_common.parse_freq(".6"), 600000) + + def test_parse_freq_whitespace(self): + self.assertEqual(chirp_common.parse_freq("1 "), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1"), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1 "), 1000000) + + self.assertEqual(chirp_common.parse_freq("1.0 "), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1.0"), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1.0 "), 1000000) + self.assertEqual(chirp_common.parse_freq(""), 0) + self.assertEqual(chirp_common.parse_freq(" "), 0) + + def test_parse_freq_bad(self): + self.assertRaises(ValueError, chirp_common.parse_freq, "a") + self.assertRaises(ValueError, chirp_common.parse_freq, "1.a") + self.assertRaises(ValueError, chirp_common.parse_freq, "a.b") + self.assertRaises(ValueError, chirp_common.parse_freq, + "1.0000001") + + def test_format_freq(self): + self.assertEqual(chirp_common.format_freq(146520000), "146.520000") + self.assertEqual(chirp_common.format_freq(54000000), "54.000000") + self.assertEqual(chirp_common.format_freq(1800000), "1.800000") + self.assertEqual(chirp_common.format_freq(1), "0.000001") + self.assertEqual(chirp_common.format_freq(1250000000), "1250.000000") + + @mock.patch('chirp.CHIRP_VERSION', new='daily-20151021') + def test_compare_version_to_current(self): + self.assertTrue(chirp_common.is_version_newer('daily-20180101')) + self.assertFalse(chirp_common.is_version_newer('daily-20140101')) + self.assertFalse(chirp_common.is_version_newer('0.3.0')) + self.assertFalse(chirp_common.is_version_newer('0.3.0dev')) + + @mock.patch('chirp.CHIRP_VERSION', new='0.3.0dev') + def test_compare_version_to_current_dev(self): + self.assertTrue(chirp_common.is_version_newer('daily-20180101')) + + def test_from_Hz(self): + # FIXME: These are wrong! Adding them here purely to test the + # python3 conversion, but they should be fixed. + self.assertEqual(140, chirp_common.from_GHz(14000000001)) + self.assertEqual(140, chirp_common.from_MHz(14000001)) + self.assertEqual(140, chirp_common.from_kHz(14001)) + + +class TestSplitTone(base.BaseTest): + def _test_split_tone_decode(self, tx, rx, **vals): + mem = chirp_common.Memory() + chirp_common.split_tone_decode(mem, tx, rx) + for key, value in list(vals.items()): + self.assertEqual(getattr(mem, key), value) + + def test_split_tone_decode_none(self): + self._test_split_tone_decode((None, None, None), + (None, None, None), + tmode='') + + def test_split_tone_decode_tone(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('', 0, None), + tmode='Tone', + rtone=100.0) + + def test_split_tone_decode_tsql(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('Tone', 100.0, None), + tmode='TSQL', + ctone=100.0) + + def test_split_tone_decode_dtcs(self): + self._test_split_tone_decode(('DTCS', 23, None), + ('DTCS', 23, None), + tmode='DTCS', + dtcs=23) + + def test_split_tone_decode_cross_tone_tone(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('Tone', 123.0, None), + tmode='Cross', + cross_mode='Tone->Tone', + rtone=100.0, + ctone=123.0) + + def test_split_tone_decode_cross_tone_dtcs(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('DTCS', 32, 'R'), + tmode='Cross', + cross_mode='Tone->DTCS', + rtone=100.0, + rx_dtcs=32, + dtcs_polarity='NR') + + def test_split_tone_decode_cross_dtcs_tone(self): + self._test_split_tone_decode(('DTCS', 32, 'R'), + ('Tone', 100.0, None), + tmode='Cross', + cross_mode='DTCS->Tone', + ctone=100.0, + dtcs=32, + dtcs_polarity='RN') + + def test_split_tone_decode_cross_dtcs_dtcs(self): + self._test_split_tone_decode(('DTCS', 32, 'R'), + ('DTCS', 25, 'R'), + tmode='Cross', + cross_mode='DTCS->DTCS', + dtcs=32, + rx_dtcs=25, + dtcs_polarity='RR') + + def test_split_tone_decode_cross_none_dtcs(self): + self._test_split_tone_decode((None, None, None), + ('DTCS', 25, 'R'), + tmode='Cross', + cross_mode='->DTCS', + rx_dtcs=25, + dtcs_polarity='NR') + + def test_split_tone_decode_cross_none_tone(self): + self._test_split_tone_decode((None, None, None), + ('Tone', 100.0, None), + tmode='Cross', + cross_mode='->Tone', + ctone=100.0) + + def _set_mem(self, **vals): + mem = chirp_common.Memory() + for key, value in list(vals.items()): + setattr(mem, key, value) + return chirp_common.split_tone_encode(mem) + + def split_tone_encode_test_none(self): + self.assertEqual(self._set_mem(tmode=''), + (('', None, None), + ('', None, None))) + + def split_tone_encode_test_tone(self): + self.assertEqual(self._set_mem(tmode='Tone', rtone=100.0), + (('Tone', 100.0, None), + ('', None, None))) + + def split_tone_encode_test_tsql(self): + self.assertEqual(self._set_mem(tmode='TSQL', ctone=100.0), + (('Tone', 100.0, None), + ('Tone', 100.0, None))) + + def split_tone_encode_test_dtcs(self): + self.assertEqual(self._set_mem(tmode='DTCS', dtcs=23, + dtcs_polarity='RN'), + (('DTCS', 23, 'R'), + ('DTCS', 23, 'N'))) + + def split_tone_encode_test_cross_tone_tone(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='Tone->Tone', + rtone=100.0, ctone=123.0), + (('Tone', 100.0, None), + ('Tone', 123.0, None))) + + def split_tone_encode_test_cross_tone_dtcs(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='Tone->DTCS', + rtone=100.0, rx_dtcs=25), + (('Tone', 100.0, None), + ('DTCS', 25, 'N'))) + + def split_tone_encode_test_cross_dtcs_tone(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='DTCS->Tone', + ctone=100.0, dtcs=25), + (('DTCS', 25, 'N'), + ('Tone', 100.0, None))) + + def split_tone_encode_test_cross_none_dtcs(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='->DTCS', + rx_dtcs=25), + (('', None, None), + ('DTCS', 25, 'N'))) + + def split_tone_encode_test_cross_none_tone(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='->Tone', + ctone=100.0), + (('', None, None), + ('Tone', 100.0, None))) + + +class TestStepFunctions(base.BaseTest): + _625 = [145856250, + 445856250, + 862731250, + 146118750, + ] + _125 = [145862500, + 445862500, + 862737500, + ] + _005 = [145005000, + 445005000, + 850005000, + ] + _025 = [145002500, + 445002500, + 850002500, + ] + + def test_is_fractional_step(self): + for freq in self._125 + self._625: + print(freq) + self.assertTrue(chirp_common.is_fractional_step(freq)) + + def test_is_6_25(self): + for freq in self._625: + self.assertTrue(chirp_common.is_6_25(freq)) + + def test_is_12_5(self): + for freq in self._125: + self.assertTrue(chirp_common.is_12_5(freq)) + + def test_is_5_0(self): + for freq in self._005: + self.assertTrue(chirp_common.is_5_0(freq)) + + def test_is_2_5(self): + for freq in self._025: + self.assertTrue(chirp_common.is_2_5(freq)) + + def test_required_step(self): + steps = {2.5: self._025, + 5.0: self._005, + 6.25: self._625, + 12.5: self._125, + } + for step, freqs in list(steps.items()): + for freq in freqs: + self.assertEqual(step, chirp_common.required_step(freq)) + + def test_required_step_fail(self): + self.assertRaises(errors.InvalidDataError, + chirp_common.required_step, + 146520500) + + def test_fix_rounded_step_250(self): + self.assertEqual(146106250, + chirp_common.fix_rounded_step(146106000)) + + def test_fix_rounded_step_500(self): + self.assertEqual(146112500, + chirp_common.fix_rounded_step(146112000)) + + def test_fix_rounded_step_750(self): + self.assertEqual(146118750, + chirp_common.fix_rounded_step(146118000)) + + +class TestImageMetadata(base.BaseTest): + def test_make_metadata(self): + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + raw_metadata = TestRadio._make_metadata() + metadata = json.loads(base64.b64decode(raw_metadata).decode()) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, metadata) + + def test_strip_metadata(self): + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + raw_metadata = TestRadio._make_metadata() + raw_data = (b'foooooooooooooooooooooo' + TestRadio.MAGIC + + TestRadio._make_metadata()) + data, metadata = chirp_common.FileBackedRadio._strip_metadata(raw_data) + self.assertEqual(b'foooooooooooooooooooooo', data) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, metadata) + + def test_load_mmap_no_metadata(self): + f = tempfile.NamedTemporaryFile() + f.write(b'thisisrawdata') + f.flush() + + with mock.patch('chirp.memmap.MemoryMap') as mock_mmap: + chirp_common.FileBackedRadio(None).load_mmap(f.name) + mock_mmap.assert_called_once_with(b'thisisrawdata') + + def test_load_mmap_bad_metadata(self): + f = tempfile.NamedTemporaryFile() + f.write(b'thisisrawdata') + f.write(chirp_common.FileBackedRadio.MAGIC + b'bad') + f.flush() + + with mock.patch('chirp.memmap.MemoryMap') as mock_mmap: + chirp_common.FileBackedRadio(None).load_mmap(f.name) + mock_mmap.assert_called_once_with(b'thisisrawdata') + + def test_save_mmap_includes_metadata(self): + # Make sure that a file saved with a .img extension includes + # the metadata blob + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + with tempfile.NamedTemporaryFile(suffix='.Img') as f: + fn = f.name + r = TestRadio(None) + r._mmap = mock.Mock() + r._mmap.get_byte_compatible.return_value.get_packed.return_value = ( + b'thisisrawdata') + r.save_mmap(fn) + with open(fn, 'rb') as f: + filedata = f.read() + os.remove(fn) + data, metadata = chirp_common.FileBackedRadio._strip_metadata(filedata) + self.assertEqual(b'thisisrawdata', data) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, metadata) + + def test_save_mmap_no_metadata_not_img_file(self): + # Make sure that if we save without a .img extension we do + # not include the metadata blob + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + with tempfile.NamedTemporaryFile(suffix='.txt') as f: + fn = f.name + r = TestRadio(None) + r._mmap = mock.Mock() + r._mmap.get_byte_compatible.return_value.get_packed.return_value = ( + b'thisisrawdata') + r.save_mmap(fn) + with open(fn, 'rb') as f: + filedata = f.read() + os.remove(fn) + data, metadata = chirp_common.FileBackedRadio._strip_metadata(filedata) + self.assertEqual(b'thisisrawdata', data) + self.assertEqual({}, metadata) + + def test_load_mmap_saves_metadata_on_radio(self): + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + with tempfile.NamedTemporaryFile(suffix='.img') as f: + fn = f.name + r = TestRadio(None) + r._mmap = mock.Mock() + r._mmap.get_byte_compatible.return_value.get_packed.return_value = ( + b'thisisrawdata') + r.save_mmap(fn) + + newr = TestRadio(None) + newr.load_mmap(fn) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, newr.metadata) diff --git a/tests/unit/test_directory.py b/tests/unit/test_directory.py new file mode 100644 index 0000000..d48bb48 --- /dev/null +++ b/tests/unit/test_directory.py @@ -0,0 +1,69 @@ +import base64 +import json +import tempfile + +from tests.unit import base +from chirp import chirp_common +from chirp import directory + + +class TestDirectory(base.BaseTest): + def setUp(self): + super(TestDirectory, self).setUp() + + directory.enable_reregistrations() + + class FakeAlias(chirp_common.Alias): + VENDOR = 'Taylor' + MODEL = 'Barmaster 2000' + VARIANT = 'A' + + @directory.register + class FakeRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + ALIASES = [FakeAlias] + + @classmethod + def match_model(cls, file_data, image_file): + return file_data == b'thisisrawdata' + + self.test_class = FakeRadio + + def _test_detect_finds_our_class(self, tempfn): + radio = directory.get_radio_by_image(tempfn) + self.assertTrue(isinstance(radio, self.test_class)) + return radio + + def test_detect_with_no_metadata(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b'thisisrawdata') + f.flush() + self._test_detect_finds_our_class(f.name) + + def test_detect_with_metadata_base_class(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b'thisisrawdata') + f.write(self.test_class.MAGIC + b'-') + f.write(self.test_class._make_metadata()) + f.flush() + self._test_detect_finds_our_class(f.name) + + def test_detect_with_metadata_alias_class(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b'thisisrawdata') + f.write(self.test_class.MAGIC + b'-') + FakeAlias = self.test_class.ALIASES[0] + fake_metadata = base64.b64encode(json.dumps( + {'vendor': FakeAlias.VENDOR, + 'model': FakeAlias.MODEL, + 'variant': FakeAlias.VARIANT, + }).encode()) + f.write(fake_metadata) + f.flush() + radio = self._test_detect_finds_our_class(f.name) + self.assertEqual('Taylor', radio.VENDOR) + self.assertEqual('Barmaster 2000', radio.MODEL) + self.assertEqual('A', radio.VARIANT) + diff --git a/tests/unit/test_icom_clone.py b/tests/unit/test_icom_clone.py new file mode 100644 index 0000000..9ade954 --- /dev/null +++ b/tests/unit/test_icom_clone.py @@ -0,0 +1,117 @@ +# Copyright 2019 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import glob +import os +import logging +import unittest + +from chirp import directory +directory.safe_import_drivers() +from chirp.drivers import icf +from chirp import memmap +from tests import icom_clone_simulator + + +class BaseIcomCloneTest(): + def setUp(self): + self.radio = directory.get_radio(self.RADIO_IDENT)(None) + self.simulator = icom_clone_simulator.FakeIcomRadio(self.radio) + self.radio.set_pipe(self.simulator) + + def image_filename(self, filename): + tests_base = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(tests_base, 'images', filename) + + def load_from_test_image(self, filename): + self.simulator.load_from_file(self.image_filename(filename)) + + def image_version(self, filename): + end = self.radio.get_memsize() + + with open(self.image_filename(filename), 'rb') as f: + data = f.read() + if data[-8:-6] == b'%02x' % self.radio.get_model()[0]: + return 2 + elif data[end-16:end] == b'IcomCloneFormat3': + return 3 + + + def test_sync_in(self): + test_file = self.IMAGE_FILE + self.load_from_test_image(test_file) + self.radio.sync_in() + + img_ver = self.image_version(test_file) + if img_ver == 2: + endstring = b''.join(b'%02x' % ord(c) + for c in self.radio._model[:2]) + self.assertEqual(endstring + b'0001', self.radio._mmap[-8:]) + elif img_ver == 3: + self.assertEqual(b'IcomCloneFormat3', self.radio._mmap[-16:]) + elif img_ver is None: + self.assertEqual(self.radio.get_memsize(), len(self.radio._mmap)) + + def test_sync_out(self): + self.radio._mmap = memmap.MemoryMapBytes( + bytes(b'\x00') * self.radio.get_memsize()) + self.radio._mmap[50] = bytes(b'abcdefgh') + self.radio.sync_out() + self.assertEqual(b'abcdefgh', self.simulator._memory[50:58]) + + +class TestRawRadioData(unittest.TestCase): + def test_get_payload(self): + radio = directory.get_radio('Icom_IC-2730A')(None) + payload = radio.get_payload(bytes(b'\x00\x10\xFE\x00'), True, True) + self.assertEqual(b'\x00\x10\xFF\x0E\x00\xF2', payload) + + payload = radio.get_payload(bytes(b'\x00\x10\xFE\x00'), True, False) + self.assertEqual(b'\x00\x10\xFF\x0E\x00', payload) + + def test_process_frame_payload(self): + radio = directory.get_radio('Icom_IC-2730A')(None) + data = radio.process_frame_payload(bytes(b'\x00\x10\xFF\x0E\x00')) + self.assertEqual(b'\x00\x10\xFE\x00', data) + + +class TestAdapterMeta(type): + def __new__(cls, name, parents, dct): + return super(TestAdapterMeta, cls).__new__(cls, name, parents, dct) + + +test_file_glob = os.path.join(os.path.dirname(os.path.abspath(__file__)), + '..', 'images', + 'Icom_*.img') +import sys +for image_file in glob.glob(test_file_glob): + base, _ext = os.path.splitext(os.path.basename(image_file)) + + try: + radio = directory.get_radio(base) + except Exception: + continue + + if issubclass(radio, icf.IcomRawCloneModeRadio): + # The simulator does not behave like a raw radio + continue + + class_name = 'Test_%s' % base + sys.modules[__name__].__dict__[class_name] = \ + TestAdapterMeta(class_name, + (BaseIcomCloneTest, unittest.TestCase), + dict(RADIO_IDENT=base, IMAGE_FILE=image_file)) diff --git a/tests/unit/test_import_logic.py b/tests/unit/test_import_logic.py new file mode 100644 index 0000000..ecd9aa9 --- /dev/null +++ b/tests/unit/test_import_logic.py @@ -0,0 +1,373 @@ +from tests.unit import base +from chirp import import_logic +from chirp import chirp_common +from chirp import errors + + +class FakeRadio(chirp_common.Radio): + def __init__(self, arg): + self.POWER_LEVELS = list([chirp_common.PowerLevel('lo', watts=5), + chirp_common.PowerLevel('hi', watts=50)]) + self.TMODES = list(['', 'Tone', 'TSQL']) + self.HAS_CTONE = True + self.HAS_RX_DTCS = False + self.MODES = list(['FM', 'AM', 'DV']) + self.DUPLEXES = list(['', '-', '+']) + + def filter_name(self, name): + return 'filtered-name' + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_tmodes = self.TMODES + rf.valid_modes = self.MODES + rf.valid_duplexes = self.DUPLEXES + rf.has_ctone = self.HAS_CTONE + rf.has_rx_dtcs = self.HAS_RX_DTCS + return rf + + +class FakeDstarRadio(FakeRadio, chirp_common.IcomDstarSupport): + pass + + +class DstarTests(base.BaseTest): + def _test_ensure_has_calls(self, mem, + ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls): + radio = FakeDstarRadio(None) + self.mox.StubOutWithMock(radio, 'get_urcall_list') + self.mox.StubOutWithMock(radio, 'get_repeater_call_list') + radio.get_urcall_list().AndReturn(ini_urcalls) + radio.get_repeater_call_list().AndReturn(ini_rptcalls) + self.mox.ReplayAll() + import_logic.ensure_has_calls(radio, mem) + self.assertEqual(sorted(ini_urcalls), sorted(exp_urcalls)) + self.assertEqual(sorted(ini_rptcalls), sorted(exp_rptcalls)) + + def test_ensure_has_calls_empty(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['', '', '', '', ''] + ini_rptcalls = ['', '', '', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[0] = mem.dv_urcall + exp_rptcalls[0] = mem.dv_rpt1call + exp_rptcalls[1] = mem.dv_rpt2call + self._test_ensure_has_calls(mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_partial(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', '', '', ''] + ini_rptcalls = ['FOO', 'BAR', '', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[2] = mem.dv_urcall + exp_rptcalls[2] = mem.dv_rpt1call + exp_rptcalls[3] = mem.dv_rpt2call + self._test_ensure_has_calls(mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_almost_full(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self._test_ensure_has_calls(mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_urcall_full(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', 'BOOM'] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self.assertRaises(errors.RadioError, + self._test_ensure_has_calls, + mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_rptcall_full1(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self.assertRaises(errors.RadioError, + self._test_ensure_has_calls, + mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_rptcall_full2(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', 'BAT', 'BOOM'] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self.assertRaises(errors.RadioError, + self._test_ensure_has_calls, + mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + +class ImportFieldTests(base.BaseTest): + def test_import_name(self): + mem = chirp_common.Memory() + mem.name = 'foo' + import_logic._import_name(FakeRadio(None), None, mem) + self.assertEqual(mem.name, 'filtered-name') + + def test_import_power_same(self): + radio = FakeRadio(None) + same_rf = radio.get_features() + mem = chirp_common.Memory() + mem.power = same_rf.valid_power_levels[0] + import_logic._import_power(radio, same_rf, mem) + self.assertEqual(mem.power, same_rf.valid_power_levels[0]) + + def test_import_power_no_src(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + mem = chirp_common.Memory() + mem.power = None + import_logic._import_power(radio, src_rf, mem) + self.assertEqual(mem.power, radio.POWER_LEVELS[0]) + + def test_import_power_no_dst(self): + radio = FakeRadio(None) + src_rf = radio.get_features() # Steal a copy before we stub out + self.mox.StubOutWithMock(radio, 'get_features') + radio.get_features().AndReturn(chirp_common.RadioFeatures()) + self.mox.ReplayAll() + mem = chirp_common.Memory() + mem.power = src_rf.valid_power_levels[0] + import_logic._import_power(radio, src_rf, mem) + self.assertEqual(mem.power, None) + + def test_import_power_closest(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + src_rf.valid_power_levels = [ + chirp_common.PowerLevel('foo', watts=7), + chirp_common.PowerLevel('bar', watts=51), + chirp_common.PowerLevel('baz', watts=1), + ] + mem = chirp_common.Memory() + mem.power = src_rf.valid_power_levels[0] + import_logic._import_power(radio, src_rf, mem) + self.assertEqual(mem.power, radio.POWER_LEVELS[0]) + + def test_import_tone_diffA_tsql(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + src_rf.has_ctone = False + mem = chirp_common.Memory() + mem.tmode = 'TSQL' + mem.rtone = 100.0 + import_logic._import_tone(radio, src_rf, mem) + self.assertEqual(mem.ctone, 100.0) + + def test_import_tone_diffB_tsql(self): + radio = FakeRadio(None) + radio.HAS_CTONE = False + src_rf = chirp_common.RadioFeatures() + src_rf.has_ctone = True + mem = chirp_common.Memory() + mem.tmode = 'TSQL' + mem.ctone = 100.0 + import_logic._import_tone(radio, src_rf, mem) + self.assertEqual(mem.rtone, 100.0) + + def test_import_dtcs_diffA_dtcs(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + src_rf.has_rx_dtcs = True + mem = chirp_common.Memory() + mem.tmode = 'DTCS' + mem.rx_dtcs = 32 + import_logic._import_dtcs(radio, src_rf, mem) + self.assertEqual(mem.dtcs, 32) + + def test_import_dtcs_diffB_dtcs(self): + radio = FakeRadio(None) + radio.HAS_RX_DTCS = True + src_rf = chirp_common.RadioFeatures() + src_rf.has_rx_dtcs = False + mem = chirp_common.Memory() + mem.tmode = 'DTCS' + mem.dtcs = 32 + import_logic._import_dtcs(radio, src_rf, mem) + self.assertEqual(mem.rx_dtcs, 32) + + def test_import_mode_valid_fm(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.mode = 'Auto' + mem.freq = 146000000 + import_logic._import_mode(radio, None, mem) + self.assertEqual(mem.mode, 'FM') + + def test_import_mode_valid_am(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.mode = 'Auto' + mem.freq = 18000000 + import_logic._import_mode(radio, None, mem) + self.assertEqual(mem.mode, 'AM') + + def test_import_mode_invalid(self): + radio = FakeRadio(None) + radio.MODES.remove('AM') + mem = chirp_common.Memory() + mem.mode = 'Auto' + mem.freq = 1800000 + self.assertRaises(import_logic.DestNotCompatible, + import_logic._import_mode, radio, None, mem) + + def test_import_duplex_vhf(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 146000000 + mem.offset = 146600000 + mem.duplex = 'split' + import_logic._import_duplex(radio, None, mem) + self.assertEqual(mem.duplex, '+') + self.assertEqual(mem.offset, 600000) + + def test_import_duplex_negative(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 146600000 + mem.offset = 146000000 + mem.duplex = 'split' + import_logic._import_duplex(radio, None, mem) + self.assertEqual(mem.duplex, '-') + self.assertEqual(mem.offset, 600000) + + def test_import_duplex_uhf(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 431000000 + mem.offset = 441000000 + mem.duplex = 'split' + import_logic._import_duplex(radio, None, mem) + self.assertEqual(mem.duplex, '+') + self.assertEqual(mem.offset, 10000000) + + def test_import_duplex_too_big_vhf(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 146000000 + mem.offset = 246000000 + mem.duplex = 'split' + self.assertRaises(import_logic.DestNotCompatible, + import_logic._import_duplex, radio, None, mem) + + def test_import_mem(self, errors=[]): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + mem = chirp_common.Memory() + + self.mox.StubOutWithMock(mem, 'dupe') + self.mox.StubOutWithMock(import_logic, '_import_name') + self.mox.StubOutWithMock(import_logic, '_import_power') + self.mox.StubOutWithMock(import_logic, '_import_tone') + self.mox.StubOutWithMock(import_logic, '_import_dtcs') + self.mox.StubOutWithMock(import_logic, '_import_mode') + self.mox.StubOutWithMock(import_logic, '_import_duplex') + self.mox.StubOutWithMock(radio, 'validate_memory') + + mem.dupe().AndReturn(mem) + import_logic._import_name(radio, src_rf, mem) + import_logic._import_power(radio, src_rf, mem) + import_logic._import_tone(radio, src_rf, mem) + import_logic._import_dtcs(radio, src_rf, mem) + import_logic._import_mode(radio, src_rf, mem) + import_logic._import_duplex(radio, src_rf, mem) + radio.validate_memory(mem).AndReturn(errors) + + self.mox.ReplayAll() + + import_logic.import_mem(radio, src_rf, mem) + + def test_import_mem_with_warnings(self): + self.test_import_mem([chirp_common.ValidationWarning('Test')]) + + def test_import_mem_with_errors(self): + self.assertRaises(import_logic.DestNotCompatible, + self.test_import_mem, + [chirp_common.ValidationError('Test')]) + + def test_import_bank(self): + dst_mem = chirp_common.Memory() + dst_mem.number = 1 + src_mem = chirp_common.Memory() + src_mem.number = 2 + dst_radio = FakeRadio(None) + src_radio = FakeRadio(None) + dst_bm = chirp_common.BankModel(dst_radio) + src_bm = chirp_common.BankModel(src_radio) + + dst_banks = [chirp_common.Bank(dst_bm, 0, 'A'), + chirp_common.Bank(dst_bm, 1, 'B'), + chirp_common.Bank(dst_bm, 2, 'C'), + ] + src_banks = [chirp_common.Bank(src_bm, 1, '1'), + chirp_common.Bank(src_bm, 2, '2'), + chirp_common.Bank(src_bm, 3, '3'), + ] + + self.mox.StubOutWithMock(dst_radio, 'get_mapping_models') + self.mox.StubOutWithMock(src_radio, 'get_mapping_models') + self.mox.StubOutWithMock(dst_bm, 'get_mappings') + self.mox.StubOutWithMock(src_bm, 'get_mappings') + self.mox.StubOutWithMock(dst_bm, 'get_memory_mappings') + self.mox.StubOutWithMock(src_bm, 'get_memory_mappings') + self.mox.StubOutWithMock(dst_bm, 'remove_memory_from_mapping') + self.mox.StubOutWithMock(dst_bm, 'add_memory_to_mapping') + + dst_radio.get_mapping_models().AndReturn([dst_bm]) + dst_bm.get_mappings().AndReturn(dst_banks) + src_radio.get_mapping_models().AndReturn([src_bm]) + src_bm.get_mappings().AndReturn(src_banks) + src_bm.get_memory_mappings(src_mem).AndReturn([src_banks[0]]) + dst_bm.get_memory_mappings(dst_mem).AndReturn([dst_banks[1]]) + dst_bm.remove_memory_from_mapping(dst_mem, dst_banks[1]) + dst_bm.add_memory_to_mapping(dst_mem, dst_banks[0]) + + self.mox.ReplayAll() + + import_logic.import_bank(dst_radio, src_radio, dst_mem, src_mem) diff --git a/tests/unit/test_mappingmodel.py b/tests/unit/test_mappingmodel.py new file mode 100644 index 0000000..2f263d7 --- /dev/null +++ b/tests/unit/test_mappingmodel.py @@ -0,0 +1,283 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.unit import base +from chirp import chirp_common +from chirp.drivers import icf + + +class TestBaseMapping(base.BaseTest): + CLS = chirp_common.MemoryMapping + + def test_mapping(self): + model = chirp_common.MappingModel(None, 'Foo') + mapping = self.CLS(model, 1, 'Foo') + self.assertEqual(str(mapping), 'Foo') + self.assertEqual(mapping.get_name(), 'Foo') + self.assertEqual(mapping.get_index(), 1) + self.assertEqual(repr(mapping), '%s-1' % self.CLS.__name__) + self.assertEqual(mapping._model, model) + + def test_mapping_eq(self): + mapping1 = self.CLS(None, 1, 'Foo') + mapping2 = self.CLS(None, 1, 'Bar') + mapping3 = self.CLS(None, 2, 'Foo') + + self.assertEqual(mapping1, mapping2) + self.assertNotEqual(mapping1, mapping3) + + +class TestBaseBank(TestBaseMapping): + CLS = chirp_common.Bank + + +class _TestBaseClass(base.BaseTest): + ARGS = tuple() + + def setUp(self): + super(_TestBaseClass, self).setUp() + self.model = self.CLS(*self.ARGS) + + def _test_base(self, method, *args): + self.assertRaises(NotImplementedError, + getattr(self.model, method), *args) + + +class TestBaseMappingModel(_TestBaseClass): + CLS = chirp_common.MappingModel + ARGS = tuple([None, 'Foo']) + + def test_base_class(self): + methods = [('get_num_mappings', ()), + ('get_mappings', ()), + ('add_memory_to_mapping', (None, None)), + ('remove_memory_from_mapping', (None, None)), + ('get_mapping_memories', (None,)), + ('get_memory_mappings', (None,)), + ] + for method, args in methods: + self._test_base(method, *args) + + def test_get_name(self): + self.assertEqual(self.model.get_name(), 'Foo') + + +class TestBaseBankModel(TestBaseMappingModel): + ARGS = tuple([None]) + CLS = chirp_common.BankModel + + def test_get_name(self): + self.assertEqual(self.model.get_name(), 'Banks') + + +class TestBaseMappingModelIndexInterface(_TestBaseClass): + CLS = chirp_common.MappingModelIndexInterface + + def test_base_class(self): + methods = [('get_index_bounds', ()), + ('get_memory_index', (None, None)), + ('set_memory_index', (None, None, None)), + ('get_next_mapping_index', (None,)), + ] + for method, args in methods: + self._test_base(method, *args) + + +class TestIcomBanks(TestBaseMapping): + def test_icom_bank(self): + bank = icf.IcomBank(None, 1, 'Foo') + # IcomBank has an index attribute used by IcomBankModel + self.assertTrue(hasattr(bank, 'index')) + + +class TestIcomBankModel(base.BaseTest): + CLS = icf.IcomBankModel + + def _get_rf(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 10) + return rf + + def setUp(self): + super(TestIcomBankModel, self).setUp() + + class FakeRadio(icf.IcomCloneModeRadio): + _num_banks = 10 + _bank_index_bounds = (0, 10) + + def get_features(the_radio): + return self._get_rf() + + def _set_bank(self, number, index): + pass + + def _get_bank(self, number): + pass + + def _get_bank_index(self, number): + pass + + def _set_bank_index(self, number, index): + pass + + def get_memory(self, number): + pass + + self._radio = FakeRadio(None) + self._model = self.CLS(self._radio) + self.mox.StubOutWithMock(self._radio, '_set_bank') + self.mox.StubOutWithMock(self._radio, '_get_bank') + self.mox.StubOutWithMock(self._radio, '_set_bank_index') + self.mox.StubOutWithMock(self._radio, '_get_bank_index') + self.mox.StubOutWithMock(self._radio, 'get_memory') + + def test_get_num_mappings(self): + self.assertEqual(self._model.get_num_mappings(), 10) + + def test_get_mappings(self): + banks = self._model.get_mappings() + self.assertEqual(len(banks), 10) + i = 0 + for bank in banks: + index = chr(ord("A") + i) + self.assertEqual(bank.get_index(), index) + self.assertEqual(bank.get_name(), 'BANK-%s' % index) + self.assertEqual(bank.index, i) + i += 1 + + def test_add_memory_to_mapping(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + bank = banks[2] + self._radio._set_bank(5, 2) + self.mox.ReplayAll() + self._model.add_memory_to_mapping(mem, bank) + + def _setup_test_remove_memory_from_mapping(self, curbank): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + bank = banks[2] + self._radio._get_bank(5).AndReturn(curbank) + if curbank == 2: + self._radio._set_bank(5, None) + self.mox.ReplayAll() + return mem, bank + + def test_remove_memory_from_mapping(self): + mem, bank = self._setup_test_remove_memory_from_mapping(2) + self._model.remove_memory_from_mapping(mem, bank) + + def test_remove_memory_from_mapping_wrong_bank(self): + mem, bank = self._setup_test_remove_memory_from_mapping(3) + self.assertRaises(Exception, + self._model.remove_memory_from_mapping, mem, bank) + + def test_remove_memory_from_mapping_no_bank(self): + mem, bank = self._setup_test_remove_memory_from_mapping(None) + self.assertRaises(Exception, + self._model.remove_memory_from_mapping, mem, bank) + + def test_get_mapping_memories(self): + banks = self._model.get_mappings() + expected = [] + for i in range(1, 10): + should_include = bool(i % 2) + self._radio._get_bank(i).AndReturn( + should_include and banks[1].index or None) + if should_include: + self._radio.get_memory(i).AndReturn(i) + expected.append(i) + self.mox.ReplayAll() + members = self._model.get_mapping_memories(banks[1]) + self.assertEqual(members, expected) + + def test_get_memory_mappings(self): + banks = self._model.get_mappings() + mem1 = chirp_common.Memory() + mem1.number = 5 + mem2 = chirp_common.Memory() + mem2.number = 6 + self._radio._get_bank(mem1.number).AndReturn(2) + self._radio._get_bank(mem2.number).AndReturn(None) + self.mox.ReplayAll() + self.assertEqual(self._model.get_memory_mappings(mem1)[0], banks[2]) + self.assertEqual(self._model.get_memory_mappings(mem2), []) + + +class TestIcomIndexedBankModel(TestIcomBankModel): + CLS = icf.IcomIndexedBankModel + + def _get_rf(self): + rf = super(TestIcomIndexedBankModel, self)._get_rf() + rf.has_bank_index = True + return rf + + def test_get_index_bounds(self): + self.assertEqual(self._model.get_index_bounds(), (0, 10)) + + def test_get_memory_index(self): + mem = chirp_common.Memory() + mem.number = 5 + self._radio._get_bank_index(mem.number).AndReturn(1) + self.mox.ReplayAll() + self.assertEqual(self._model.get_memory_index(mem, None), 1) + + def test_set_memory_index(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + self.mox.StubOutWithMock(self._model, 'get_memory_mappings') + self._model.get_memory_mappings(mem).AndReturn([banks[3]]) + self._radio._set_bank_index(mem.number, 1) + self.mox.ReplayAll() + self._model.set_memory_index(mem, banks[3], 1) + + def test_set_memory_index_bad_bank(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + self.mox.StubOutWithMock(self._model, 'get_memory_mappings') + self._model.get_memory_mappings(mem).AndReturn([banks[4]]) + self.mox.ReplayAll() + self.assertRaises(Exception, + self._model.set_memory_index, mem, banks[3], 1) + + def test_set_memory_index_bad_index(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + self.mox.StubOutWithMock(self._model, 'get_memory_mappings') + self._model.get_memory_mappings(mem).AndReturn([banks[3]]) + self.mox.ReplayAll() + self.assertRaises(Exception, + self._model.set_memory_index, mem, banks[3], 99) + + def test_get_next_mapping_index(self): + banks = self._model.get_mappings() + for i in range(*self._radio.get_features().memory_bounds): + self._radio._get_bank(i).AndReturn((i % 2) and banks[1].index) + if bool(i % 2): + self._radio._get_bank_index(i).AndReturn(i) + idx = 0 + for i in range(*self._radio.get_features().memory_bounds): + self._radio._get_bank(i).AndReturn((i % 2) and banks[2].index) + if i % 2: + self._radio._get_bank_index(i).AndReturn(idx) + idx += 1 + self.mox.ReplayAll() + self.assertEqual(self._model.get_next_mapping_index(banks[1]), 0) + self.assertEqual(self._model.get_next_mapping_index(banks[2]), 5) diff --git a/tests/unit/test_memedit_edits.py b/tests/unit/test_memedit_edits.py new file mode 100644 index 0000000..fdf971c --- /dev/null +++ b/tests/unit/test_memedit_edits.py @@ -0,0 +1,82 @@ +try: + import mox +except ImportError: + from mox3 import mox +from tests.unit import base + +__builtins__["_"] = lambda s: s + +memedit = None + + +class TestEdits(base.BaseTest): + def setUp(self): + global memedit + super(TestEdits, self).setUp() + base.mock_gtk() + from chirp.ui import memedit as memedit_module + memedit = memedit_module + + def tearDown(self): + super(TestEdits, self).tearDown() + base.unmock_gtk() + + def _test_tone_column_change(self, col, + ini_tmode='', ini_cmode='', + exp_tmode=None, exp_cmode=None): + editor = self.mox.CreateMock(memedit.MemoryEditor) + editor._config = self.mox.CreateMockAnything() + editor._config.get_bool("no_smart_tmode").AndReturn(False) + editor.col = lambda x: x + editor.store = self.mox.CreateMockAnything() + editor.store.get_iter('path').AndReturn('iter') + editor.store.get('iter', 'Tone Mode', 'Cross Mode').AndReturn( + (ini_tmode, ini_cmode)) + if exp_tmode: + editor.store.set('iter', 'Tone Mode', exp_tmode) + if exp_cmode and col != 'Cross Mode': + editor.store.set('iter', 'Cross Mode', exp_cmode) + self.mox.ReplayAll() + memedit.MemoryEditor.ed_tone_field(editor, None, 'path', None, col) + + def _test_auto_tone_mode(self, col, exp_tmode, exp_cmode): + cross_exp_cmode = (exp_tmode == "Cross" and exp_cmode or None) + + # No tmode -> expected tmode, maybe requires cross mode change + self._test_tone_column_change(col, exp_tmode=exp_tmode, + exp_cmode=cross_exp_cmode) + + # Expected tmode does not re-set tmode, may change cmode + self._test_tone_column_change(col, ini_tmode=exp_tmode, + exp_cmode=cross_exp_cmode) + + # Invalid tmode -> expected, may change cmode + self._test_tone_column_change(col, ini_tmode="foo", + exp_tmode=exp_tmode, + exp_cmode=cross_exp_cmode) + + # Expected cmode does not re-set cmode + self._test_tone_column_change(col, ini_tmode="Cross", + ini_cmode=exp_cmode) + + # Invalid cmode -> expected + self._test_tone_column_change(col, ini_tmode="Cross", + ini_cmode="foo", exp_cmode=exp_cmode) + + def test_auto_tone_mode_tone(self): + self._test_auto_tone_mode('Tone', 'Tone', 'Tone->Tone') + + def test_auto_tone_mode_tsql(self): + self._test_auto_tone_mode('ToneSql', 'TSQL', 'Tone->Tone') + + def test_auto_tone_mode_dtcs(self): + self._test_auto_tone_mode('DTCS Code', 'DTCS', 'DTCS->') + + def test_auto_tone_mode_dtcs_rx(self): + self._test_auto_tone_mode('DTCS Rx Code', 'Cross', '->DTCS') + + def test_auto_tone_mode_dtcs_pol(self): + self._test_auto_tone_mode('DTCS Pol', 'DTCS', 'DTCS->') + + def test_auto_tone_mode_cross(self): + self._test_auto_tone_mode('Cross Mode', 'Cross', 'Tone->Tone') diff --git a/tests/unit/test_platform.py b/tests/unit/test_platform.py new file mode 100644 index 0000000..394f894 --- /dev/null +++ b/tests/unit/test_platform.py @@ -0,0 +1,62 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +try: + import mox +except ImportError: + from mox3 import mox +import os + +from tests.unit import base +from chirp import platform + + +class Win32PlatformTest(base.BaseTest): + def _test_init(self): + self.mox.StubOutWithMock(platform, 'comports') + self.mox.StubOutWithMock(os, 'mkdir') + self.mox.StubOutWithMock(os, 'getenv') + os.mkdir(mox.IgnoreArg()) + os.getenv("APPDATA").AndReturn("foo") + os.getenv("USERPROFILE").AndReturn("foo") + + def test_init(self): + self._test_init() + self.mox.ReplayAll() + platform.Win32Platform() + + def test_serial_ports_sorted(self): + self._test_init() + + fake_comports = [] + numbers = [1, 11, 2, 12, 7, 3, 123] + for i in numbers: + fake_comports.append(("COM%i" % i, None, None)) + + platform.comports().AndReturn(fake_comports) + self.mox.ReplayAll() + ports = platform.Win32Platform().list_serial_ports() + + correct_order = ["COM%i" % i for i in sorted(numbers)] + self.assertEqual(ports, correct_order) + + def test_serial_ports_bad_portnames(self): + self._test_init() + + platform.comports().AndReturn([('foo', None, None)]) + self.mox.ReplayAll() + ports = platform.Win32Platform().list_serial_ports() + self.assertEqual(ports, ['foo']) diff --git a/tests/unit/test_repeaterbook.py b/tests/unit/test_repeaterbook.py new file mode 100644 index 0000000..2563b3f --- /dev/null +++ b/tests/unit/test_repeaterbook.py @@ -0,0 +1,28 @@ +import tempfile +import unittest + +from chirp import chirp_common +from chirp.drivers import repeaterbook + + +class TestRepeaterBook(unittest.TestCase): + def _fetch_and_load(self, query): + fn = tempfile.mktemp('.csv') + chirp_common.urlretrieve(query, fn) + radio = repeaterbook.RBRadio(fn) + return fn, radio + + def test_political(self): + query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \ + "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \ + "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%" + query = query % ('41', '%%', '005') + self._fetch_and_load(query) + + def test_proximity(self): + loc = '97124' + band = '%%' + dist = '20' + query = "https://www.repeaterbook.com/repeaters/downloads/CHIRP/" \ + "app_direct.php?loc=%s&band=%s&dist=%s" % (loc, band, dist) + self._fetch_and_load(query) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py new file mode 100644 index 0000000..ccc4308 --- /dev/null +++ b/tests/unit/test_settings.py @@ -0,0 +1,140 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.unit import base +from chirp import settings + + +class TestSettingValues(base.BaseTest): + def _set_and_test(self, rsv, *values): + for value in values: + rsv.set_value(value) + self.assertEqual(rsv.get_value(), value) + + def _set_and_catch(self, rsv, *values): + for value in values: + self.assertRaises(settings.InvalidValueError, + rsv.set_value, value) + + def test_radio_setting_value_integer(self): + value = settings.RadioSettingValueInteger(0, 10, 5) + self.assertEqual(value.get_value(), 5) + self._set_and_test(value, 1, 0, 10) + self._set_and_catch(value, -1, 11) + + def test_radio_setting_value_float(self): + value = settings.RadioSettingValueFloat(1.0, 10.5, 5.0) + self.assertEqual(value.get_value(), 5.0) + self._set_and_test(value, 2.5, 1.0, 10.5) + self._set_and_catch(value, 0.9, 10.6, -1.5) + + def test_radio_setting_value_boolean(self): + value = settings.RadioSettingValueBoolean(True) + self.assertTrue(value.get_value()) + self._set_and_test(value, True, False) + + def test_radio_setting_value_list(self): + opts = ["Abc", "Def", "Ghi"] + value = settings.RadioSettingValueList(opts, "Abc") + self.assertEqual(value.get_value(), "Abc") + self.assertEqual(int(value), 0) + self._set_and_test(value, "Def", "Ghi", "Abc") + self._set_and_catch(value, "Jkl", "Xyz") + self.assertEqual(value.get_options(), opts) + + def test_radio_setting_value_string(self): + value = settings.RadioSettingValueString(1, 5, "foo", autopad=False) + self.assertEqual(value.get_value(), "foo") + self.assertEqual(str(value), "foo") + self._set_and_test(value, "a", "abc", "abdef") + self._set_and_catch(value, "", "abcdefg") + + def test_validate_callback(self): + class TestException(Exception): + pass + + value = settings.RadioSettingValueString(0, 5, "foo", autopad=False) + + def test_validate(val): + if val == "bar": + raise TestException() + value.set_validate_callback(test_validate) + value.set_value("baz") + self.assertRaises(TestException, value.set_value, "bar") + + def test_changed(self): + value = settings.RadioSettingValueBoolean(False) + self.assertFalse(value.changed()) + value.set_value(False) + self.assertFalse(value.changed()) + value.set_value(True) + self.assertTrue(value.changed()) + + +class TestSettingContainers(base.BaseTest): + def test_radio_setting_group(self): + s1 = settings.RadioSetting("s1", "Setting 1") + s2 = settings.RadioSetting("s2", "Setting 2") + s3 = settings.RadioSetting("s3", "Setting 3") + group = settings.RadioSettingGroup("foo", "Foo Group", s1) + self.assertEqual(group.get_name(), "foo") + self.assertEqual(group.get_shortname(), "Foo Group") + self.assertEqual(group.values(), [s1]) + self.assertEqual(group.keys(), ["s1"]) + group.append(s2) + self.assertEqual(group.items(), [("s1", s1), ("s2", s2)]) + self.assertEqual(group["s1"], s1) + group["s3"] = s3 + self.assertEqual(group.values(), [s1, s2, s3]) + self.assertEqual(group.keys(), ["s1", "s2", "s3"]) + self.assertEqual([x for x in group], [s1, s2, s3]) + + def set_dupe(): + group["s3"] = s3 + self.assertRaises(KeyError, set_dupe) + + def test_radio_setting(self): + val = settings.RadioSettingValueBoolean(True) + rs = settings.RadioSetting("foo", "Foo", val) + self.assertEqual(rs.value, val) + rs.value = False + self.assertEqual(val.get_value(), False) + + def test_radio_setting_multi(self): + val1 = settings.RadioSettingValueBoolean(True) + val2 = settings.RadioSettingValueBoolean(False) + rs = settings.RadioSetting("foo", "Foo", val1, val2) + self.assertEqual(rs[0], val1) + self.assertEqual(rs[1], val2) + rs[0] = False + rs[1] = True + self.assertEqual(val1.get_value(), False) + self.assertEqual(val2.get_value(), True) + + def test_apply_callback(self): + class TestException(Exception): + pass + + rs = settings.RadioSetting("foo", "Foo") + self.assertFalse(rs.has_apply_callback()) + + def test_cb(setting, data1, data2): + self.assertEqual(setting, rs) + self.assertEqual(data1, "foo") + self.assertEqual(data2, "bar") + raise TestException() + rs.set_apply_callback(test_cb, "foo", "bar") + self.assertTrue(rs.has_apply_callback()) + self.assertRaises(TestException, rs.run_apply_callback) diff --git a/tests/unit/test_shiftdialog.py b/tests/unit/test_shiftdialog.py new file mode 100644 index 0000000..fe0b62d --- /dev/null +++ b/tests/unit/test_shiftdialog.py @@ -0,0 +1,112 @@ +import unittest +try: + import mox +except ImportError: + from mox3 import mox + +from tests.unit import base +from chirp import chirp_common +from chirp import errors + +shiftdialog = None + + +class FakeRadio(object): + def __init__(self, *memories): + self._mems = {} + for location in memories: + mem = chirp_common.Memory() + mem.number = location + self._mems[location] = mem + self._features = chirp_common.RadioFeatures() + + def get_features(self): + return self._features + + def get_memory(self, location): + try: + return self._mems[location] + except KeyError: + mem = chirp_common.Memory() + mem.number = location + mem.empty = True + return mem + + def set_memory(self, memory): + self._mems[memory.number] = memory + + def erase_memory(self, location): + del self._mems[location] + + +class FakeRadioThread(object): + def __init__(self, radio): + self.radio = radio + + def lock(self): + pass + + def unlock(self): + pass + + +class ShiftDialogTest(base.BaseTest): + def setUp(self): + global shiftdialog + super(ShiftDialogTest, self).setUp() + base.mock_gtk() + from chirp.ui import shiftdialog as shiftdialog_module + shiftdialog = shiftdialog_module + + def tearDown(self): + super(ShiftDialogTest, self).tearDown() + base.unmock_gtk() + + def _test_hole(self, fn, starting, arg, expected): + radio = FakeRadio(*tuple(starting)) + radio.get_features().memory_bounds = (0, 5) + sd = shiftdialog.ShiftDialog(FakeRadioThread(radio)) + + if isinstance(arg, tuple): + getattr(sd, fn)(*arg) + else: + getattr(sd, fn)(arg) + + self.assertEqual(expected, sorted(radio._mems.keys())) + self.assertEqual(expected, + sorted([mem.number for mem in radio._mems.values()])) + + def _test_delete_hole(self, starting, arg, expected): + self._test_hole('_delete_hole', starting, arg, expected) + + def _test_insert_hole(self, starting, pos, expected): + self._test_hole('_insert_hole', starting, pos, expected) + + def test_delete_hole_with_hole(self): + self._test_delete_hole([1, 2, 3, 5], + 2, + [1, 2, 5]) + + def test_delete_hole_without_hole(self): + self._test_delete_hole([1, 2, 3, 4, 5], + 2, + [1, 2, 3, 4]) + + def test_delete_hole_with_all(self): + self._test_delete_hole([1, 2, 3, 5], + (2, True), + [1, 2, 4]) + + def test_delete_hole_with_all_full(self): + self._test_delete_hole([1, 2, 3, 4, 5], + (2, True), + [1, 2, 3, 4]) + + def test_insert_hole_with_space(self): + self._test_insert_hole([1, 2, 3, 5], + 2, + [1, 3, 4, 5]) + + def test_insert_hole_without_space(self): + self.assertRaises(errors.InvalidMemoryLocation, + self._test_insert_hole, [1, 2, 3, 4, 5], 2, []) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000..575f3e3 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,21 @@ +from chirp import util +from tests.unit import base + + +class TestUtils(base.BaseTest): + def test_hexprint_with_string(self): + util.hexprint('00000000000000') + + def test_hexprint_with_bytes(self): + util.hexprint(b'00000000000000') + + def test_struct_pack(self): + struct = util.StringStruct + + self.assertEqual('\x00c', + struct.pack('bc', 0, 'c')) + + def test_struct_unpack(self): + struct = util.StringStruct + + self.assertEqual((1, 'c'), struct.unpack('bc', '\x01c')) diff --git a/tests/unit/test_yaesu_clone.py b/tests/unit/test_yaesu_clone.py new file mode 100644 index 0000000..869c3b5 --- /dev/null +++ b/tests/unit/test_yaesu_clone.py @@ -0,0 +1,41 @@ +from builtins import bytes +import unittest + +from chirp.drivers import yaesu_clone +from chirp import memmap + + +class TestYaesuChecksum(unittest.TestCase): + def _test_checksum(self, mmap): + cs = yaesu_clone.YaesuChecksum(0, 2, 3) + + self.assertEqual(42, cs.get_existing(mmap)) + self.assertEqual(0x8A, cs.get_calculated(mmap)) + try: + mmap = mmap.get_byte_compatible() + mmap[0] = 3 + except AttributeError: + # str or bytes + try: + # str + mmap = memmap.MemoryMap('\x03' + mmap[1:]) + except TypeError: + # bytes + mmap = memmap.MemoryMapBytes(b'\x03' + mmap[1:]) + + cs.update(mmap) + self.assertEqual(95, cs.get_calculated(mmap)) + + def test_with_MemoryMap(self): + mmap = memmap.MemoryMap('...\x2A') + self._test_checksum(mmap) + + def test_with_MemoryMapBytes(self): + mmap = memmap.MemoryMapBytes(bytes(b'...\x2A')) + self._test_checksum(mmap) + + def test_with_bytes(self): + self._test_checksum(b'...\x2A') + + def test_with_str(self): + self._test_checksum('...\x2A') diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000..a9f08af --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,35 @@ +# +# +# Makefile for serialsniff +# +# Author: IT2 Stuart Blake Tener, USNR (N3GWG) +# +# Version 1.00 +# +# This makefile checks to see if we are running under the OS "Darwin" +# and passes the proper macro flag ("MACOS") if we are. +# +# Incidentally, it is noteworthy that MacOS (Darwin) is not the only +# operating system which is deficient some of the library routines +# MacOS is currently defficient. Thus, it is instructive that other +# operating systems names might need to be added for compilation to +# become occurring in their environments. +# +# + +ifndef SYSNAME + SYSNAME := $(shell uname -s) + ifeq ($(SYSNAME),Darwin) + MYFLAGS := "-DMACOS" + REMOVE := srm -vrfz + else + REMOVE := rm -rf + endif +endif + +serialsniff: serialsniff.c + $(CC) $? -o $@ $(LDFLAGS) $(CFLAGS) $(MYFLAGS) + +clean: + $(REMOVE) serialsniff *~ *.o *.bak core tags shar a.out + diff --git a/tools/bitdiff.py b/tools/bitdiff.py new file mode 100644 index 0000000..d06bdb2 --- /dev/null +++ b/tools/bitdiff.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# +# Copyright 2013 Jens Jensen AF5MI + +import sys +import os +import argparse +import time + + +def printDiff(pos, byte1, byte2, args): + bits1 = '{0:08b}'.format(byte1) + bits2 = '{0:08b}'.format(byte2) + print "@%04Xh" % pos + print "1:%02Xh, %sb" % (byte1, bits1) + print "2:%02Xh, %sb" % (byte2, bits2) + if args.csv: + writeDiffCSV(pos, byte1, byte2, args) + + +def writeDiffCSV(pos, byte1, byte2, args): + bits1 = '{0:08b}'.format(byte1) + bits2 = '{0:08b}'.format(byte2) + csvline = '%s, %s, %04X, %02X, %s, %02X, %s, %s, %s' % \ + (args.file1, args.file2, pos, byte1, bits1, + byte2, bits2, args.setting, args.value) + if not os.path.isfile(args.csv): + fh = open(args.csv, "w") + header = "filename1, filename2, byte_offset, byte1, " \ + "bits1, byte2, bits2, item_msg, value_msg" + fh.write(header + os.linesep) + else: + fh = open(args.csv, "a") + fh.write(csvline + os.linesep) + fh.close() + + +def compareFiles(args): + f1 = open(args.file1, "rb") + f1.seek(args.offset) + f2 = open(args.file2, "rb") + f2.seek(args.offset) + + while True: + pos = f1.tell() - args.offset + c1 = f1.read(1) + c2 = f2.read(1) + if not (c1 and c2): + break + b1 = ord(c1) + b2 = ord(c2) + if b1 != b2: + printDiff(pos, b1, b2, args) + + pos = f1.tell() - args.offset + print "bytes read: %02d" % pos + f1.close() + f2.close() + + +def compareFilesDat(args): + f1 = open(args.file1, "r") + f1contents = f1.read() + f1.close() + f2 = open(args.file2, "r") + f2contents = f2.read() + f2.close() + + f1strlist = f1contents.split() + f1intlist = map(int, f1strlist) + f2strlist = f2contents.split() + f2intlist = map(int, f2strlist) + f1bytes = bytearray(f1intlist) + f2bytes = bytearray(f2intlist) + + length = len(f1intlist) + for i in range(length): + b1 = f1bytes[i] + b2 = f2bytes[i] + pos = i + if b1 != b2: + printDiff(pos, b1, b2, args) + + pos = length + print "bytes read: %02d" % pos + + +def convertFileToBin(args): + f1 = open(args.file1, "r") + f1contents = f1.read() + f1.close() + f1strlist = f1contents.split() + f1intlist = map(int, f1strlist) + f1bytes = bytearray(f1intlist) + f2 = open(args.file2, "wb") + f2.write(f1bytes) + f2.close + + +def convertFileToDat(args): + f1 = open(args.file1, "rb") + f1contents = f1.read() + f1.close() + f2 = open(args.file2, "w") + for i in range(0, len(f1contents)): + f2.write(" %d " % (ord(f1contents[i]), )) + if i % 16 == 15: + f2.write("\r\n") + f2.close + + +# main + +ap = argparse.ArgumentParser(description="byte-/bit- comparison of two files") +ap.add_argument("file1", help="first (reference) file to parse") +ap.add_argument("file2", help="second file to parse") + +mutexgrp1 = ap.add_mutually_exclusive_group() +mutexgrp1.add_argument("-o", "--offset", default=0, + help="offset (hex) to start comparison") +mutexgrp1.add_argument("-d", "--dat", action="store_true", + help="process input files from .DAT/.ADJ format " + "(from 'jujumao' oem programming software " + "for chinese radios)") +mutexgrp1.add_argument("--convert2bin", action="store_true", + help="convert file1 from .dat/.adj to " + "binary image file2") +mutexgrp1.add_argument("--convert2dat", action="store_true", + help="convert file1 from bin to .dat/.adj file2") + +ap.add_argument("-w", "--watch", action="store_true", + help="'watch' changes. runs in a loop") + +csvgrp = ap.add_argument_group("csv output") +csvgrp.add_argument("-c", "--csv", + help="file to append csv results. format: filename1, " + "filename2, byte_offset, byte1, bits1, byte2, " + "bits2, item_msg, value_msg") +csvgrp.add_argument("-s", "--setting", + help="user-meaningful field indicating setting/item " + "modified, e.g. 'beep' or 'txtone'") +csvgrp.add_argument("-v", "--value", + help="user-meaningful field indicating values " + "changed, e.g. 'true->false' or '110.9->100.0'") + +args = ap.parse_args() +if args.offset: + args.offset = int(args.offset, 16) + +print "f1:", args.file1, " f2:", args.file2 +if args.setting or args.value: + print "setting:", args.setting, "- value:", args.value + +while True: + if (args.dat): + compareFilesDat(args) + elif (args.convert2bin): + convertFileToBin(args) + elif (args.convert2dat): + convertFileToDat(args) + else: + compareFiles(args) + if not args.watch: + break + print "------" + time.sleep(delay) diff --git a/tools/check_for_bug.sh b/tools/check_for_bug.sh new file mode 100755 index 0000000..c8b042d --- /dev/null +++ b/tools/check_for_bug.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +# This checks a given revision to make sure that it has a bug number +# in it, of the form "#123". It should be used in your pretxncommit +# hook + +hg log -r $1 --template {desc} | egrep -q "\#[0-9]+" diff --git a/tools/checkpatch.sh b/tools/checkpatch.sh new file mode 100755 index 0000000..5cf3a6f --- /dev/null +++ b/tools/checkpatch.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# +# CHIRP coding standards compliance script +# +# To add a test to this file, create a new check_foo() function +# and then add it to the list of TESTS= below +# + +TESTS="check_long_lines check_bug_number check_commit_message_line_length" + +function check_long_lines() { + local rev="$1" + local files="$2" + + # For now, ignore this check on chirp/ + files=$(echo $files | sed -r 's#\bchirp[^ ]*\b##') + + if [ -z "$files" ]; then + return + fi + + pep8 --select=E501 $files || \ + error "Please use <80 columns in source files" +} + +function check_bug_number() { + local rev="$1" + hg log -vr $rev | grep -qE '#[0-9]+' || \ + error "A bug number is required like #123" +} + +function _less_than_80() { + while true; do + read line + if [ -z "$line" ]; then + break + elif [ $(echo -n "$line" | wc -c) -ge 80 ]; then + return 1 + fi + done +} + +function check_commit_message_line_length() { + local rev="$1" + hg log -vr $rev | (_less_than_80) || \ + error "Please keep commit message lines to <80 columns" +} + +# --- END OF TEST FUNCTIONS --- + +function error() { + echo FAIL: $* + ERROR=1 +} + +function get_touched_files() { + local rev="$1" + hg status -n --change $rev | grep '\.py$' +} + +rev=${1:-tip} +files=$(get_touched_files $rev) + +for testname in $TESTS; do + eval "$testname $rev \"$files\"" +done + +if [ -z "$ERROR" ]; then + echo "Patch '${rev}' is OK" +else + exit 1 +fi diff --git a/tools/cpep8.blacklist b/tools/cpep8.blacklist new file mode 100644 index 0000000..6a98e72 --- /dev/null +++ b/tools/cpep8.blacklist @@ -0,0 +1,3 @@ +# cpep8.blacklist: The list of files that do not meet PEP8 standards. +# DO NOT ADD NEW FILES!! Instead, fix the code to be compliant. +# Over time, this list should shrink and (eventually) be eliminated. diff --git a/tools/cpep8.exceptions b/tools/cpep8.exceptions new file mode 100644 index 0000000..4efef30 --- /dev/null +++ b/tools/cpep8.exceptions @@ -0,0 +1,10 @@ +# This file contains a list of per-file exceptions to the pep8 style rules +# Each line must contain the file name, a tab, and a comma-separated list +# of style rules to ignore in that file. This mechanism should be used +# sparingly and as a measure of last resort. +./share/make_supported.py E402 +./tests/run_tests E402 +./tests/unit/test_memedit_edits.py E402 +./chirpw E402 +./chirp/memmap.py E731 +./chirp/util.py E731 diff --git a/tools/cpep8.manifest b/tools/cpep8.manifest new file mode 100644 index 0000000..368c6d2 --- /dev/null +++ b/tools/cpep8.manifest @@ -0,0 +1,152 @@ +./chirp/__init__.py +./chirp/bandplan.py +./chirp/bandplan_au.py +./chirp/bandplan_iaru_r1.py +./chirp/bandplan_iaru_r2.py +./chirp/bandplan_iaru_r3.py +./chirp/bandplan_na.py +./chirp/bitwise.py +./chirp/bitwise_grammar.py +./chirp/chirp_common.py +./chirp/detect.py +./chirp/directory.py +./chirp/drivers/__init__.py +./chirp/drivers/alinco.py +./chirp/drivers/anytone.py +./chirp/drivers/ap510.py +./chirp/drivers/baofeng_uv3r.py +./chirp/drivers/baofeng_wp970i.py +./chirp/drivers/bjuv55.py +./chirp/drivers/btech.py +./chirp/drivers/ft1500m.py +./chirp/drivers/ft1802.py +./chirp/drivers/ft1d.py +./chirp/drivers/ft2800.py +./chirp/drivers/ft2900.py +./chirp/drivers/ft4.py +./chirp/drivers/ft50.py +./chirp/drivers/ft60.py +./chirp/drivers/ft7100.py +./chirp/drivers/ft7800.py +./chirp/drivers/ft817.py +./chirp/drivers/ft818.py +./chirp/drivers/ft857.py +./chirp/drivers/ft90.py +./chirp/drivers/ftm350.py +./chirp/drivers/generic_csv.py +./chirp/drivers/generic_tpe.py +./chirp/drivers/h777.py +./chirp/drivers/ic208.py +./chirp/drivers/ic2100.py +./chirp/drivers/ic2200.py +./chirp/drivers/ic2720.py +./chirp/drivers/ic2730.py +./chirp/drivers/ic2820.py +./chirp/drivers/ic9x.py +./chirp/drivers/ic9x_icf.py +./chirp/drivers/ic9x_icf_ll.py +./chirp/drivers/ic9x_ll.py +./chirp/drivers/icf.py +./chirp/drivers/icomciv.py +./chirp/drivers/icq7.py +./chirp/drivers/ict70.py +./chirp/drivers/ict7h.py +./chirp/drivers/ict8.py +./chirp/drivers/icw32.py +./chirp/drivers/icx8x.py +./chirp/drivers/icx8x_ll.py +./chirp/drivers/id31.py +./chirp/drivers/id51.py +./chirp/drivers/id800.py +./chirp/drivers/id880.py +./chirp/drivers/idrp.py +./chirp/drivers/kenwood_hmk.py +./chirp/drivers/kenwood_itm.py +./chirp/drivers/kenwood_live.py +./chirp/drivers/kguv8d.py +./chirp/drivers/kguv9dplus.py +./chirp/drivers/kyd.py +./chirp/drivers/leixen.py +./chirp/drivers/puxing.py +./chirp/drivers/radioddity_r2.py +./chirp/drivers/rfinder.py +./chirp/drivers/template.py +./chirp/drivers/th350.py +./chirp/drivers/th9800.py +./chirp/drivers/th_uv3r.py +./chirp/drivers/th_uv3r25.py +./chirp/drivers/th_uv8000.py +./chirp/drivers/th_uvf8d.py +./chirp/drivers/thd72.py +./chirp/drivers/thuv1f.py +./chirp/drivers/tk8102.py +./chirp/drivers/tk8180.py +./chirp/drivers/tmv71.py +./chirp/drivers/tmv71_ll.py +./chirp/drivers/ts480.py +./chirp/drivers/ts590.py +./chirp/drivers/uv5r.py +./chirp/drivers/uvb5.py +./chirp/drivers/vx170.py +./chirp/drivers/vx2.py +./chirp/drivers/vx3.py +./chirp/drivers/vx5.py +./chirp/drivers/vx510.py +./chirp/drivers/vx6.py +./chirp/drivers/vx7.py +./chirp/drivers/vx8.py +./chirp/drivers/vxa700.py +./chirp/drivers/wouxun.py +./chirp/drivers/wouxun_common.py +./chirp/drivers/yaesu_clone.py +./chirp/elib_intl.py +./chirp/errors.py +./chirp/import_logic.py +./chirp/logger.py +./chirp/memmap.py +./chirp/platform.py +./chirp/pyPEG.py +./chirp/radioreference.py +./chirp/settings.py +./chirp/ui/__init__.py +./chirp/ui/bandplans.py +./chirp/ui/bankedit.py +./chirp/ui/clone.py +./chirp/ui/cloneprog.py +./chirp/ui/common.py +./chirp/ui/config.py +./chirp/ui/dstaredit.py +./chirp/ui/editorset.py +./chirp/ui/fips.py +./chirp/ui/importdialog.py +./chirp/ui/inputdialog.py +./chirp/ui/mainapp.py +./chirp/ui/memdetail.py +./chirp/ui/memedit.py +./chirp/ui/miscwidgets.py +./chirp/ui/radiobrowser.py +./chirp/ui/reporting.py +./chirp/ui/settingsedit.py +./chirp/ui/shiftdialog.py +./chirp/util.py +./chirpc +./chirpw +./locale/check_parameters.py +./rpttool +./setup.py +./share/make_supported.py +./tests/__init__.py +./tests/run_tests +./tests/unit/__init__.py +./tests/unit/base.py +./tests/unit/test_bitwise.py +./tests/unit/test_chirp_common.py +./tests/unit/test_import_logic.py +./tests/unit/test_mappingmodel.py +./tests/unit/test_memedit_edits.py +./tests/unit/test_platform.py +./tests/unit/test_settings.py +./tests/unit/test_shiftdialog.py +./tools/bitdiff.py +./tools/cpep8.py +./tools/img2thd72.py diff --git a/tools/cpep8.py b/tools/cpep8.py new file mode 100755 index 0000000..2bf55d0 --- /dev/null +++ b/tools/cpep8.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# cpep8.py - Check Python source files for PEP8 compliance. +# +# Copyright 2015 Zachary T Welch +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import logging +import argparse +import pep8 + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--all", action="store_true", + help="Check all files, ignoring blacklist") +parser.add_argument("-d", "--dir", action="store", default=".", + help="Root directory of source tree") +parser.add_argument("-s", "--stats", action="store_true", + help="Only show statistics") +parser.add_argument("--strict", action="store_true", + help="Ignore listed exceptions") +parser.add_argument("-S", "--scan", action="store_true", + help="Scan for additional files") +parser.add_argument("-u", "--update", action="store_true", + help="Update manifest/blacklist files") +parser.add_argument("-v", "--verbose", action="store_true", + help="Display list of checked files") +parser.add_argument("files", metavar="file", nargs='*', + help="List of files to check (if none, check all)") +args = parser.parse_args() + + +def file_to_lines(name): + fh = file(name, "r") + lines = fh.read().split("\n") + lines.pop() + fh.close() + return lines + + +scriptdir = os.path.dirname(sys.argv[0]) +manifest_filename = os.path.join(scriptdir, "cpep8.manifest") +blacklist_filename = os.path.join(scriptdir, "cpep8.blacklist") +exceptions_filename = os.path.join(scriptdir, "cpep8.exceptions") + +manifest = [] +if args.scan: + for root, dirs, files in os.walk(args.dir): + for f in files: + filename = os.path.join(root, f) + if f.endswith('.py'): + manifest.append(filename) + continue + with file(filename, "r") as fh: + shebang = fh.readline() + if shebang.startswith("#!/usr/bin/env python"): + manifest.append(filename) +else: + manifest += file_to_lines(manifest_filename) + + +# unless we are being --strict, load per-file style exceptions +exceptions = {} +if not args.strict: + exception_lines = file_to_lines(exceptions_filename) + exception_lists = [x.split('\t') + for x in exception_lines if not x.startswith('#')] + for filename, codes in exception_lists: + exceptions[filename] = codes + + +def get_exceptions(f): + try: + ignore = exceptions[f] + except KeyError: + ignore = None + return ignore + +if args.update: + print "Starting update of %d files" % len(manifest) + bad = [] + for f in manifest: + checker = pep8.StyleGuide(quiet=True, ignore=get_exceptions(f)) + results = checker.check_files([f]) + if results.total_errors: + bad.append(f) + print "%s: %s" % (results.total_errors and "FAIL" or "PASS", f) + + with file(blacklist_filename, "w") as fh: + print >>fh, """\ +# cpep8.blacklist: The list of files that do not meet PEP8 standards. +# DO NOT ADD NEW FILES!! Instead, fix the code to be compliant. +# Over time, this list should shrink and (eventually) be eliminated.""" + print >>fh, "\n".join(sorted(bad)) + + if args.scan: + with file(manifest_filename, "w") as fh: + print >>fh, "\n".join(sorted(manifest)) + sys.exit(0) + +if args.files: + manifest = args.files + +# read the blacklisted source files +blacklist = file_to_lines(blacklist_filename) + +check_list = [] +for f in manifest: + if args.all or f not in blacklist: + check_list.append(f) +check_list = sorted(check_list) + +total_errors = 0 +for f in check_list: + if args.verbose: + print "Checking %s" % f + + checker = pep8.Checker(f, quiet=args.stats, ignore=get_exceptions(f)) + results = checker.check_all() + if args.stats: + checker.report.print_statistics() + total_errors += results + +sys.exit(total_errors and 1 or 0) diff --git a/tools/icomsio.sh b/tools/icomsio.sh new file mode 100755 index 0000000..f5f126d --- /dev/null +++ b/tools/icomsio.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# +# ICOM ID-RP* serial helper script +# +# Copyright 2009 Dan Smith +# +# This script will scan the USB bus on this system and determine +# the product ID of any attached ICOM repeater modules. It will +# unload and then reload the FTDI serial driver with the proper +# options to detect the device. After that, it will determine the +# device name and link /dev/icom to that device for easy access. + +LINK="icom" +VENDOR="0x0c26" +DEVICE=$(lsusb -d ${VENDOR}: | cut -d ' ' -f 6 | cut -d : -f 2 | sed -r 's/\n/ /g') + +product_to_name() { + local prod=$1 + + if [ "$prod" = "0012" ]; then + echo "ID-RP2000V TX" + elif [ "$prod" = "0013" ]; then + echo "ID-RP2000V RX" + elif [ "$prod" = "0010" ]; then + echo "ID-RP4000V TX" + elif [ "$prod" = "0011" ]; then + echo "ID-RP4000V RX" + elif [ "$prod" = "000b" ]; then + echo "ID-RP2D" + elif [ "$prod" = "000c" ]; then + echo "ID-RP2V TX" + elif [ "$prod" = "000d" ]; then + echo "ID-RP2V RX" + else + echo "Unknown module (id=${prod})" + fi +} + +if [ $(id -u) != 0 ]; then + echo "This script must be run as root" + exit 1 +fi + +if [ -z "$DEVICE" ]; then + echo "No devices found" + exit 1 +fi + +if echo $DEVICE | grep -q ' '; then + echo "Multiple devices found. Choose one:" + i=0 + for dev in $DEVICE; do + name=$(product_to_name $dev) + echo " ${i}: ${name}" + i=$(($i + 1)) + done + + echo -n "> " + read num + + array=($DEVICE) + + DEVICE=${array[$num]} + if [ -z "$DEVICE" ]; then + exit + fi +fi + +modprobe -r ftdi_sio || { + echo "Unable to unload ftdi_sio" + exit 1 +} + +modprobe ftdi_sio vendor=${VENDOR} product=0x${DEVICE} || { + echo "Failed to load ftdi_sio" + exit 1 +} + +sleep 0.5 + +info=$(lsusb -d ${VENDOR}:0x${DEVICE}) +bus=$(echo $info | cut -d ' ' -f 2 | sed 's/^0*//') +dev=$(echo $info | cut -d ' ' -f 4 | sed 's/^0*//') + +for usbserial in /sys/class/tty/ttyUSB*; do + driver=$(basename $(readlink -f ${usbserial}/device/driver)) + device=$(basename $usbserial) + if [ "$driver" = "ftdi_sio" ]; then + name=$(product_to_name $DEVICE) + ln -sf /dev/${device} /dev/${LINK} + echo "Device $name is /dev/${device} -> /dev/${LINK}" + break + fi +done + diff --git a/tools/img2thd72.py b/tools/img2thd72.py new file mode 100644 index 0000000..407bdf7 --- /dev/null +++ b/tools/img2thd72.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# coding=utf-8 +# ex: set tabstop=4 expandtab shiftwidth=4 softtabstop=4: +# +# © Copyright Vernon Mauery , 2010. All Rights Reserved +# +# This is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This sofware is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this software. If not, see . + + +import sys +import getopt +from PIL import Image as im + + +def die(msg): + print msg + sys.exit(1) + + +def thd72bitmap(fname, invert): + img = im.open(ifname) + if img.size != (120, 48): + die("Image has wrong dimensions: must be 120x48") + + colors = img.getcolors() + if len(colors) != 2: + die("Image must be 1 bits per pixel (black and white)") + + if ('-i', '') in opts: + c, black = colors[0] + c, white = colors[1] + else: + c, white = colors[0] + c, black = colors[1] + + colors = {black: 1, white: 0} + data = img.getdata() + buf = '' + for y in range(6): + for x in range(120): + b = 0 + for i in range(8): + b |= colors[data[x + 120 * (y * 8 + i)]] << i + buf += chr(b) + return buf + + +def display_thd72(buf): + dots = {0: '*', 1: ' '} + lines = [] + for y in range(48): + line = '' + for x in range(120): + byte = y/8*120 + x + line += dots[(ord(buf[byte]) >> (y % 8)) & 0x01] + lines.append(line) + for l in lines: + print l + + +def usage(): + print "\nUsage: %s <-s|-g> [-i] [-d] " \ + " " % sys.argv[0] + print "\nThis program will modify whatever nvram file provided or will" + print "create a new one if the file does not exist. After using this to" + print "modify the image, you can use that file to upload all or part of" + print "it to your radio" + print "\nOption explanations:" + print " -s Save as the startup image" + print " -g Save as the GPS logger image" + print " -i Invert colors (black for white)" + print " Depending on the file format the bits may be inverted." + print " If your bitmap file turns out to be inverted, use -i." + print " -d Display the bitmap as dots (for confirmation)" + print " Each black pixel is '*' and each white pixel is ' '" + print " This will print up to 120 dots wide, so beware your" + print " terminal size." + sys.exit(1) + +if __name__ == "__main__": + opts, args = getopt.getopt(sys.argv[1:], "idgs") + if len(args) != 2: + usage() + ifname = args[0] + ofname = args[1] + invert = ('-i', '') in opts + gps = ('-g', '') in opts + startup = ('-s', '') in opts + if (gps and startup) or not (gps or startup): + usage() + if gps: + imgpos = 0xe800 + tagpos = 18 + else: + imgpos = 0xe500 + tagpos = 17 + + buf = thd72bitmap(ifname, invert) + imgfname = ifname + '\xff' * (48-len(ifname)) + of = file(ofname, "rb+") + of.seek(tagpos) + of.write('\x01') + of.seek(imgpos) + of.write(buf) + of.write(imgfname) + of.seek(65536) + of.close() + + if ('-d', '') in opts: + display_thd72(buf) + + blocks = [0, ] + blocks.append(imgpos/256) + blocks.append(1+imgpos/256) + blocks.append(2+imgpos/256) + print "Modified block list:", blocks diff --git a/tools/serialsniff.c b/tools/serialsniff.c new file mode 100644 index 0000000..7a80ac4 --- /dev/null +++ b/tools/serialsniff.c @@ -0,0 +1,440 @@ +/* + * + * Copyright 2008 Dan Smith + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * + * Modifications made by: + * Stuart Blake Tener, N3GWG + * Email: + * Mobile phone: +1 (310) 358-0202 + * + * 02 MAR 2009 - Version 1.01 + * + * Logic was changed to use "ptsname" instead of "ptsname_r" + * in pursuance of provisioning greater compatibility with other + * Unix variants and Open Standards Unix flavors which have not + * otherwise implemented the "ptsname_r" system call. + * Changes developed and tested under MacOS 10.5.6 (Leopard) + * + * Added "--quiescent" switch, which when used on the command + * line prevents the printing of "Timeout" and count notices + * on the console. + * Changes developed and tested under MacOS 10.5.6 (Leopard) + * + * Added program title and version tagline, printed when the + * software is first started. + * + * 03 MAR 2009 - Version 1.02 + * + * Added "--digits" switch, which when used on the command + * line allows for setting the number of hex digits print per + * line. + * + * Added code to allow "-q" shorthand for "quiescent mode". + * + * Changes were made to add "#ifdef" statements so that only code + * appropriate to MacOS would be compiled if a "#define MACOS" is + * defined early within the source code. + * + * Cleaned up comments in the source for my new source code. + * + * Changes developed and tested under MacOS 10.5.6 (Leopard) + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STREQ(a,b) (strcmp(a,b) == 0) + +char *version = "1.02 (03 MAR 2009)"; +int quiescent = 0; +int total_hex = 20; + +struct path { + int fd; + char path[1024]; + char name[1024]; + int rawlog_fd; +}; + +void hexdump(char *buf, int len, FILE *dest) +{ + /* + * In precedence to the modification of this procedure to support the + * variable size hexadecimal output, the total bytes output was fixed + * to be a length of 8. + * + * The amendment of this procedure to support the "total_hex" variable + * allows for the user to pass a command line argument instantiating a + * desired number of hexadecimal bytes (and their ASCII equivelent) to + * be displayed. + * + */ + + int i; + int j; + + for (i = 0; i < len; i += total_hex) { + for (j = i; j < i + total_hex; j++) { + if ((j % 4) == 0) + fprintf(dest, " "); + + if (j < len) + fprintf(dest, "%02x", buf[j] & 0xFF); + else + fprintf(dest, "--"); + } + + fprintf(dest, " "); + + for (j = i; j < i + total_hex; j++) { + if ((j % 4) == 0) + fprintf(dest, " "); + + if (j > len) + fprintf(dest, "."); + else if ((buf[j] > ' ') && (buf[j] < '~')) + fprintf(dest, "%c", buf[j]); + else + fprintf(dest, "."); + } + + fprintf(dest, "\n"); + } +} + +int saferead(int fd, char *buf, int len) +{ + struct itimerval val; + int ret; + int count = 0; + + memset(&val, 0, sizeof(val)); + val.it_value.tv_usec = 50000; + setitimer(ITIMER_REAL, &val, NULL); + + while (count < len) { + getitimer(ITIMER_REAL, &val); + if ((val.it_value.tv_sec == 0) && + (val.it_value.tv_usec == 0)) { + if (!quiescent) + printf("Timeout\n"); + break; + } + + ret = read(fd, &(buf[count]), len - count); + if (ret > 0) + count += ret; + } + + return count; +} + +void proxy(struct path *pathA, struct path *pathB) +{ + fd_set rfds; + int ret; + struct timeval tv; + + while (1) { + int count = 0; + int ret; + char buf[4096]; + + FD_ZERO(&rfds); + + FD_SET(pathA->fd, &rfds); + FD_SET(pathB->fd, &rfds); + + ret = select(30, &rfds, NULL, NULL, NULL); + if (ret == -1) { + perror("select"); + break; + } + + if (FD_ISSET(pathA->fd, &rfds)) { + count = saferead(pathA->fd, buf, sizeof(buf)); + if (count < 0) + break; + + ret = write(pathB->fd, buf, count); + if (ret != count) + printf("Failed to write %i (%i)\n", count, ret); + if (!quiescent) + printf("%s %i:\n", pathA->name, count); + hexdump(buf, count, stdout); + + if (pathA->rawlog_fd >= 0) { + ret = write(pathA->rawlog_fd, buf, count); + if (ret != count) + printf("Failed to write %i to %s log", + count, + pathA->name); + } + + } + + if (FD_ISSET(pathB->fd, &rfds)) { + count = saferead(pathB->fd, buf, sizeof(buf)); + if (count < 0) + break; + + ret = write(pathA->fd, buf, count); + if (ret != count) + printf("Failed to write %i (%i)\n", count, ret); + if (!quiescent) + printf("%s %i:\n", pathB->name, count); + hexdump(buf, count, stdout); + + if (pathB->rawlog_fd >= 0) { + ret = write(pathB->rawlog_fd, buf, count); + if (ret != count) + printf("Failed to write %i to %s log", + count, + pathB->name); + } + } + } +} + +static bool open_pty(struct path *path) +{ +#ifdef MACOS + char *ptsname_path; +#endif + + path->fd = posix_openpt(O_RDWR); + if (path->fd < 0) { + perror("posix_openpt"); + return false; + } + + grantpt(path->fd); + unlockpt(path->fd); + +#ifdef MACOS + ptsname_path = ptsname(path->fd); + strncpy(path->path,ptsname_path,sizeof(path->path) - 1); +#else + ptsname_r(path->fd, path->path, sizeof(path->path)); +#endif + + fprintf(stderr, "%s\n", path->path); + + return true; +} + +static bool open_serial(const char *serpath, struct path *path) +{ + path->fd = open(serpath, O_RDWR); + if (path->fd < 0) + perror(serpath); + + strncpy(path->path, serpath, sizeof(path->path)); + + return path->fd >= 0; +} + +static bool open_socket(const char *foo, struct path *path) +{ + int lfd; + struct sockaddr_in srv; + struct sockaddr_in cli; + unsigned int cli_len = sizeof(cli); + int optval = 1; + + lfd = socket(AF_INET, SOCK_STREAM, 0); + if (lfd < 0) { + perror("socket"); + return false; + } + + srv.sin_family = AF_INET; + srv.sin_port = htons(2000); + srv.sin_addr.s_addr = INADDR_ANY; + + setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); + + if (bind(lfd, (struct sockaddr *)&srv, sizeof(srv)) < 0) { + perror("bind"); + return false; + } + + if (listen(lfd, 1) < 0) { + perror("listen"); + return false; + } + + printf("Waiting...\n"); + + path->fd = accept(lfd, (struct sockaddr *)&cli, &cli_len); + if (path->fd < 0) { + perror("accept"); + return false; + } + + printf("Accepted socket client\n"); + + strcpy(path->path, "SOCKET"); + + return true; +} + +static bool open_path(const char *opt, struct path *path) +{ + if (STREQ(opt, "pty")) + return open_pty(path); + else if (STREQ(opt, "listen")) + return open_socket(opt, path); + else + return open_serial(opt, path); +} + +static bool open_log(const char *filename, struct path *path) +{ + path->rawlog_fd = open(filename, O_WRONLY | O_CREAT, 0644); + if (path->rawlog_fd < 0) + perror(filename); + + return path->rawlog_fd >= 0; +} + +static void usage() +{ + printf("Usage:\n" + "serialsniff [OPTIONS]\n" + "Where OPTIONS are:\n" + "\n" + " -A,--pathA=DEV Path to device A (or 'pty')\n" + " -B,--pathB=DEV Path to device B (or 'pty')\n" + " --logA=FILE Log pathA (raw) to FILE\n" + " --logB=FILE Log pathB (raw) to FILE\n" + " --nameA=NAME Set pathA name to NAME\n" + " --nameB=NAME Set pathB name to NAME\n" + " --q,-q,--quiescent Run in quiescent mode\n" + " --d,-d,--digits Number of hex digits to print in one line\n\n" + " --d=nn or -d nn or --digits nn\n" + "\n" + ); +} + +int main(int argc, char **argv) +{ + + struct sigaction sa; + + struct path pathA; + struct path pathB; + + int c; + + strcpy(pathA.name, "A"); + strcpy(pathB.name, "B"); + pathA.fd = pathA.rawlog_fd = -1; + pathB.fd = pathB.rawlog_fd = -1; + + printf("\nserialsniff - Version %s\n\n",version); + + while (1) { + int optind; + static struct option lopts[] = { + {"pathA", 1, 0, 'A'}, + {"pathB", 1, 0, 'B'}, + {"logA", 1, 0, 1 }, + {"logB", 1, 0, 2 }, + {"nameA", 1, 0, 3 }, + {"nameB", 1, 0, 4 }, + {"quiescent", 0, 0, 'q' }, + {"digits", 1, 0, 'd'}, + {0, 0, 0, 0} + }; + + c = getopt_long(argc, argv, "A:B:d:l:q", + lopts, &optind); + if (c == -1) + break; + + switch (c) { + + case 'A': + if (!open_path(optarg, &pathA)) + return 1; + break; + + case 'B': + if (!open_path(optarg, &pathB)) + return 2; + break; + + case 1: + if (!open_log(optarg, &pathA)) + return 3; + break; + + case 2: + if (!open_log(optarg, &pathB)) + return 4; + break; + + case 3: + strncpy(pathA.name, optarg, sizeof(pathA.name)); + break; + + case 4: + strncpy(pathB.name, optarg, sizeof(pathB.name)); + break; + + case 'q': + quiescent = 1; + break; + + case 'd': + total_hex=atoi(optarg); + break; + + case '?': + return 3; + } + } + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + sigaction(SIGALRM, &sa, NULL); + + if ((pathA.fd < 0) || (pathB.fd < 0)) { + usage(); + return -1; + } + + proxy(&pathA, &pathB); + + return 0; +} diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..32f367e --- /dev/null +++ b/tox.ini @@ -0,0 +1,61 @@ +[tox] +envlist = unit,driver,style,py3clean,py3unit,py3driver +skipsdist = True + +[testenv] +basepython = python2.7 +sitepackages = True +passenv = HOME +whitelist_externals = bash +deps = future + +[testenv:unit] +deps = + pytest + mox + mock + future + pytest-xdist +commands = + pytest --disable-warnings -v tests/unit {posargs} + python ./share/make_supported.py /dev/null + +[testenv:driver] +deps = + future + pytest + pytest-xdist +commands = + pytest --disable-warnings -v tests/test_drivers.py {posargs} + +[testenv:style] +deps = + pep8==1.6.2 + future +commands = + python ./tools/cpep8.py + +[textenv:py3clean] +commands = + py3clean chirp tests + +[testenv:py3unit] +basepython = python3 +sitepackages = False +setenv = + PYTHONPATH=../.. +deps = + -rrequirements.txt + -rtest-requirements.txt +commands = + pytest --disable-warnings -v tests/unit {posargs} + +[testenv:py3driver] +basepython = python3 +sitepackages = False +setenv = + PYTHONPATH=../.. + CHIRP_DEBUG=y +deps = {[testenv:py3unit]deps} +commands = + pytest --disable-warnings -v tests/test_drivers.py {posargs} -- cgit v1.2.3