[chirp_devel] FIx for #5599

Rick DeWitt
Thu Mar 29 13:14:31 PDT 2018


Attached is the new driver for Radio Shack PRO-649 scanner. Issue #5599
The Clone test will report Failed, since I trap a radio download timeout 
and generate an empty memory set.
The radios were sold with a USB dongle that does not handshake the 
download dump. There may be a future patch to fix this if I ever find a 
dongle that works. Meanwhile it is a "write-only" device. This is 
explained in the download prompts.

-- 
Rick DeWitt
AA0RD
Sequim, Washington, USA
360-681-3494

-------------- next part --------------
# HG changeset patch
# User Rick DeWitt <aa0rd at yahoo.com>
# Date 1522354007 25200
#      Thu Mar 29 13:06:47 2018 -0700
# Node ID 23a731e948781b77f1ed2ffe78d7efc232a73db9
# Parent  1cfdf281afcd8233193e1eb0a7e47bbbfc39f0ca
[rs649.py] New driver

Add new driver for Radio Shack PRO-649 scanner and aliases. Issue #5599

diff -r 1cfdf281afcd -r 23a731e94878 chirp/drivers/rs649.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/rs649.py	Thu Mar 29 13:06:47 2018 -0700
@@ -0,0 +1,620 @@
+# Copyright 2017
+#
+#  Developed for the Radio Shack PRO-649 programmable 200-channel scanner
+#  by Rick DeWitt (AA0RD), AA0RD at yahoo.com
+#  Vers 1.0 : Only processes known memory block
+#
+# This program is free software: you can 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 struct
+import logging
+import math
+import time
+
+LOG = logging.getLogger(__name__)
+
+from chirp import chirp_common, directory, memmap
+from chirp import bitwise, errors, util, platform
+from chirp.settings import RadioSettingGroup, RadioSetting, \
+    RadioSettingValueBoolean, \
+    RadioSettingValueFloat,InvalidValueError, RadioSettings
+from textwrap import dedent
+
+
+MEM_FORMAT = """
+#seekto 0x0;
+struct {
+  ul24 rxfreq;
+  u8  unk7:1
+      fmode:1     // FM mode?; required to be set
+      unk5:1
+      unk4:1
+      delay:1
+      lockout:1
+      unk1:1
+      unk0:1;
+} chans[200];
+
+struct blok {
+  u8 byx[32];
+};
+
+#seekto 0x320;        // Going to store names here
+struct {
+  u8 id[8];           // 8 bytes per name gives 32-byte blocks of 4
+} names[200];         // Uses 1600 bytes, Takes us up to 0x960
+
+#seekto 0x960;
+struct blok empty27[27];    // 27 empty 32-byte blocks
+
+#seekto 0xCC0;            // unknown data here
+struct {
+  u8 unkx1[32];
+} unkblk1;
+
+#seekto 0xCE0;
+struct blok empty9[9];
+
+#seekto 0xE00;    // Settings
+struct {
+  u8  mtb0[27];
+  u8  unk7:1
+      unk6:1
+      unk5:1
+      unk4:1
+      pri_set:1
+      unk2:1
+      unk1:1
+      unk0:1;
+  ul24 pri_frq;
+  u8  ux7:1
+      ux6:1
+      ux5:1
+      ux4:1
+      pri_dly:1
+      ux2:1
+      ux1:1
+      ux0:1;
+} settings;
+
+#seekto 0xE20;        // Bank enable map
+struct {
+  ul16  bnk16:1
+      bnk15:1
+	  bnk14:1
+	  bnk13:1
+	  bnk12:1
+	  bnk11:1
+	  bnk10:1
+	  bnk9:1
+	  bnk8:1
+	  bnk7:1
+	  bnk6:1
+	  bnk5:1
+	  bnk4:1
+	  bnk3:1
+	  bnk2:1
+	  bnk1:1;
+  u8  x1[30];
+} banks;
+
+#seekto 0xE40;
+struct blok x2;        // all 0x30 ???
+
+#seekto 0xE60;         // more 0x30 and 00
+struct blok x3;
+
+#seekto 0xE80;
+struct {
+  char  mod_num[8];
+} mod_id;
+
+"""
+
+MEM_SIZE = 0xE80        # mod_id is extraneous
+BLOCK_SIZE = 32    # 8 4-byte Chans; no 2-byte checksum
+CHANS_BLOCK = 8
+BLOCKS = 116
+STIMEOUT = 2
+BAUD_RATE = 4800
+DLY_LIST=["", "Delay"]
+prix = 0            # Start with no priority channel assigned
+
+def _clean_buffer(radio):
+    """Empty the radio read buffer."""
+    radio.pipe.timeout = 0.005
+    LOG.debug("Cleaning buffer..")
+    junk = radio.pipe.read(256)
+    radio.pipe.timeout = STIMEOUT
+    if junk:
+        LOG.warning("Got %i bytes of junk before starting" % len(junk))
+
+
+def load_empty_mem(self):
+    """Create a blank memory data set"""
+    Spc1 = [0x1A,0x7F,0x23,0x3F,0x93,0xBF,0x96,0x7F,0x5D,0x80,0x64,
+                    0x80]
+    Spc2 = [0x48,0,0x40,0xFB,0,0,0x5E,0x46,8,0,0xB0,0x1E,8,0,0x34,
+            0x1B,8,0,0x59,0xFC]
+    Spc3 = [7,0x0F,0,0,0x15,3,0,9,0x33,0x3F,0x3D,0xFF,0x81,0xFF,0x8C,
+                    0x3F,0x11,0x3F,0x18,0x6F,0x9E,0xFF,0xA4,0x3F,0,0,
+                    0x7F,0,0,0]
+    dx = ""
+    for nb in range(0, BLOCKS):
+        for nc in range(0, BLOCK_SIZE):
+            chx = chr(0)            # Load empty data
+            nx = nb * BLOCK_SIZE + nc
+            if (nx >= 0xCC4 and nx <= 0xCCF):
+                chx = chr(Spc1[nx -0xCC4])
+            if (nx >= 0xE07 and nx <= 0xE1A):
+                # Init Chan and mode, PRI Freq, dir and mode
+                chx = chr(Spc2[nx -0xE07])
+            if (nx >= 0xE22 and nx <= 0xE3F):
+                chx = chr(Spc3[nx -0xE22])
+            if (nx >= 0xE40 and nx <  0xE6A ):
+                chx = chr(0x30)
+            dx += chx
+    dx += self.MODEL.ljust(8)
+    return dx
+
+
+def do_download(radio, flg):
+    """Download Scanner Memory."""
+    # 'flg' is boolean to clear data or not
+    radio.pipe.baudrate = BAUD_RATE
+    radio.pipe.timeout = STIMEOUT
+    # Get the serial port connection
+    serial = radio.pipe
+    _clean_buffer(radio)
+
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = BLOCKS
+    status.msg = "Downloading from Scanner Memory..."
+    radio.status_fn(status)
+
+    #Download data array
+    # ---- SO FAR: Does not read the dump ----!!!
+    Wcmd = "\xec"
+    serial.write(Wcmd)
+    ack = serial.read(len(Wcmd))            #  cmd readback
+    if (flg):
+        data =  ""
+    for nb in range(0, BLOCKS):
+        serial.write("D")        # Gimme a block
+        ack = serial.read(1)
+        cnt = 1
+        time.sleep(.01)
+        for nc in range(0, BLOCK_SIZE):
+            chx = serial.read(cnt)
+            if len(chx) == 0:            # Timeout error; quit reading
+                msg = "Timeout reading data from scanner."
+                # Raise a different error type; trapped in sync_in
+                raise errors.InvalidDataError(msg)
+            if len(chx) != cnt:
+                msg = "Error: Not the amount of data expected."
+                raise errors.RadioError(msg)
+            data += chx
+        # End for nc
+        cbx = serial.read(2)        # get 2 checksum bytes and ignore
+        # UI Update after each 8-chan block
+        status.cur =  nb
+        radio.status_fn(status)
+    # End for nb		
+    data += radio.MODEL.ljust(8)    # Append model code
+    return data
+
+
+def do_upload(radio):
+    """Upload memory to scanner."""
+    radio.pipe.baudrate = BAUD_RATE
+    radio.pipe.parity = "N"
+    radio.pipe.timeout = STIMEOUT
+    # Get the serial port connection
+    serial = radio.pipe
+    _clean_buffer(radio)
+
+    # UI progress
+    status = chirp_common.Status()
+    status.cur = 0
+    status.max = BLOCKS
+    status.msg = "Uploading to Scanner Memory..."
+    radio.status_fn(status)
+
+    # Send prefix
+    serial.write("\x55\xab\xcd\x05\x02")
+    ack = serial.read(5)
+#    LOG.warning("Ack: " +  binascii.b2a_hex(ack))
+
+    # Send 116 35-byte blocks of 8 chan info and checksum
+    i = 0        # data/memory index
+    for nb in range(0, BLOCKS):
+        cksum = 0
+        serial.write("\x55")
+        ack = serial.read(1)
+        for nc in range(0, BLOCK_SIZE):    # Write 32 bytes; 8 4-byte chans
+            chx = radio.get_mmap()[i]            # Returns 1-byte as char
+            serial.write(chx)
+            ack = serial.read(1)
+            cksum += ord(chx)                        # Numeric value of char
+#            LOG.warning("DatAck: " +  binascii.b2a_hex(ack) + " # " + str(cksum))
+            i += 1
+        cb1 = cksum % 256
+        cb2 =int(math.floor(cksum / 256))
+        serial.write(chr(cb1) + chr(cb2))
+        ack = serial.read(2)
+    # UI Update
+        status.cur = nb
+        radio.status_fn(status)
+
+    # next nb
+
+def model_match(cls, data):
+    """Match the opened/downloaded image to the correct version"""
+    if len(data) == 0xE88:
+        rid = data[0xE80:0xE88]
+        return rid.startswith(cls.MODEL)
+    else:
+        return False
+
+
+class WS1010Alias(chirp_common.Alias):
+    """PRO-649 alias for Whistler WS1010."""
+    VENDOR = "Whistler"
+    MODEL = "WS1010"
+
+
+class PRS404Alias(chirp_common.Alias):
+    """PRO-649 alias for Radio Shack PRO-404."""
+    VENDOR= "RadioShack"
+    MODEL = "PRO-404"
+
+
+class PSR100Alias(chirp_common.Alias):
+    """PRO-649 alias GRE PSR-100."""
+    VENDOR = "GRE"
+    MODEL = "PSR-100"
+
+	
+ at directory.register
+class PRO649(chirp_common.CloneModeRadio):
+    """Radio Shack PRO-649 Scanner."""
+    VENDOR = "RadioShack"
+    MODEL = "PRO-649"
+    NAME_LENGTH = 7
+    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
+        "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
+    ALIASES = [WS1010Alias, PRS404Alias, PSR100Alias, ]
+
+
+    @classmethod
+    def get_prompts(cls):
+        """Define the upload and download info prompts."""
+        rp = chirp_common.RadioPrompts()
+        rp.pre_download =_(dedent( """\
+            SORRY! But the PRO649 scanner does not support
+            handshake downloading! 
+            At least not with the standard dongle. 
+            You will see 'Sending...', but the interface will time out,
+            and an empty channel set will be loaded.
+            Enable 'Show Empty' to see the blank channels.
+            
+            Set Comment = 'Delay' to enable scan delay for that channel.
+            Set Skip = S to lockout, P for Priority channel.
+            """))
+
+        rp.pre_upload = _(dedent("""\
+            Follow these instructions to upload your info:
+
+            1 - Turn off your scanner
+            2 - Connect your interface cable
+            3 - Turn on your scanner
+            4 - Do the upload of your scanner data
+            5 - Turn off your scanner
+            6 - Unplug the interface cable.
+            Set Comment = 'Delay' to enable scan delay for that channel.
+            Set Skip = S to lockout, P for Priority channel.
+            """))
+        return rp
+
+
+    # Attributes defined in chirp_common.py class RadioFeatures
+    def get_features(self):
+        """Define valid radio features."""
+        rf = chirp_common.RadioFeatures()
+        rf.has_bank = False
+        rf.has_settings = True
+        rf.has_tuning_step = False
+        rf.can_odd_split = False
+        rf.has_name = True
+        rf.has_offset = False
+        rf.has_comment = True            # Using for Delay on/off
+        rf.has_dtcs = False
+        rf.has_rx_dtcs = False
+        rf.has_dtcs_polarity = False
+        rf.has_ctone = False
+        rf.has_cross = False
+        rf.has_tuning_step = False
+        rf.valid_name_length = self.NAME_LENGTH
+        rf.valid_characters = self.VALID_CHARS
+        rf.valid_duplexes = [""]    # To avoid 'not supported' warning
+        rf.valid_skips = ["", "S", "P"]
+        rf.valid_modes = ["WFM"]
+        rf.memory_bounds = (1, 200)  # This radio supports channels 1-200
+        rf.valid_bands = [(29000000, 54000000),     # 10m, 6m, VHF-Low
+                  (108000000, 136987500),   # Aircraft
+                  (133700000, 174000000),   # 2m, Military,land-mobile, VHF-hi
+                  (380000000, 512000000),   # 70cm, UHF-Air, land-mobile, feds
+                          ]
+        return rf
+
+    # Do a download of the radio from the serial port
+    def sync_in(self):
+        """Standard function call to initiate radio download."""
+        try:
+            data =do_download(self, False)        # Will fail and raise error
+        except errors.RadioError:
+            # Then pass through any real errors we raise
+            raise
+        except errors.InvalidDataError:    # Special case
+            data = load_empty_mem(self)
+        except:
+            # If anything unexpected happens, make sure we raise
+            # a RadioError and log the problem
+            raise errors.RadioError('Unexpected error communicating '
+                                    'with the radio.')
+        self._mmap = memmap.MemoryMap(data)
+        self.process_mmap()
+
+
+    # Do an upload of the radio to the serial port
+    def sync_out(self):
+        """Standard function call to initiate radio upload."""
+        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')
+
+    # This function supports the 'Show Raw Memory' developer function
+    # which is invoked from a UI row right-click pull-down
+    def get_raw_memory(self, number):
+        """Return selected object representation string."""
+        rpx = repr(self._memobj.chans[number - 1])
+        rpx += repr(self._memobj.names[number - 1])
+        return rpx
+
+    def process_mmap(self):
+        """Process the mem map into the mem object"""
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+
+    # 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):
+        """Standard function call to populate the UI rows."""
+        global prix
+        # Get a low-level memory object mapped to the image
+        # chans array is base-0, number ("loc") is base 1
+        _mem = self._memobj.chans[number - 1]
+        _nam = self._memobj.names[number - 1]
+        _sets = self._memobj.settings
+        _prfrq =  _sets.pri_frq * 1250.0
+
+        # Create a high-level memory object to return to the UI
+        mem = chirp_common.Memory()
+
+        mem.number = number
+        mem.mode = "WFM"
+        mem.freq = _mem.rxfreq  * 1250.0            # 1000 * 12.5 step
+        mem.name = ""
+        _namelength = self.get_features().valid_name_length
+        for char in _nam.id:
+            if char != 0xff:
+                mem.name += chr(char)
+                if len(mem.name) >= _namelength:
+                    continue
+        mem.name = mem.name.rstrip()
+        mem.skip =  ""
+        if ((_prfrq  != 0.0) and (mem.freq == _prfrq)):    # This is pri chan
+            mem.skip = "P"
+            prix = number
+        if (_mem.lockout ):        # T/F
+            mem.skip = "S"
+        mem.comment =  DLY_LIST[_mem.delay ]
+        # We'll consider any blank (i.e. 0MHz frequency) to be empty
+        if mem.freq == 0:
+            mem.empty = True
+            mem.comment = ""
+            mem.skip = "S"
+            mem.name = "None"
+            for i in range (0, _namelength + 1):   # needed after CSV import
+                _nam.id[i] = 0xFF
+        else:
+            # Turn on bit6, possible FM mode, after CSV import
+            _mem.fmode = True
+            rx = mem.freq / 1000000.0
+            if (rx >= 108.0) and (rx < 136.99):    #AM Aircraft band
+                _mem.fmode = False
+
+        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):
+        """Standard function call to update raw memory from UI values."""
+        global prix
+        # Get a low-level memory object mapped to the image
+        _mem = self._memobj.chans[mem.number - 1]
+        _sets = self._memobj.settings
+        _nam = self._memobj.names[mem.number - 1]
+
+        _mem.fmode = 1
+        _namelength = self.get_features().valid_name_length
+        if (mem.empty):
+            _mem.lockout = False
+            _mem.delay = False
+            _mem.fmode = True
+            _mem.rxfreq = 0
+            for i in range(0,_namelength + 1):
+                _nam.id[i]= 0xFF
+        else:
+        # Convert to low-level frequency representation
+            _mem.rxfreq = int(mem.freq / 1250.0)
+            _mem.fmode = True        # FM, default
+            rx = mem.freq / 1000000.0            # 123.456
+            if (rx >= 108.0) and (rx < 136.99):    #AM Aircraft band
+                _mem.fmode = False
+
+            for i in range(0, _namelength + 1):
+                try:
+                    _nam.id[i] = ord(mem.name[i])
+                except IndexError:
+                    _nam.id[i] = 0xFF
+
+        # Set Lockout and Delay (comment)
+            _mem.lockout = (mem.skip == "S")
+            if len(mem.comment) > 0:
+                _mem.delay = (mem.comment[0]  == "D")
+            else:
+                 _mem.delay = False
+            if (mem.skip == "P" ):            # Priority freq
+                setattr(_sets, "pri_frq", _mem.rxfreq)    # only the last one
+                prix = mem.number
+            if ((mem.number == prix) and (mem.skip != "P")):   # Clear pri frq
+                setattr(_sets, "pri_frq",0.0)
+
+    def get_settings(self):
+        """Translate the bits in the mem_struct into settings in the UI"""
+        _sets = self._memobj.settings
+        _bnks = self._memobj.banks
+        basic = RadioSettingGroup("basic", "Basic")
+        group = RadioSettings(basic)
+
+        rs = RadioSetting("banks.bnk1", "Bank  1",
+                            RadioSettingValueBoolean((_bnks.bnk1 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk2", "Bank  2",
+                            RadioSettingValueBoolean((_bnks.bnk2 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk3", "Bank  3",
+                            RadioSettingValueBoolean((_bnks.bnk3 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk4", "Bank  4",
+                            RadioSettingValueBoolean((_bnks.bnk4 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk5", "Bank  5",
+                            RadioSettingValueBoolean((_bnks.bnk5 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk6", "Bank  6",
+                            RadioSettingValueBoolean((_bnks.bnk6 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk7", "Bank  7",
+                            RadioSettingValueBoolean((_bnks.bnk7 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk8", "Bank  8",
+                            RadioSettingValueBoolean((_bnks.bnk8 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk9", "Bank  9",
+                            RadioSettingValueBoolean((_bnks.bnk9 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("banks.bnk10", "Bank 10",
+                            RadioSettingValueBoolean((_bnks.bnk10 == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("settings.pri_set", "Priority Scan",
+                            RadioSettingValueBoolean((_sets.pri_set == 1)))
+        basic.append(rs)
+
+        rs = RadioSetting("settings.pri_dly", "Priority Scan Delay",
+                            RadioSettingValueBoolean((_sets.pri_dly == 1)))
+        basic.append(rs)
+
+        shopri = False
+        if (shopri):          # Only for dev/debug, otherwise confuses user
+            val = _sets.pri_frq / 800.0        # display pri freq as read-only
+            rs = RadioSetting("settings.pri_frq", "Priority Scan Freq (MHz)",
+                              RadioSettingValueFloat(0.0, 480.0,val, 0.001,3))
+            rs.set_apply_callback(dumfun, _sets,"pri_frq")
+            basic.append(rs)
+
+        return group       # END get_settings()
+
+    def set_settings(self, settings):
+        """Copy UI settings back into raw memory."""
+        _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):
+        """Test img memory belongs to this driver."""
+        match_size = False
+        match_model = False
+
+        # testing the file data size
+        if len(filedata) == MEM_SIZE + 8:      # +'PRO-649 '
+            match_size = True
+
+        # Testing the firmware model fingerprint for aliases
+        match_model = model_match(cls, filedata)
+		
+        if match_size and match_model:
+            return True
+        else:
+            return False
+


More information about the chirp_devel mailing list