# HG changeset patch # User Rick DeWitt # 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@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 . + +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" + + +@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 +