[hamradio-commits] [chirp] 01/04: New upstream version 20170124

Dave Hibberd hibby-guest at moszumanska.debian.org
Tue Jan 24 20:44:43 UTC 2017


This is an automated email from the git hooks/post-receive script.

hibby-guest pushed a commit to branch master
in repository chirp.

commit 84922147dd2ac531b95e7a52f1e9bcb2e2e397dc
Author: Hibby <d at vehibberd.com>
Date:   Tue Jan 24 20:09:59 2017 +0000

    New upstream version 20170124
---
 PKG-INFO                                       |   2 +-
 chirp/__init__.py                              |   2 +-
 chirp/chirp_common.py                          |   2 +-
 chirp/dmrmarc.py                               | 139 +++++
 chirp/drivers/alinco.py                        |  39 +-
 chirp/drivers/btech.py                         |   5 +-
 chirp/drivers/fd268.py                         |  12 +-
 chirp/drivers/kyd.py                           |   3 +-
 chirp/{__init__.py => drivers/repeaterbook.py} |  27 +-
 chirp/drivers/retevis_rt1.py                   | 747 +++++++++++++++++++++++++
 chirp/drivers/retevis_rt21.py                  |  31 +-
 chirp/drivers/retevis_rt22.py                  |  55 +-
 chirp/drivers/uv5r.py                          |  18 +-
 chirp/drivers/vx8.py                           |   2 +-
 chirp/platform.py                              |   1 +
 chirp/settings.py                              |   1 +
 chirp/ui/mainapp.py                            | 221 +++++++-
 17 files changed, 1234 insertions(+), 73 deletions(-)

diff --git a/PKG-INFO b/PKG-INFO
index f336a43..95f827c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.0
 Name: chirp
-Version: daily-20161123
+Version: daily-20170124
 Summary: UNKNOWN
 Home-page: UNKNOWN
 Author: UNKNOWN
diff --git a/chirp/__init__.py b/chirp/__init__.py
index 33863f9..6f4f7bd 100644
--- a/chirp/__init__.py
+++ b/chirp/__init__.py
@@ -17,7 +17,7 @@ import os
 import sys
 from glob import glob
 
-CHIRP_VERSION="daily-20161123"
+CHIRP_VERSION="daily-20170124"
 
 module_dir = os.path.dirname(sys.modules["chirp"].__file__)
 __all__ = []
