[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