# Copyright 2017 # # Developed for the Radio Shack PRO-649 programmable 200-channel scanner by Rick DeWitt (AA0RD) # AA0RD@yahoo.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 . import struct import logging import math import binascii #Used for debugging outputs 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 unk6:1 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[7]; } mod_id; """ MEM_SIZE = 0xE80 BLOCK_SIZE = 32 # 8 4-byte Chans; no 2-byte checksum CHANS_BLOCK = 8 BLOCKS = 116 STIMEOUT = 2 BAUD_RATE = 4800 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 do_download(radio): """Download Scanner Memory.""" radio.pipe.baudrate = BAUD_RATE radio.pipe.timeout = STIMEOUT radio.pipe.xonxof = True # For dump mode # 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 ----!!! data = "" serial.write("\xec") # 'echo' dump ack = serial.read(1) # cmd readback LOG.warning("CfgAck: " + binascii.b2a_hex(ack)) for nb in range(0, BLOCKS): serial.write("\x44") # Gimme a block ack = serial.read(1) LOG.warning("44Ack: " + binascii.b2a_hex(ack)) for nc in range(0, CHANS_BLOCK): cnt = 1 chx = serial.read(cnt) if len(chx) == 0: msg = "Timeout reading data from scanner; check your cable." raise errors.RadioError(msg) if len(chx) != cnt: msg = "Error reading data from scanner: not the amount of data expected." raise errors.RadioError(msg) data += chx LOG.warning("chx:" + binascii.b2a_hex(chx)) # end for nc cbx = serial.read(2) # get 2 checksum bytes and ignore # UI Update after each 8-chan block status.cur = BLOCKS radio.status_fn(status) # end for nb # Append model code data += "PRO-649" 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 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" ALIASES = [WS1010Alias, PRS404Alias, PSR100Alias] NAME_LENGTH = 7 @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. If you try it you will see 'Sending...' but the interface will time out. """)) 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. """)) 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_comment = False rf.has_tuning_step = False rf.can_odd_split = False rf.has_name = True rf.has_offset = False rf.has_mode = True # Using for Delay on/off rf.valid_modes = ["DV","Auto"] rf.has_dtcs = False rf.has_rx_dtcs = False rf.has_dtcs_polarity = False rf.has_ctone = False rf.has_cross = False rf.valid_name_length = self.NAME_LENGTH rf.valid_duplexes = [] rf.valid_skips = ["", "S", "P"] rf.memory_bounds = (1, 200) # This radio supports memories 1-200 rf.valid_bands = [(29000000, 54000000), # 10m, 6m, VHF-Low (108000000, 136987500), # Aircraft (133700000, 174000000), # 2m, Military,land-mobile, VHF-hi (380000000, 512000000), # 70-centimeters, UH-Air, 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) except errors.RadioError: # Pass through any real errors we raise raise except: # If anything unexpected happens, make sure we raise # a RadioError and log the problem LOG.exception('Unexpected error during download') raise errors.RadioError('Unexpected error communicating ' 'with the radio') self._mmap = memmap.MemoryMap(data) self.process_mmap() self._memobj = bitwise.parse(MEM_FORMAT, self._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): """Standard function call to return selected object representation string.""" return repr(self._memobj.chans[number-1]) + repr(self._memobj.names[number-1]) 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 _mem = self._memobj.chans[number-1] # chans array is base-0, number ("loc") is base 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.freq = _mem.rxfreq * 1250.0 # 1000 * 12.5 step for char in _nam.id: if char != 0xff: mem.name += chr(char) mem.skip = "" if ((_prfrq != 0.0) and (mem.freq == _prfrq)): # This is the priority scan channel mem.skip = "P" prix = number if (_mem.lockout ): # T/F mem.skip = "S" mem.mode = "Auto" # Delay On if (_mem.delay ): mem.mode = "DV" # We'll consider any blank (i.e. 0MHz frequency) to be empty if mem.freq == 0: mem.empty = True mem.mode = "Auto" mem.skip = "S" mem.name = "None" 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] # Convert to low-level frequency representation _mem.rxfreq = int(mem.freq / 1250.0) _namelength = self.get_features().valid_name_length for i in range(_namelength): try: _nam.id[i] = ord(mem.name[i]) except IndexError: _nam.id[i] = 0xFF # Set Lockout and Delay (mode) _mem.lockout = (mem.skip == "S") _mem.delay = (mem.mode == "DV") if (mem.skip == "P" ): # User has designated this chan as the Priority freq setattr(_sets, "pri_frq", _mem.rxfreq) # only the last one sticks prix = mem.number if ((mem.number == prix) and (mem.skip != "P")): # Clear the priority freq 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 # define mem struct write-back shortcut _bnks = self._memobj.banks basic = RadioSettingGroup("basic", "Basic") group = RadioSettings(basic) def Htz2raw(setting, obj, atrb): # < Callback """Convert floating Freq in Mhz to UL24 integer.""" vr = float(str(setting.value)) # This function is not invoked now value = vr * 1250.0 setattr(obj, atrb, value) return def dumfun(setting, obj, atrb): """Dummy call back, to make the setting read-only.""" return 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 + 7: # +'PRO-649' match_size = True # testing the firmware model fingerprint rid = filedata[MEM_SIZE:MEM_SIZE+7] # 'PRO-649' should be appended if rid == cls.MODEL: match_model = True if match_size and match_model: return True else: return False