diff --git a/chirp/chirp_common.py b/chirp/chirp_common.py
index 008222e..8ec3917 100644
--- a/chirp/chirp_common.py
+++ b/chirp/chirp_common.py
@@ -70,7 +70,7 @@ CROSS_MODES = [
 
 MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY",
          "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR",
-         "FSK", "FSKR"]
+         "FSK", "FSKR", "DMR"]
 
 TONE_MODES = [
     "",
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 <tom at tomh.us>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import 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/alinco.py b/chirp/drivers/alinco.py
index 65ff70f..f1b4225 100644
--- a/chirp/drivers/alinco.py
+++ b/chirp/drivers/alinco.py
@@ -592,6 +592,7 @@ struct {
 } memory[1000];
 """
 
+
 @directory.register
 class AlincoDJG7EG(AlincoStyleRadio):
     """Alinco DJ-G7EG"""
@@ -600,12 +601,14 @@ class AlincoDJG7EG(AlincoStyleRadio):
     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]
+    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"]
 
-    _model = "AL~DJ-G7EG" # This is a bit of a hack to avoid overwriting _identify()
+    # This is a bit of a hack to avoid overwriting _identify()
+    _model = "AL~DJ-G7EG"
     _memsize = 0x1a7c0
     _range = [(500000, 1300000000)]
 
@@ -613,7 +616,7 @@ class AlincoDJG7EG(AlincoStyleRadio):
         rf = chirp_common.RadioFeatures()
         rf.has_dtcs_polarity = False
         rf.has_bank = False
-        rf.has_settings =  False
+        rf.has_settings = False
 
         rf.valid_modes = self.MODES
         rf.valid_tmodes = ["", "Tone", "TSQL", "Cross", "TSQL-R", "DTCS"]
@@ -715,7 +718,8 @@ class AlincoDJG7EG(AlincoStyleRadio):
             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:
+            if self.TMODES[_mem.squelch_type] == "TSQL" and \
+                    _mem.tx_tone != _mem.rx_tone:
                 mem.tmode = "Cross"
                 mem.cross_mode = "Tone->Tone"
             else:
@@ -733,7 +737,7 @@ class AlincoDJG7EG(AlincoStyleRadio):
         # Get a low-level memory object mapped to the image
         _mem = self._memobj.memory[mem.number]
         if mem.empty:
-            _mem.unknown = 0x00 # Maybe 0 is empty, 2 is used?
+            _mem.unknown = 0x00  # Maybe 0 is empty, 2 is used?
         else:
             _mem.unknown = 0x02
             _mem.freq = mem.freq
@@ -746,34 +750,35 @@ class AlincoDJG7EG(AlincoStyleRadio):
                 try:
                     _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1
                 except ValueError:
-                    raise errors.UnsupportedToneError("This radio does not support " +
-                                              "tone %.1fHz" % mem.rtone)
+                    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)
+                    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
+                # 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)
+                    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)
+                    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)
+                    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.name = "\x00".join(mem.name).ljust(32, "\x00")
diff --git a/chirp/drivers/btech.py b/chirp/drivers/btech.py
index 249f29c..0d62ebd 100644
--- a/chirp/drivers/btech.py
+++ b/chirp/drivers/btech.py
@@ -483,6 +483,8 @@ KT8900R_id = "280528"
 
 # LUITON LT-588UV
 LT588UV_fp = "V2G1F4"
+# Added by rstrickoff gen 2 id
+LT588UV_fp1 = "V2G214"
 
 
 #### MAGICS
@@ -2716,4 +2718,5 @@ class LT588UV(BTech):
     _vhf_range = (136000000, 175000000)
     _uhf_range = (400000000, 481000000)
     _magic = MSTRING_KT8900
-    _fileid = [LT588UV_fp, ]
+    _fileid = [LT588UV_fp,
+               LT588UV_fp1]
diff --git a/chirp/drivers/fd268.py b/chirp/drivers/fd268.py
index b524c4f..44e5c66 100644
--- a/chirp/drivers/fd268.py
+++ b/chirp/drivers/fd268.py
@@ -1,4 +1,4 @@
-# Copyright 2015 Pavel Milanes CO7WT, <co7wt at frcuba.co.cu> <pavelmc at gmail.com>
+# Copyright 2015 Pavel Milanes CO7WT <pavelmc at gmail.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -889,7 +889,7 @@ class FD288ARadio(FeidaxinFD2x8yRadio):
 
 @directory.register
 class FD288BRadio(FeidaxinFD2x8yRadio):
-    """Feidaxin FD-288 Radio"""
+    """Feidaxin FD-288B Radio"""
     MODEL = "FD-288B"
     _range = (400000000, 470000000)
     _VFO_DEFAULT = 439000000
@@ -934,3 +934,11 @@ class FD460ARadio(FeidaxinFD2x8yRadio):
     _VFO_DEFAULT = 439000000
     _IDENT = "\xFF\xEE\x4A\xFF"
 
+
+ at 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/kyd.py b/chirp/drivers/kyd.py
index 01ec7b3..c14af0b 100644
--- a/chirp/drivers/kyd.py
+++ b/chirp/drivers/kyd.py
@@ -209,6 +209,7 @@ class MT700Alias(chirp_common.Alias):
     VENDOR = "Plant-Tours"
     MODEL = "MT-700"
 
+
 @directory.register
 class NC630aRadio(chirp_common.CloneModeRadio):
     """KYD NC-630A"""
@@ -509,7 +510,7 @@ class NC630aRadio(chirp_common.CloneModeRadio):
 
         # testing model fingerprint
         if filedata[0x01B8:0x01BE] == cls._fileid:
-             match_model = True
+            match_model = True
 
         if match_size and match_model:
             return True
diff --git a/chirp/__init__.py b/chirp/drivers/repeaterbook.py
similarity index 51%
copy from chirp/__init__.py
copy to chirp/drivers/repeaterbook.py
index 33863f9..26a34fd 100644
--- a/chirp/__init__.py
+++ b/chirp/drivers/repeaterbook.py
@@ -1,4 +1,4 @@
-# Copyright 2008 Dan Smith <dsmith at danplanet.com>
+# Copyright 2016 Tom Hayward <tom at tomh.us>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -13,15 +13,20 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
-import sys
-from glob import glob
+from chirp import chirp_common
+from chirp.drivers import generic_csv
 
-CHIRP_VERSION="daily-20161123"
 
-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)
+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."
+        mem.comment = unicode(mem.comment, 'iso-8859-1')
+        return mem
+
+    def _clean_name(self, headers, line, mem):
+        "Converts iso-8859-1 encoded names to unicode for pyGTK."
+        mem.name = unicode(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..e91300a
--- /dev/null
+++ b/chirp/drivers/retevis_rt1.py
@@ -0,0 +1,747 @@
+# Copyright 2016 Jim Unroe <rock.unroe at gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import 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")
+
+
+ at 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)
+        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
index e1ed491..29f83c7 100644
--- a/chirp/drivers/retevis_rt21.py
+++ b/chirp/drivers/retevis_rt21.py
@@ -46,6 +46,12 @@ struct {
      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
@@ -82,6 +88,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,
@@ -455,6 +463,7 @@ class RT21Radio(chirp_common.CloneModeRadio):
                 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)
@@ -497,6 +506,23 @@ class RT21Radio(chirp_common.CloneModeRadio):
                               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):
@@ -516,7 +542,10 @@ class RT21Radio(chirp_common.CloneModeRadio):
                         obj = self._memobj.settings
                         setting = element.get_name()
 
-                    if setting == "tot":
+                    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))
diff --git a/chirp/drivers/retevis_rt22.py b/chirp/drivers/retevis_rt22.py
index a7098c9..9c0e2aa 100644
--- a/chirp/drivers/retevis_rt22.py
+++ b/chirp/drivers/retevis_rt22.py
@@ -97,21 +97,26 @@ def _rt22_enter_programming_mode(radio):
     serial = radio.pipe
 
     magic = "PROGRGS"
-    try:
+    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)
-    except:
-        _rt22_exit_programming_mode(radio)
-        raise errors.RadioError("Error communicating with radio")
 
-    if not ack:
-        _rt22_exit_programming_mode(radio)
-        raise errors.RadioError("No response from radio")
-    elif ack != CMD_ACK:
-        _rt22_exit_programming_mode(radio)
-        raise errors.RadioError("Radio refused to enter programming mode")
+        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")
@@ -311,11 +316,31 @@ class RT22Radio(chirp_common.CloneModeRadio):
         self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
 
     def sync_in(self):
-        self._mmap = do_download(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):
-        do_upload(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])
@@ -599,3 +624,9 @@ class ZTX6(RT22Radio):
     """Zastone ZT-X6"""
     VENDOR = "Zastone"
     MODEL = "ZT-X6"
+
+ at directory.register
+class LT316(RT22Radio):
+    """Luiton LT-316"""
+    VENDOR = "LUITON"
+    MODEL = "LT-316"
diff --git a/chirp/drivers/uv5r.py b/chirp/drivers/uv5r.py
index 8a2f9ee..83ce148 100644
--- a/chirp/drivers/uv5r.py
+++ b/chirp/drivers/uv5r.py
@@ -1633,9 +1633,24 @@ class RT5RAlias(chirp_common.Alias):
     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)"
+
+
 @directory.register
 class BaofengUV5RGeneric(BaofengUV5R):
-    ALIASES = [UV5XAlias, RT5RAlias]
+    ALIASES = [UV5XAlias, RT5RAlias, RT5RVAlias, RT5Alias]
 
 
 @directory.register
@@ -1719,6 +1734,7 @@ class IntekKT980Radio(BaofengUV5R):
 class BaofengBFF8HPRadio(BaofengUV5R):
     VENDOR = "Baofeng"
     MODEL = "BF-F8HP"
+    ALIASES = [RT5_TPAlias, ]
     _basetype = BASETYPE_F8HP
     _idents = [UV5R_MODEL_291,
                UV5R_MODEL_A58
diff --git a/chirp/drivers/vx8.py b/chirp/drivers/vx8.py
index d65e77c..6e8dd5f 100644
--- a/chirp/drivers/vx8.py
+++ b/chirp/drivers/vx8.py
@@ -550,7 +550,7 @@ class VX8Radio(yaesu_clone.YaesuCloneModeRadio):
         return rf
 
     def get_raw_memory(self, number):
-        return repr(self._memobj.memory[number])
+        return repr(self._memobj.memory[number-1])
 
     def _checksums(self):
         return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
diff --git a/chirp/platform.py b/chirp/platform.py
index 0b9696f..8250af1 100644
--- a/chirp/platform.py
+++ b/chirp/platform.py
@@ -302,6 +302,7 @@ class UnixPlatform(Platform):
         ports = ["/dev/ttyS*",
                  "/dev/ttyUSB*",
                  "/dev/ttyAMA*",
+                 "/dev/ttyACM*",
                  "/dev/cu.*",
                  "/dev/cuaU*",
                  "/dev/cua0*",
diff --git a/chirp/settings.py b/chirp/settings.py
index 210e046..3133903 100644
--- a/chirp/settings.py
+++ b/chirp/settings.py
@@ -251,6 +251,7 @@ class RadioSettingValueMap(RadioSettingValueList):
             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'"""
diff --git a/chirp/ui/mainapp.py b/chirp/ui/mainapp.py
index 23ac948..a6835c9 100644
--- a/chirp/ui/mainapp.py
+++ b/chirp/ui/mainapp.py
@@ -29,7 +29,7 @@ import sys
 
 from chirp.ui import inputdialog, common
 from chirp import platform, directory, util
-from chirp.drivers import generic_xml, generic_csv
+from chirp.drivers import generic_xml, 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
@@ -316,7 +316,8 @@ of file.
 
     def do_open(self, fname=None, tempname=None):
         if not fname:
-            types = [(_("CHIRP Radio Images") + " (*.img)", "*.img"),
+            types = [(_("All files") + " (*.*)", "*"),
+                     (_("CHIRP Radio Images") + " (*.img)", "*.img"),
                      (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
                      (_("CSV Files") + " (*.csv)", "*.csv"),
                      (_("DAT Files") + " (*.dat)", "*.dat"),
@@ -786,7 +787,8 @@ of file.
         return True
 
     def do_import(self):
-        types = [(_("CHIRP Files") + " (*.chirp)", "*.chirp"),
+        types = [(_("All files") + " (*.*)", "*"),
+                 (_("CHIRP Files") + " (*.chirp)", "*.chirp"),
                  (_("CHIRP Radio Images") + " (*.img)", "*.img"),
                  (_("CSV Files") + " (*.csv)", "*.csv"),
                  (_("DAT Files") + " (*.dat)", "*.dat"),
@@ -806,7 +808,66 @@ of file.
         count = eset.do_import(filen)
         reporting.report_model_usage(eset.rthread.radio, "import", count > 0)
 
-    def do_repeaterbook_prompt(self):
+    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, 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("<big><big><b>RepeaterBook</b></big>\r\n" +
@@ -882,9 +943,9 @@ of file.
 
         return True
 
-    def do_repeaterbook(self, do_import):
+    def do_repeaterbook_political(self, do_import):
         self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
-        if not self.do_repeaterbook_prompt():
+        if not self.do_repeaterbook_political_prompt():
             self.window.set_cursor(None)
             return
 
@@ -912,6 +973,7 @@ of file.
         query = query % (code,
                          band and band or "%%",
                          county and county or "%%")
+        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
@@ -927,24 +989,115 @@ of file.
             self.window.set_cursor(None)
             return
 
-        class RBRadio(generic_csv.CSVRadio,
-                      chirp_common.NetworkSourceRadio):
-            VENDOR = "RepeaterBook"
-            MODEL = ""
+        try:
+            # Validate CSV
+            radio = repeaterbook.RBRadio(filename)
+            if radio.errors:
+                reporting.report_misc_error("repeaterbook",
+                                            ("query=%s\n" % query) +
+                                            ("\n") +
+                                            ("\n".join(radio.errors)))
+        except errors.InvalidDataError, e:
+            common.show_error(str(e))
+            self.window.set_cursor(None)
+            return
+        except Exception, e:
+            common.log_exception()
+
+        reporting.report_model_usage(radio, "import", True)
+
+        self.window.set_cursor(None)
+        if do_import:
+            eset = self.get_current_editorset()
+            count = eset.do_import(filename)
+        else:
+            self.do_open_live(radio, read_only=True)
+
+    def do_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()):
+            d.add_field(k[1:], fields[k][0])
+            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 = '%'
 
-            def _clean_comment(self, headers, line, mem):
-                "Converts iso-8859-1 encoded comments to unicode for pyGTK."
-                mem.comment = unicode(mem.comment, 'iso-8859-1')
-                return mem
+        query = "https://www.repeaterbook.com/repeaters/downloads/CHIRP/" \
+                "app_direct.php?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
+        gtk.gdk.window_process_all_updates()
+        while gtk.events_pending():
+            gtk.main_iteration(False)
 
-            def _clean_name(self, headers, line, mem):
-                "Converts iso-8859-1 encoded names to unicode for pyGTK."
-                mem.name = unicode(mem.name, 'iso-8859-1')
-                return mem
+        fn = tempfile.mktemp(".csv")
+        filename, headers = urllib.urlretrieve(query, fn)
+        if not os.path.exists(filename):
+            LOG.error("Failed, headers were: %s", headers)
+            common.show_error(_("RepeaterBook query failed"))
+            self.window.set_cursor(None)
+            return
 
         try:
             # Validate CSV
-            radio = RBRadio(filename)
+            radio = repeaterbook.RBRadio(filename)
             if radio.errors:
                 reporting.report_misc_error("repeaterbook",
                                             ("query=%s\n" % query) +
@@ -1436,14 +1589,18 @@ of file.
             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 ["qrbook", "irbook"]:
-            self.do_repeaterbook(action[0] == "i")
+        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":
@@ -1531,14 +1688,22 @@ of file.
       <menuitem action="download"/>
       <menuitem action="upload"/>
       <menu action="importsrc" name="importsrc">
+        <menuitem action="idmrmarc"/>
         <menuitem action="iradioreference"/>
-        <menuitem action="irbook"/>
+        <menu action="irbook" name="irbook">
+            <menuitem action="irbookpolitical"/>
+            <menuitem action="irbookproximity"/>
+        </menu>
         <menuitem action="ipr"/>
         <menuitem action="irfinder"/>
       </menu>
       <menu action="querysrc" name="querysrc">
+        <menuitem action="qdmrmarc"/>
         <menuitem action="qradioreference"/>
-        <menuitem action="qrbook"/>
+        <menu action="qrbook" name="qrbook">
+            <menuitem action="qrbookpolitical"/>
+            <menuitem action="qrbookproximity"/>
+        </menu>
         <menuitem action="qpr"/>
         <menuitem action="qrfinder"/>
       </menu>
@@ -1609,17 +1774,27 @@ of file.
             ('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),

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-hamradio/chirp.git



More information about the pkg-hamradio-commits mailing list