# HG changeset patch # User Rick DeWitt # Date 1529443645 25200 # Tue Jun 19 14:27:25 2018 -0700 # Node ID ed11ed06d671398c9e281b230a73f4c582e6de89 # Parent b08fbd75a49999f7f8054a054d7ae6a8cc45c684 [chirp_common, mainapp, lt725uv] Add support for new "Info" prompt per issue #5889 diff -r b08fbd75a499 -r ed11ed06d671 chirp/chirp_common.py --- a/chirp/chirp_common.py Wed Jun 13 06:14:11 2018 -0700 +++ b/chirp/chirp_common.py Tue Jun 19 14:27:25 2018 -0700 @@ -1,1485 +1,1487 @@ -# Copyright 2008 Dan Smith -# -# 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 . - -import math -from chirp import errors, memmap - -SEPCHAR = "," - -# 50 Tones -TONES = [67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, - 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, - 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, - 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, - 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, - 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, - 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, - 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, - ] - -TONES_EXTRA = [56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, - 62.5, 63.0, 64.0] - -OLD_TONES = list(TONES) -[OLD_TONES.remove(x) for x in [159.8, 165.5, 171.3, 177.3, 183.5, 189.9, - 196.6, 199.5, 206.5, 229.1, 254.1]] - -# 104 DTCS Codes -DTCS_CODES = [ - 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, - 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, - 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, - 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, - 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, - 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, - 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, - 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, - 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, - 731, 732, 734, 743, 754, -] - -# 512 Possible DTCS Codes -ALL_DTCS_CODES = [] -for a in range(0, 8): - for b in range(0, 8): - for c in range(0, 8): - ALL_DTCS_CODES.append((a * 100) + (b * 10) + c) - -CROSS_MODES = [ - "Tone->Tone", - "DTCS->", - "->DTCS", - "Tone->DTCS", - "DTCS->Tone", - "->Tone", - "DTCS->DTCS", - "Tone->" -] - -MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY", - "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR", - "FSK", "FSKR", "DMR"] - -TONE_MODES = [ - "", - "Tone", - "TSQL", - "DTCS", - "DTCS-R", - "TSQL-R", - "Cross", -] - -TUNING_STEPS = [ - 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0, - 125.0, 200.0, - # Need to fix drivers using this list as an index! - 9.0, 1.0, 2.5, -] - -SKIP_VALUES = ["", "S", "P"] - -CHARSET_UPPER_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890" -CHARSET_ALPHANUMERIC = \ - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 1234567890" -CHARSET_ASCII = "".join([chr(x) for x in range(ord(" "), ord("~") + 1)]) - -# http://aprs.org/aprs11/SSIDs.txt -APRS_SSID = ( - "0 Your primary station usually fixed and message capable", - "1 generic additional station, digi, mobile, wx, etc", - "2 generic additional station, digi, mobile, wx, etc", - "3 generic additional station, digi, mobile, wx, etc", - "4 generic additional station, digi, mobile, wx, etc", - "5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)", - "6 Special activity, Satellite ops, camping or 6 meters, etc", - "7 walkie talkies, HT's or other human portable", - "8 boats, sailboats, RV's or second main mobile", - "9 Primary Mobile (usually message capable)", - "10 internet, Igates, echolink, winlink, AVRS, APRN, etc", - "11 balloons, aircraft, spacecraft, etc", - "12 APRStt, DTMF, RFID, devices, one-way trackers*, etc", - "13 Weather stations", - "14 Truckers or generally full time drivers", - "15 generic additional station, digi, mobile, wx, etc") -APRS_POSITION_COMMENT = ( - "off duty", "en route", "in service", "returning", "committed", - "special", "priority", "custom 0", "custom 1", "custom 2", "custom 3", - "custom 4", "custom 5", "custom 6", "EMERGENCY") -# http://aprs.org/symbols/symbolsX.txt -APRS_SYMBOLS = ( - "Police/Sheriff", "[reserved]", "Digi", "Phone", "DX Cluster", - "HF Gateway", "Small Aircraft", "Mobile Satellite Groundstation", - "Wheelchair", "Snowmobile", "Red Cross", "Boy Scouts", "House QTH (VHF)", - "X", "Red Dot", "0 in Circle", "1 in Circle", "2 in Circle", - "3 in Circle", "4 in Circle", "5 in Circle", "6 in Circle", "7 in Circle", - "8 in Circle", "9 in Circle", "Fire", "Campground", "Motorcycle", - "Railroad Engine", "Car", "File Server", "Hurricane Future Prediction", - "Aid Station", "BBS or PBBS", "Canoe", "[reserved]", "Eyeball", - "Tractor/Farm Vehicle", "Grid Square", "Hotel", "TCP/IP", "[reserved]", - "School", "PC User", "MacAPRS", "NTS Station", "Balloon", "Police", "TBD", - "Recreational Vehicle", "Space Shuttle", "SSTV", "Bus", "ATV", - "National WX Service Site", "Helicopter", "Yacht/Sail Boat", "WinAPRS", - "Human/Person", "Triangle", "Mail/Postoffice", "Large Aircraft", - "WX Station", "Dish Antenna", "Ambulance", "Bicycle", - "Incident Command Post", "Dual Garage/Fire Dept", "Horse/Equestrian", - "Fire Truck", "Glider", "Hospital", "IOTA", "Jeep", "Truck", "Laptop", - "Mic-Repeater", "Node", "Emergency Operations Center", "Rover (dog)", - "Grid Square above 128m", "Repeater", "Ship/Power Boat", "Truck Stop", - "Truck (18 wheeler)", "Van", "Water Station", "X-APRS", "Yagi at QTH", - "TDB", "[reserved]" -) - - -def watts_to_dBm(watts): - """Converts @watts in watts to dBm""" - return int(10 * math.log10(int(watts * 1000))) - - -def dBm_to_watts(dBm): - """Converts @dBm from dBm to watts""" - return int(math.pow(10, (dBm - 30) / 10)) - - -class PowerLevel: - """Represents a power level supported by a radio""" - - def __init__(self, label, watts=0, dBm=0): - if watts: - dBm = watts_to_dBm(watts) - self._power = int(dBm) - self._label = label - - def __str__(self): - return str(self._label) - - def __int__(self): - return self._power - - def __sub__(self, val): - return int(self) - int(val) - - def __add__(self, val): - return int(self) + int(val) - - def __eq__(self, val): - if val is not None: - return int(self) == int(val) - return False - - def __lt__(self, val): - return int(self) < int(val) - - def __gt__(self, val): - return int(self) > int(val) - - def __nonzero__(self): - return int(self) != 0 - - def __repr__(self): - return "%s (%i dBm)" % (self._label, self._power) - - -def parse_freq(freqstr): - """Parse a frequency string and return the value in integral Hz""" - freqstr = freqstr.strip() - if freqstr == "": - return 0 - elif freqstr.endswith(" MHz"): - return parse_freq(freqstr.split(" ")[0]) - elif freqstr.endswith(" kHz"): - return int(freqstr.split(" ")[0]) * 1000 - - if "." in freqstr: - mhz, khz = freqstr.split(".") - if mhz == "": - mhz = 0 - khz = khz.ljust(6, "0") - if len(khz) > 6: - raise ValueError("Invalid kHz value: %s", khz) - mhz = int(mhz) * 1000000 - khz = int(khz) - else: - mhz = int(freqstr) * 1000000 - khz = 0 - - return mhz + khz - - -def format_freq(freq): - """Format a frequency given in Hz as a string""" - - return "%i.%06i" % (freq / 1000000, freq % 1000000) - - -class ImmutableValueError(ValueError): - pass - - -class Memory: - """Base class for a single radio memory""" - freq = 0 - number = 0 - extd_number = "" - name = "" - vfo = 0 - rtone = 88.5 - ctone = 88.5 - dtcs = 23 - rx_dtcs = 23 - tmode = "" - cross_mode = "Tone->Tone" - dtcs_polarity = "NN" - skip = "" - power = None - duplex = "" - offset = 600000 - mode = "FM" - tuning_step = 5.0 - - comment = "" - - empty = False - - immutable = [] - - # A RadioSettingGroup of additional settings supported by the radio, - # or an empty list if none - extra = [] - - def __init__(self): - self.freq = 0 - self.number = 0 - self.extd_number = "" - self.name = "" - self.vfo = 0 - self.rtone = 88.5 - self.ctone = 88.5 - self.dtcs = 23 - self.rx_dtcs = 23 - self.tmode = "" - self.cross_mode = "Tone->Tone" - self.dtcs_polarity = "NN" - self.skip = "" - self.power = None - self.duplex = "" - self.offset = 600000 - self.mode = "FM" - self.tuning_step = 5.0 - - self.comment = "" - - self.empty = False - - self.immutable = [] - - _valid_map = { - "rtone": TONES + TONES_EXTRA, - "ctone": TONES + TONES_EXTRA, - "dtcs": ALL_DTCS_CODES, - "rx_dtcs": ALL_DTCS_CODES, - "tmode": TONE_MODES, - "dtcs_polarity": ["NN", "NR", "RN", "RR"], - "cross_mode": CROSS_MODES, - "mode": MODES, - "duplex": ["", "+", "-", "split", "off"], - "skip": SKIP_VALUES, - "empty": [True, False], - "dv_code": [x for x in range(0, 100)], - } - - def __repr__(self): - return "Memory[%i]" % self.number - - def dupe(self): - """Return a deep copy of @self""" - mem = self.__class__() - for k, v in self.__dict__.items(): - mem.__dict__[k] = v - - return mem - - def clone(self, source): - """Absorb all of the properties of @source""" - for k, v in source.__dict__.items(): - self.__dict__[k] = v - - CSV_FORMAT = ["Location", "Name", "Frequency", - "Duplex", "Offset", "Tone", - "rToneFreq", "cToneFreq", "DtcsCode", - "DtcsPolarity", "Mode", "TStep", - "Skip", "Comment", - "URCALL", "RPT1CALL", "RPT2CALL", "DVCODE"] - - def __setattr__(self, name, val): - if not hasattr(self, name): - raise ValueError("No such attribute `%s'" % name) - - if name in self.immutable: - raise ImmutableValueError("Field %s is not " % name + - "mutable on this memory") - - if name in self._valid_map and val not in self._valid_map[name]: - raise ValueError("`%s' is not in valid list: %s" % - (val, self._valid_map[name])) - - self.__dict__[name] = val - - def format_freq(self): - """Return a properly-formatted string of this memory's frequency""" - return format_freq(self.freq) - - def parse_freq(self, freqstr): - """Set the frequency from a string""" - self.freq = parse_freq(freqstr) - return self.freq - - def __str__(self): - if self.tmode == "Tone": - tenc = "*" - else: - tenc = " " - - if self.tmode == "TSQL": - tsql = "*" - else: - tsql = " " - - if self.tmode == "DTCS": - dtcs = "*" - else: - dtcs = " " - - if self.duplex == "": - dup = "/" - else: - dup = self.duplex - - return \ - "Memory %s: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]" % \ - (self.number if self.extd_number == "" else self.extd_number, - format_freq(self.freq), - dup, - format_freq(self.offset), - self.mode, - self.name, - self.rtone, - tenc, - self.ctone, - tsql, - self.dtcs, - dtcs, - self.dtcs_polarity, - self.tuning_step) - - def to_csv(self): - """Return a CSV representation of this memory""" - return [ - "%i" % self.number, - "%s" % self.name, - format_freq(self.freq), - "%s" % self.duplex, - format_freq(self.offset), - "%s" % self.tmode, - "%.1f" % self.rtone, - "%.1f" % self.ctone, - "%03i" % self.dtcs, - "%s" % self.dtcs_polarity, - "%s" % self.mode, - "%.2f" % self.tuning_step, - "%s" % self.skip, - "%s" % self.comment, - "", "", "", ""] - - @classmethod - def _from_csv(cls, _line): - line = _line.strip() - if line.startswith("Location"): - raise errors.InvalidMemoryLocation("Non-CSV line") - - vals = line.split(SEPCHAR) - if len(vals) < 11: - raise errors.InvalidDataError("CSV format error " + - "(14 columns expected)") - - if vals[10] == "DV": - mem = DVMemory() - else: - mem = Memory() - - mem.really_from_csv(vals) - return mem - - def really_from_csv(self, vals): - """Careful parsing of split-out @vals""" - try: - self.number = int(vals[0]) - except: - raise errors.InvalidDataError( - "Location '%s' is not a valid integer" % vals[0]) - - self.name = vals[1] - - try: - self.freq = float(vals[2]) - except: - raise errors.InvalidDataError("Frequency is not a valid number") - - if vals[3].strip() in ["+", "-", ""]: - self.duplex = vals[3].strip() - else: - raise errors.InvalidDataError("Duplex is not +,-, or empty") - - try: - self.offset = float(vals[4]) - except: - raise errors.InvalidDataError("Offset is not a valid number") - - self.tmode = vals[5] - if self.tmode not in TONE_MODES: - raise errors.InvalidDataError("Invalid tone mode `%s'" % - self.tmode) - - try: - self.rtone = float(vals[6]) - except: - raise errors.InvalidDataError("rTone is not a valid number") - if self.rtone not in TONES: - raise errors.InvalidDataError("rTone is not valid") - - try: - self.ctone = float(vals[7]) - except: - raise errors.InvalidDataError("cTone is not a valid number") - if self.ctone not in TONES: - raise errors.InvalidDataError("cTone is not valid") - - try: - self.dtcs = int(vals[8], 10) - except: - raise errors.InvalidDataError("DTCS code is not a valid number") - if self.dtcs not in DTCS_CODES: - raise errors.InvalidDataError("DTCS code is not valid") - - try: - self.rx_dtcs = int(vals[8], 10) - except: - raise errors.InvalidDataError("DTCS Rx code is not a valid number") - if self.rx_dtcs not in DTCS_CODES: - raise errors.InvalidDataError("DTCS Rx code is not valid") - - if vals[9] in ["NN", "NR", "RN", "RR"]: - self.dtcs_polarity = vals[9] - else: - raise errors.InvalidDataError("DtcsPolarity is not valid") - - if vals[10] in MODES: - self.mode = vals[10] - else: - raise errors.InvalidDataError("Mode is not valid") - - try: - self.tuning_step = float(vals[11]) - except: - raise errors.InvalidDataError("Tuning step is invalid") - - try: - self.skip = vals[12] - except: - raise errors.InvalidDataError("Skip value is not valid") - - return True - - -class DVMemory(Memory): - """A Memory with D-STAR attributes""" - dv_urcall = "CQCQCQ" - dv_rpt1call = "" - dv_rpt2call = "" - dv_code = 0 - - def __str__(self): - string = Memory.__str__(self) - - string += " <%s,%s,%s>" % (self.dv_urcall, - self.dv_rpt1call, - self.dv_rpt2call) - - return string - - def to_csv(self): - return [ - "%i" % self.number, - "%s" % self.name, - format_freq(self.freq), - "%s" % self.duplex, - format_freq(self.offset), - "%s" % self.tmode, - "%.1f" % self.rtone, - "%.1f" % self.ctone, - "%03i" % self.dtcs, - "%s" % self.dtcs_polarity, - "%s" % self.mode, - "%.2f" % self.tuning_step, - "%s" % self.skip, - "%s" % self.comment, - "%s" % self.dv_urcall, - "%s" % self.dv_rpt1call, - "%s" % self.dv_rpt2call, - "%i" % self.dv_code] - - def really_from_csv(self, vals): - Memory.really_from_csv(self, vals) - - self.dv_urcall = vals[15].rstrip()[:8] - self.dv_rpt1call = vals[16].rstrip()[:8] - self.dv_rpt2call = vals[17].rstrip()[:8] - try: - self.dv_code = int(vals[18].strip()) - except Exception: - self.dv_code = 0 - - -class MemoryMapping(object): - """Base class for a memory mapping""" - - def __init__(self, model, index, name): - self._model = model - self._index = index - self._name = name - - def __str__(self): - return self.get_name() - - def __repr__(self): - return "%s-%s" % (self.__class__.__name__, self._index) - - def get_name(self): - """Returns the mapping name""" - return self._name - - def get_index(self): - """Returns the immutable index (string or int)""" - return self._index - - def __eq__(self, other): - return self.get_index() == other.get_index() - - -class MappingModel(object): - """Base class for a memory mapping model""" - - def __init__(self, radio, name): - self._radio = radio - self._name = name - - def get_name(self): - return self._name - - def get_num_mappings(self): - """Returns the number of mappings in the model (should be - callable without consulting the radio""" - raise NotImplementedError() - - def get_mappings(self): - """Return a list of mappings""" - raise NotImplementedError() - - def add_memory_to_mapping(self, memory, mapping): - """Add @memory to @mapping.""" - raise NotImplementedError() - - def remove_memory_from_mapping(self, memory, mapping): - """Remove @memory from @mapping. - Shall raise exception if @memory is not in @bank""" - raise NotImplementedError() - - def get_mapping_memories(self, mapping): - """Return a list of memories in @mapping""" - raise NotImplementedError() - - def get_memory_mappings(self, memory): - """Return a list of mappings that @memory is in""" - raise NotImplementedError() - - -class Bank(MemoryMapping): - """Base class for a radio's Bank""" - - -class NamedBank(Bank): - """A bank that can have a name""" - - def set_name(self, name): - """Changes the user-adjustable bank name""" - self._name = name - - -class BankModel(MappingModel): - """A bank model where one memory is in zero or one banks at any point""" - - def __init__(self, radio, name='Banks'): - super(BankModel, self).__init__(radio, name) - - -class MappingModelIndexInterface: - """Interface for mappings with index capabilities""" - - def get_index_bounds(self): - """Returns a tuple (lo,hi) of the min and max mapping indices""" - raise NotImplementedError() - - def get_memory_index(self, memory, mapping): - """Returns the index of @memory in @mapping""" - raise NotImplementedError() - - def set_memory_index(self, memory, mapping, index): - """Sets the index of @memory in @mapping to @index""" - raise NotImplementedError() - - def get_next_mapping_index(self, mapping): - """Returns the next available mapping index in @mapping, or raises - Exception if full""" - raise NotImplementedError() - - -class MTOBankModel(BankModel): - """A bank model where one memory can be in multiple banks at once """ - pass - - -def console_status(status): - """Write a status object to the console""" - import logging - from chirp import logger - if not logger.is_visible(logging.WARN): - return - import sys - import os - sys.stdout.write("\r%s" % status) - if status.cur == status.max: - sys.stdout.write(os.linesep) - - -class RadioPrompts: - """Radio prompt strings""" - experimental = None - pre_download = None - pre_upload = None - display_pre_upload_prompt_before_opening_port = True - - -BOOLEAN = [True, False] - - -class RadioFeatures: - """Radio Feature Flags""" - _valid_map = { - # General - "has_bank_index": BOOLEAN, - "has_dtcs": BOOLEAN, - "has_rx_dtcs": BOOLEAN, - "has_dtcs_polarity": BOOLEAN, - "has_mode": BOOLEAN, - "has_offset": BOOLEAN, - "has_name": BOOLEAN, - "has_bank": BOOLEAN, - "has_bank_names": BOOLEAN, - "has_tuning_step": BOOLEAN, - "has_ctone": BOOLEAN, - "has_cross": BOOLEAN, - "has_infinite_number": BOOLEAN, - "has_nostep_tuning": BOOLEAN, - "has_comment": BOOLEAN, - "has_settings": BOOLEAN, - - # Attributes - "valid_modes": [], - "valid_tmodes": [], - "valid_duplexes": [], - "valid_tuning_steps": [], - "valid_bands": [], - "valid_skips": [], - "valid_power_levels": [], - "valid_characters": "", - "valid_name_length": 0, - "valid_cross_modes": [], - "valid_dtcs_pols": [], - "valid_dtcs_codes": [], - "valid_special_chans": [], - - "has_sub_devices": BOOLEAN, - "memory_bounds": (0, 0), - "can_odd_split": BOOLEAN, - - # D-STAR - "requires_call_lists": BOOLEAN, - "has_implicit_calls": BOOLEAN, - } - - def __setattr__(self, name, val): - if name.startswith("_"): - self.__dict__[name] = val - return - elif name not in self._valid_map.keys(): - raise ValueError("No such attribute `%s'" % name) - - if type(self._valid_map[name]) == tuple: - # Tuple, cardinality must match - if type(val) != tuple or len(val) != len(self._valid_map[name]): - raise ValueError("Invalid value `%s' for attribute `%s'" % - (val, name)) - elif type(self._valid_map[name]) == list and not self._valid_map[name]: - # Empty list, must be another list - if type(val) != list: - raise ValueError("Invalid value `%s' for attribute `%s'" % - (val, name)) - elif type(self._valid_map[name]) == str: - if type(val) != str: - raise ValueError("Invalid value `%s' for attribute `%s'" % - (val, name)) - elif type(self._valid_map[name]) == int: - if type(val) != int: - raise ValueError("Invalid value `%s' for attribute `%s'" % - (val, name)) - elif val not in self._valid_map[name]: - # Value not in the list of valid values - raise ValueError("Invalid value `%s' for attribute `%s'" % (val, - name)) - self.__dict__[name] = val - - def __getattr__(self, name): - raise AttributeError("pylint is confused by RadioFeatures") - - def init(self, attribute, default, doc=None): - """Initialize a feature flag @attribute with default value @default, - and documentation string @doc""" - self.__setattr__(attribute, default) - self.__docs[attribute] = doc - - def get_doc(self, attribute): - """Return the description of @attribute""" - return self.__docs[attribute] - - def __init__(self): - self.__docs = {} - self.init("has_bank_index", False, - "Indicates that memories in a bank can be stored in " + - "an order other than in main memory") - self.init("has_dtcs", True, - "Indicates that DTCS tone mode is available") - self.init("has_rx_dtcs", False, - "Indicates that radio can use two different " + - "DTCS codes for rx and tx") - self.init("has_dtcs_polarity", True, - "Indicates that the DTCS polarity can be changed") - self.init("has_mode", True, - "Indicates that multiple emission modes are supported") - self.init("has_offset", True, - "Indicates that the TX offset memory property is supported") - self.init("has_name", True, - "Indicates that an alphanumeric memory name is supported") - self.init("has_bank", True, - "Indicates that memories may be placed into banks") - self.init("has_bank_names", False, - "Indicates that banks may be named") - self.init("has_tuning_step", True, - "Indicates that memories store their tuning step") - self.init("has_ctone", True, - "Indicates that the radio keeps separate tone frequencies " + - "for repeater and CTCSS operation") - self.init("has_cross", False, - "Indicates that the radios supports different tone modes " + - "on transmit and receive") - self.init("has_infinite_number", False, - "Indicates that the radio is not constrained in the " + - "number of memories that it can store") - self.init("has_nostep_tuning", False, - "Indicates that the radio does not require a valid " + - "tuning step to store a frequency") - self.init("has_comment", False, - "Indicates that the radio supports storing a comment " + - "with each memory") - self.init("has_settings", False, - "Indicates that the radio supports general settings") - - self.init("valid_modes", list(MODES), - "Supported emission (or receive) modes") - self.init("valid_tmodes", [], - "Supported tone squelch modes") - self.init("valid_duplexes", ["", "+", "-"], - "Supported duplex modes") - self.init("valid_tuning_steps", list(TUNING_STEPS), - "Supported tuning steps") - self.init("valid_bands", [], - "Supported frequency ranges") - self.init("valid_skips", ["", "S"], - "Supported memory scan skip settings") - self.init("valid_power_levels", [], - "Supported power levels") - self.init("valid_characters", CHARSET_UPPER_NUMERIC, - "Supported characters for a memory's alphanumeric tag") - self.init("valid_name_length", 6, - "The maximum number of characters in a memory's " + - "alphanumeric tag") - self.init("valid_cross_modes", list(CROSS_MODES), - "Supported tone cross modes") - self.init("valid_dtcs_pols", ["NN", "RN", "NR", "RR"], - "Supported DTCS polarities") - self.init("valid_dtcs_codes", list(DTCS_CODES), - "Supported DTCS codes") - self.init("valid_special_chans", [], - "Supported special channel names") - - self.init("has_sub_devices", False, - "Indicates that the radio behaves as two semi-independent " + - "devices") - self.init("memory_bounds", (0, 1), - "The minimum and maximum channel numbers") - self.init("can_odd_split", False, - "Indicates that the radio can store an independent " + - "transmit frequency") - - self.init("requires_call_lists", True, - "[D-STAR] Indicates that the radio requires all callsigns " + - "to be in the master list and cannot be stored " + - "arbitrarily in each memory channel") - self.init("has_implicit_calls", False, - "[D-STAR] Indicates that the radio has an implied " + - "callsign at the beginning of the master URCALL list") - - def is_a_feature(self, name): - """Returns True if @name is a valid feature flag name""" - return name in self._valid_map.keys() - - def __getitem__(self, name): - return self.__dict__[name] - - def validate_memory(self, mem): - """Return a list of warnings and errors that will be encoundered - if trying to set @mem on the current radio""" - msgs = [] - - lo, hi = self.memory_bounds - if not self.has_infinite_number and \ - (mem.number < lo or mem.number > hi) and \ - mem.extd_number not in self.valid_special_chans: - msg = ValidationWarning("Location %i is out of range" % mem.number) - msgs.append(msg) - - if (self.valid_modes and - mem.mode not in self.valid_modes and - mem.mode != "Auto"): - msg = ValidationError("Mode %s not supported" % mem.mode) - msgs.append(msg) - - if self.valid_tmodes and mem.tmode not in self.valid_tmodes: - msg = ValidationError("Tone mode %s not supported" % mem.tmode) - msgs.append(msg) - else: - if mem.tmode == "Cross": - if self.valid_cross_modes and \ - mem.cross_mode not in self.valid_cross_modes: - msg = ValidationError("Cross tone mode %s not supported" % - mem.cross_mode) - msgs.append(msg) - - if self.has_dtcs_polarity and \ - mem.dtcs_polarity not in self.valid_dtcs_pols: - msg = ValidationError("DTCS Polarity %s not supported" % - mem.dtcs_polarity) - msgs.append(msg) - - if self.valid_dtcs_codes and \ - mem.dtcs not in self.valid_dtcs_codes: - msg = ValidationError("DTCS Code %03i not supported" % mem.dtcs) - if self.valid_dtcs_codes and \ - mem.rx_dtcs not in self.valid_dtcs_codes: - msg = ValidationError("DTCS Code %03i not supported" % mem.rx_dtcs) - - if self.valid_duplexes and mem.duplex not in self.valid_duplexes: - msg = ValidationError("Duplex %s not supported" % mem.duplex) - msgs.append(msg) - - ts = mem.tuning_step - if self.valid_tuning_steps and ts not in self.valid_tuning_steps and \ - not self.has_nostep_tuning: - msg = ValidationError("Tuning step %.2f not supported" % ts) - msgs.append(msg) - - if self.valid_bands: - valid = False - for lo, hi in self.valid_bands: - if lo <= mem.freq < hi: - valid = True - break - if not valid: - msg = ValidationError( - ("Frequency {freq} is out " - "of supported range").format(freq=format_freq(mem.freq))) - msgs.append(msg) - - if self.valid_bands and \ - self.valid_duplexes and \ - mem.duplex in ["split", "-", "+"]: - if mem.duplex == "split": - freq = mem.offset - elif mem.duplex == "-": - freq = mem.freq - mem.offset - elif mem.duplex == "+": - freq = mem.freq + mem.offset - valid = False - for lo, hi in self.valid_bands: - if lo <= freq < hi: - valid = True - break - if not valid: - msg = ValidationError( - ("Tx freq {freq} is out " - "of supported range").format(freq=format_freq(freq))) - msgs.append(msg) - - if mem.power and \ - self.valid_power_levels and \ - mem.power not in self.valid_power_levels: - msg = ValidationWarning("Power level %s not supported" % mem.power) - msgs.append(msg) - - if self.valid_tuning_steps and not self.has_nostep_tuning: - try: - step = required_step(mem.freq) - if step not in self.valid_tuning_steps: - msg = ValidationError("Frequency requires %.2fkHz step" % - required_step(mem.freq)) - msgs.append(msg) - except errors.InvalidDataError, e: - msgs.append(str(e)) - - if self.valid_characters: - for char in mem.name: - if char not in self.valid_characters: - msgs.append(ValidationWarning("Name character " + - "`%s'" % char + - " not supported")) - break - - return msgs - - -class ValidationMessage(str): - """Base class for Validation Errors and Warnings""" - pass - - -class ValidationWarning(ValidationMessage): - """A non-fatal warning during memory validation""" - pass - - -class ValidationError(ValidationMessage): - """A fatal error during memory validation""" - pass - - -class Alias(object): - VENDOR = "Unknown" - MODEL = "Unknown" - VARIANT = "" - - -class Radio(Alias): - """Base class for all Radio drivers""" - BAUD_RATE = 9600 - HARDWARE_FLOW = False - ALIASES = [] - - def status_fn(self, status): - """Deliver @status to the UI""" - console_status(status) - - def __init__(self, pipe): - self.errors = [] - self.pipe = pipe - - def get_features(self): - """Return a RadioFeatures object for this radio""" - return RadioFeatures() - - @classmethod - def get_name(cls): - """Return a printable name for this radio""" - return "%s %s" % (cls.VENDOR, cls.MODEL) - - @classmethod - def get_prompts(cls): - """Return a set of strings for use in prompts""" - return RadioPrompts() - - def set_pipe(self, pipe): - """Set the serial object to be used for communications""" - self.pipe = pipe - - def get_memory(self, number): - """Return a Memory object for the memory at location @number""" - pass - - def erase_memory(self, number): - """Erase memory at location @number""" - mem = Memory() - mem.number = number - mem.empty = True - self.set_memory(mem) - - def get_memories(self, lo=None, hi=None): - """Get all the memories between @lo and @hi""" - pass - - def set_memory(self, memory): - """Set the memory object @memory""" - pass - - def get_mapping_models(self): - """Returns a list of MappingModel objects (or an empty list)""" - if hasattr(self, "get_bank_model"): - # FIXME: Backwards compatibility for old bank models - bank_model = self.get_bank_model() - if bank_model: - return [bank_model] - return [] - - def get_raw_memory(self, number): - """Return a raw string describing the memory at @number""" - pass - - def filter_name(self, name): - """Filter @name to just the length and characters supported""" - rf = self.get_features() - if rf.valid_characters == rf.valid_characters.upper(): - # Radio only supports uppercase, so help out here - name = name.upper() - return "".join([x for x in name[:rf.valid_name_length] - if x in rf.valid_characters]) - - def get_sub_devices(self): - """Return a list of sub-device Radio objects, if - RadioFeatures.has_sub_devices is True""" - return [] - - def validate_memory(self, mem): - """Return a list of warnings and errors that will be encoundered - if trying to set @mem on the current radio""" - rf = self.get_features() - return rf.validate_memory(mem) - - def get_settings(self): - """Returns a RadioSettings list containing one or more - RadioSettingGroup or RadioSetting objects. These represent general - setting knobs and dials that can be adjusted on the radio. If this - function is implemented, the has_settings RadioFeatures flag should - be True and set_settings() must be implemented as well.""" - pass - - def set_settings(self, settings): - """Accepts the top-level RadioSettingGroup returned from get_settings() - and adjusts the values in the radio accordingly. This function expects - the entire RadioSettingGroup hierarchy returned from get_settings(). - If this function is implemented, the has_settings RadioFeatures flag - should be True and get_settings() must be implemented as well.""" - pass - - -class FileBackedRadio(Radio): - """A file-backed radio stores its data in a file""" - FILE_EXTENSION = "img" - - def __init__(self, *args, **kwargs): - Radio.__init__(self, *args, **kwargs) - self._memobj = None - - def save(self, filename): - """Save the radio's memory map to @filename""" - self.save_mmap(filename) - - def load(self, filename): - """Load the radio's memory map object from @filename""" - self.load_mmap(filename) - - def process_mmap(self): - """Process a newly-loaded or downloaded memory map""" - pass - - def load_mmap(self, filename): - """Load the radio's memory map from @filename""" - mapfile = file(filename, "rb") - self._mmap = memmap.MemoryMap(mapfile.read()) - mapfile.close() - self.process_mmap() - - def save_mmap(self, filename): - """ - try to open a file and write to it - If IOError raise a File Access Error Exception - """ - try: - mapfile = file(filename, "wb") - mapfile.write(self._mmap.get_packed()) - mapfile.close() - except IOError: - raise Exception("File Access Error") - - def get_mmap(self): - """Return the radio's memory map object""" - return self._mmap - - -class CloneModeRadio(FileBackedRadio): - """A clone-mode radio does a full memory dump in and out and we store - an image of the radio into an image file""" - - _memsize = 0 - - def __init__(self, pipe): - self.errors = [] - self._mmap = None - - if isinstance(pipe, str): - self.pipe = None - self.load_mmap(pipe) - elif isinstance(pipe, memmap.MemoryMap): - self.pipe = None - self._mmap = pipe - self.process_mmap() - else: - FileBackedRadio.__init__(self, pipe) - - def get_memsize(self): - """Return the radio's memory size""" - return self._memsize - - @classmethod - def match_model(cls, filedata, filename): - """Given contents of a stored file (@filedata), return True if - this radio driver handles the represented model""" - - # Unless the radio driver does something smarter, claim - # support if the data is the same size as our memory. - # Ideally, each radio would perform an intelligent analysis to - # make this determination to avoid model conflicts with - # memories of the same size. - return len(filedata) == cls._memsize - - def sync_in(self): - "Initiate a radio-to-PC clone operation" - pass - - def sync_out(self): - "Initiate a PC-to-radio clone operation" - pass - - -class LiveRadio(Radio): - """Base class for all Live-Mode radios""" - pass - - -class NetworkSourceRadio(Radio): - """Base class for all radios based on a network source""" - - def do_fetch(self): - """Fetch the source data from the network""" - pass - - -class IcomDstarSupport: - """Base interface for radios supporting Icom's D-STAR technology""" - MYCALL_LIMIT = (1, 1) - URCALL_LIMIT = (1, 1) - RPTCALL_LIMIT = (1, 1) - - def get_urcall_list(self): - """Return a list of URCALL callsigns""" - return [] - - def get_repeater_call_list(self): - """Return a list of RPTCALL callsigns""" - return [] - - def get_mycall_list(self): - """Return a list of MYCALL callsigns""" - return [] - - def set_urcall_list(self, calls): - """Set the URCALL callsign list""" - pass - - def set_repeater_call_list(self, calls): - """Set the RPTCALL callsign list""" - pass - - def set_mycall_list(self, calls): - """Set the MYCALL callsign list""" - pass - - -class ExperimentalRadio: - """Interface for experimental radios""" - @classmethod - def get_experimental_warning(cls): - return ("This radio's driver is marked as experimental and may " + - "be unstable or unsafe to use.") - - -class Status: - """Clone status object for conveying clone progress to the UI""" - name = "Job" - msg = "Unknown" - max = 100 - cur = 0 - - def __str__(self): - try: - pct = (self.cur / float(self.max)) * 100 - nticks = int(pct) / 10 - ticks = "=" * nticks - except ValueError: - pct = 0.0 - ticks = "?" * 10 - - return "|%-10s| %2.1f%% %s" % (ticks, pct, self.msg) - - -def is_fractional_step(freq): - """Returns True if @freq requires a 12.5kHz or 6.25kHz step""" - return not is_5_0(freq) and (is_12_5(freq) or is_6_25(freq)) - - -def is_5_0(freq): - """Returns True if @freq is reachable by a 5kHz step""" - return (freq % 5000) == 0 - - -def is_12_5(freq): - """Returns True if @freq is reachable by a 12.5kHz step""" - return (freq % 12500) == 0 - - -def is_6_25(freq): - """Returns True if @freq is reachable by a 6.25kHz step""" - return (freq % 6250) == 0 - - -def is_2_5(freq): - """Returns True if @freq is reachable by a 2.5kHz step""" - return (freq % 2500) == 0 - - -def required_step(freq): - """Returns the simplest tuning step that is required to reach @freq""" - if is_5_0(freq): - return 5.0 - elif is_12_5(freq): - return 12.5 - elif is_6_25(freq): - return 6.25 - elif is_2_5(freq): - return 2.5 - else: - raise errors.InvalidDataError("Unable to calculate the required " + - "tuning step for %i.%5i" % - (freq / 1000000, freq % 1000000)) - - -def fix_rounded_step(freq): - """Some radios imply the last bit of 12.5kHz and 6.25kHz step - frequencies. Take the base @freq and return the corrected one""" - try: - required_step(freq) - return freq - except errors.InvalidDataError: - pass - - try: - required_step(freq + 500) - return freq + 500 - except errors.InvalidDataError: - pass - - try: - required_step(freq + 250) - return freq + 250 - except errors.InvalidDataError: - pass - - try: - required_step(freq + 750) - return float(freq + 750) - except errors.InvalidDataError: - pass - - raise errors.InvalidDataError("Unable to correct rounded frequency " + - format_freq(freq)) - - -def _name(name, len, just_upper): - """Justify @name to @len, optionally converting to all uppercase""" - if just_upper: - name = name.upper() - return name.ljust(len)[:len] - - -def name6(name, just_upper=True): - """6-char name""" - return _name(name, 6, just_upper) - - -def name8(name, just_upper=False): - """8-char name""" - return _name(name, 8, just_upper) - - -def name16(name, just_upper=False): - """16-char name""" - return _name(name, 16, just_upper) - - -def to_GHz(val): - """Convert @val in GHz to Hz""" - return val * 1000000000 - - -def to_MHz(val): - """Convert @val in MHz to Hz""" - return val * 1000000 - - -def to_kHz(val): - """Convert @val in kHz to Hz""" - return val * 1000 - - -def from_GHz(val): - """Convert @val in Hz to GHz""" - return val / 100000000 - - -def from_MHz(val): - """Convert @val in Hz to MHz""" - return val / 100000 - - -def from_kHz(val): - """Convert @val in Hz to kHz""" - return val / 100 - - -def split_tone_decode(mem, txtone, rxtone): - """ - Set tone mode and values on @mem based on txtone and rxtone specs like: - None, None, None - "Tone", 123.0, None - "DTCS", 23, "N" - """ - txmode, txval, txpol = txtone - rxmode, rxval, rxpol = rxtone - - mem.dtcs_polarity = "%s%s" % (txpol or "N", rxpol or "N") - - if not txmode and not rxmode: - # No tone - return - - if txmode == "Tone" and not rxmode: - mem.tmode = "Tone" - mem.rtone = txval - return - - if txmode == rxmode == "Tone" and txval == rxval: - # TX and RX same tone -> TSQL - mem.tmode = "TSQL" - mem.ctone = txval - return - - if txmode == rxmode == "DTCS" and txval == rxval: - mem.tmode = "DTCS" - mem.dtcs = txval - return - - mem.tmode = "Cross" - mem.cross_mode = "%s->%s" % (txmode or "", rxmode or "") - - if txmode == "Tone": - mem.rtone = txval - elif txmode == "DTCS": - mem.dtcs = txval - - if rxmode == "Tone": - mem.ctone = rxval - elif rxmode == "DTCS": - mem.rx_dtcs = rxval - - -def split_tone_encode(mem): - """ - Returns TX, RX tone specs based on @mem like: - None, None, None - "Tone", 123.0, None - "DTCS", 23, "N" - """ - - txmode = '' - rxmode = '' - txval = None - rxval = None - - if mem.tmode == "Tone": - txmode = "Tone" - txval = mem.rtone - elif mem.tmode == "TSQL": - txmode = rxmode = "Tone" - txval = rxval = mem.ctone - elif mem.tmode == "DTCS": - txmode = rxmode = "DTCS" - txval = rxval = mem.dtcs - elif mem.tmode == "Cross": - txmode, rxmode = mem.cross_mode.split("->", 1) - if txmode == "Tone": - txval = mem.rtone - elif txmode == "DTCS": - txval = mem.dtcs - if rxmode == "Tone": - rxval = mem.ctone - elif rxmode == "DTCS": - rxval = mem.rx_dtcs - - if txmode == "DTCS": - txpol = mem.dtcs_polarity[0] - else: - txpol = None - if rxmode == "DTCS": - rxpol = mem.dtcs_polarity[1] - else: - rxpol = None - - return ((txmode, txval, txpol), - (rxmode, rxval, rxpol)) - - -def sanitize_string(astring, validcharset=CHARSET_ASCII, replacechar='*'): - myfilter = ''.join( - [ - [replacechar, chr(x)][chr(x) in validcharset] - for x in xrange(256) - ]) - return astring.translate(myfilter) +# Copyright 2008 Dan Smith +# +# 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 . + +import math +from chirp import errors, memmap + +SEPCHAR = "," + +# 50 Tones +TONES = [67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, + 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, + 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, + 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, + 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, + 177.3, 179.9, 183.5, 186.2, 189.9, 192.8, + 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, + 225.7, 229.1, 233.6, 241.8, 250.3, 254.1, + ] + +TONES_EXTRA = [56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, + 62.5, 63.0, 64.0] + +OLD_TONES = list(TONES) +[OLD_TONES.remove(x) for x in [159.8, 165.5, 171.3, 177.3, 183.5, 189.9, + 196.6, 199.5, 206.5, 229.1, 254.1]] + +# 104 DTCS Codes +DTCS_CODES = [ + 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54, + 65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131, + 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, + 205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252, + 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, + 331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, + 413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464, + 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, + 612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723, + 731, 732, 734, 743, 754, +] + +# 512 Possible DTCS Codes +ALL_DTCS_CODES = [] +for a in range(0, 8): + for b in range(0, 8): + for c in range(0, 8): + ALL_DTCS_CODES.append((a * 100) + (b * 10) + c) + +CROSS_MODES = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS", + "Tone->" +] + +MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY", + "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR", + "FSK", "FSKR", "DMR"] + +TONE_MODES = [ + "", + "Tone", + "TSQL", + "DTCS", + "DTCS-R", + "TSQL-R", + "Cross", +] + +TUNING_STEPS = [ + 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0, 100.0, + 125.0, 200.0, + # Need to fix drivers using this list as an index! + 9.0, 1.0, 2.5, +] + +SKIP_VALUES = ["", "S", "P"] + +CHARSET_UPPER_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890" +CHARSET_ALPHANUMERIC = \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 1234567890" +CHARSET_ASCII = "".join([chr(x) for x in range(ord(" "), ord("~") + 1)]) + +# http://aprs.org/aprs11/SSIDs.txt +APRS_SSID = ( + "0 Your primary station usually fixed and message capable", + "1 generic additional station, digi, mobile, wx, etc", + "2 generic additional station, digi, mobile, wx, etc", + "3 generic additional station, digi, mobile, wx, etc", + "4 generic additional station, digi, mobile, wx, etc", + "5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)", + "6 Special activity, Satellite ops, camping or 6 meters, etc", + "7 walkie talkies, HT's or other human portable", + "8 boats, sailboats, RV's or second main mobile", + "9 Primary Mobile (usually message capable)", + "10 internet, Igates, echolink, winlink, AVRS, APRN, etc", + "11 balloons, aircraft, spacecraft, etc", + "12 APRStt, DTMF, RFID, devices, one-way trackers*, etc", + "13 Weather stations", + "14 Truckers or generally full time drivers", + "15 generic additional station, digi, mobile, wx, etc") +APRS_POSITION_COMMENT = ( + "off duty", "en route", "in service", "returning", "committed", + "special", "priority", "custom 0", "custom 1", "custom 2", "custom 3", + "custom 4", "custom 5", "custom 6", "EMERGENCY") +# http://aprs.org/symbols/symbolsX.txt +APRS_SYMBOLS = ( + "Police/Sheriff", "[reserved]", "Digi", "Phone", "DX Cluster", + "HF Gateway", "Small Aircraft", "Mobile Satellite Groundstation", + "Wheelchair", "Snowmobile", "Red Cross", "Boy Scouts", "House QTH (VHF)", + "X", "Red Dot", "0 in Circle", "1 in Circle", "2 in Circle", + "3 in Circle", "4 in Circle", "5 in Circle", "6 in Circle", "7 in Circle", + "8 in Circle", "9 in Circle", "Fire", "Campground", "Motorcycle", + "Railroad Engine", "Car", "File Server", "Hurricane Future Prediction", + "Aid Station", "BBS or PBBS", "Canoe", "[reserved]", "Eyeball", + "Tractor/Farm Vehicle", "Grid Square", "Hotel", "TCP/IP", "[reserved]", + "School", "PC User", "MacAPRS", "NTS Station", "Balloon", "Police", "TBD", + "Recreational Vehicle", "Space Shuttle", "SSTV", "Bus", "ATV", + "National WX Service Site", "Helicopter", "Yacht/Sail Boat", "WinAPRS", + "Human/Person", "Triangle", "Mail/Postoffice", "Large Aircraft", + "WX Station", "Dish Antenna", "Ambulance", "Bicycle", + "Incident Command Post", "Dual Garage/Fire Dept", "Horse/Equestrian", + "Fire Truck", "Glider", "Hospital", "IOTA", "Jeep", "Truck", "Laptop", + "Mic-Repeater", "Node", "Emergency Operations Center", "Rover (dog)", + "Grid Square above 128m", "Repeater", "Ship/Power Boat", "Truck Stop", + "Truck (18 wheeler)", "Van", "Water Station", "X-APRS", "Yagi at QTH", + "TDB", "[reserved]" +) + + +def watts_to_dBm(watts): + """Converts @watts in watts to dBm""" + return int(10 * math.log10(int(watts * 1000))) + + +def dBm_to_watts(dBm): + """Converts @dBm from dBm to watts""" + return int(math.pow(10, (dBm - 30) / 10)) + + +class PowerLevel: + """Represents a power level supported by a radio""" + + def __init__(self, label, watts=0, dBm=0): + if watts: + dBm = watts_to_dBm(watts) + self._power = int(dBm) + self._label = label + + def __str__(self): + return str(self._label) + + def __int__(self): + return self._power + + def __sub__(self, val): + return int(self) - int(val) + + def __add__(self, val): + return int(self) + int(val) + + def __eq__(self, val): + if val is not None: + return int(self) == int(val) + return False + + def __lt__(self, val): + return int(self) < int(val) + + def __gt__(self, val): + return int(self) > int(val) + + def __nonzero__(self): + return int(self) != 0 + + def __repr__(self): + return "%s (%i dBm)" % (self._label, self._power) + + +def parse_freq(freqstr): + """Parse a frequency string and return the value in integral Hz""" + freqstr = freqstr.strip() + if freqstr == "": + return 0 + elif freqstr.endswith(" MHz"): + return parse_freq(freqstr.split(" ")[0]) + elif freqstr.endswith(" kHz"): + return int(freqstr.split(" ")[0]) * 1000 + + if "." in freqstr: + mhz, khz = freqstr.split(".") + if mhz == "": + mhz = 0 + khz = khz.ljust(6, "0") + if len(khz) > 6: + raise ValueError("Invalid kHz value: %s", khz) + mhz = int(mhz) * 1000000 + khz = int(khz) + else: + mhz = int(freqstr) * 1000000 + khz = 0 + + return mhz + khz + + +def format_freq(freq): + """Format a frequency given in Hz as a string""" + + return "%i.%06i" % (freq / 1000000, freq % 1000000) + + +class ImmutableValueError(ValueError): + pass + + +class Memory: + """Base class for a single radio memory""" + freq = 0 + number = 0 + extd_number = "" + name = "" + vfo = 0 + rtone = 88.5 + ctone = 88.5 + dtcs = 23 + rx_dtcs = 23 + tmode = "" + cross_mode = "Tone->Tone" + dtcs_polarity = "NN" + skip = "" + power = None + duplex = "" + offset = 600000 + mode = "FM" + tuning_step = 5.0 + + comment = "" + + empty = False + + immutable = [] + + # A RadioSettingGroup of additional settings supported by the radio, + # or an empty list if none + extra = [] + + def __init__(self): + self.freq = 0 + self.number = 0 + self.extd_number = "" + self.name = "" + self.vfo = 0 + self.rtone = 88.5 + self.ctone = 88.5 + self.dtcs = 23 + self.rx_dtcs = 23 + self.tmode = "" + self.cross_mode = "Tone->Tone" + self.dtcs_polarity = "NN" + self.skip = "" + self.power = None + self.duplex = "" + self.offset = 600000 + self.mode = "FM" + self.tuning_step = 5.0 + + self.comment = "" + + self.empty = False + + self.immutable = [] + + _valid_map = { + "rtone": TONES + TONES_EXTRA, + "ctone": TONES + TONES_EXTRA, + "dtcs": ALL_DTCS_CODES, + "rx_dtcs": ALL_DTCS_CODES, + "tmode": TONE_MODES, + "dtcs_polarity": ["NN", "NR", "RN", "RR"], + "cross_mode": CROSS_MODES, + "mode": MODES, + "duplex": ["", "+", "-", "split", "off"], + "skip": SKIP_VALUES, + "empty": [True, False], + "dv_code": [x for x in range(0, 100)], + } + + def __repr__(self): + return "Memory[%i]" % self.number + + def dupe(self): + """Return a deep copy of @self""" + mem = self.__class__() + for k, v in self.__dict__.items(): + mem.__dict__[k] = v + + return mem + + def clone(self, source): + """Absorb all of the properties of @source""" + for k, v in source.__dict__.items(): + self.__dict__[k] = v + + CSV_FORMAT = ["Location", "Name", "Frequency", + "Duplex", "Offset", "Tone", + "rToneFreq", "cToneFreq", "DtcsCode", + "DtcsPolarity", "Mode", "TStep", + "Skip", "Comment", + "URCALL", "RPT1CALL", "RPT2CALL", "DVCODE"] + + def __setattr__(self, name, val): + if not hasattr(self, name): + raise ValueError("No such attribute `%s'" % name) + + if name in self.immutable: + raise ImmutableValueError("Field %s is not " % name + + "mutable on this memory") + + if name in self._valid_map and val not in self._valid_map[name]: + raise ValueError("`%s' is not in valid list: %s" % + (val, self._valid_map[name])) + + self.__dict__[name] = val + + def format_freq(self): + """Return a properly-formatted string of this memory's frequency""" + return format_freq(self.freq) + + def parse_freq(self, freqstr): + """Set the frequency from a string""" + self.freq = parse_freq(freqstr) + return self.freq + + def __str__(self): + if self.tmode == "Tone": + tenc = "*" + else: + tenc = " " + + if self.tmode == "TSQL": + tsql = "*" + else: + tsql = " " + + if self.tmode == "DTCS": + dtcs = "*" + else: + dtcs = " " + + if self.duplex == "": + dup = "/" + else: + dup = self.duplex + + return \ + "Memory %s: %s%s%s %s (%s) r%.1f%s c%.1f%s d%03i%s%s [%.2f]" % \ + (self.number if self.extd_number == "" else self.extd_number, + format_freq(self.freq), + dup, + format_freq(self.offset), + self.mode, + self.name, + self.rtone, + tenc, + self.ctone, + tsql, + self.dtcs, + dtcs, + self.dtcs_polarity, + self.tuning_step) + + def to_csv(self): + """Return a CSV representation of this memory""" + return [ + "%i" % self.number, + "%s" % self.name, + format_freq(self.freq), + "%s" % self.duplex, + format_freq(self.offset), + "%s" % self.tmode, + "%.1f" % self.rtone, + "%.1f" % self.ctone, + "%03i" % self.dtcs, + "%s" % self.dtcs_polarity, + "%s" % self.mode, + "%.2f" % self.tuning_step, + "%s" % self.skip, + "%s" % self.comment, + "", "", "", ""] + + @classmethod + def _from_csv(cls, _line): + line = _line.strip() + if line.startswith("Location"): + raise errors.InvalidMemoryLocation("Non-CSV line") + + vals = line.split(SEPCHAR) + if len(vals) < 11: + raise errors.InvalidDataError("CSV format error " + + "(14 columns expected)") + + if vals[10] == "DV": + mem = DVMemory() + else: + mem = Memory() + + mem.really_from_csv(vals) + return mem + + def really_from_csv(self, vals): + """Careful parsing of split-out @vals""" + try: + self.number = int(vals[0]) + except: + raise errors.InvalidDataError( + "Location '%s' is not a valid integer" % vals[0]) + + self.name = vals[1] + + try: + self.freq = float(vals[2]) + except: + raise errors.InvalidDataError("Frequency is not a valid number") + + if vals[3].strip() in ["+", "-", ""]: + self.duplex = vals[3].strip() + else: + raise errors.InvalidDataError("Duplex is not +,-, or empty") + + try: + self.offset = float(vals[4]) + except: + raise errors.InvalidDataError("Offset is not a valid number") + + self.tmode = vals[5] + if self.tmode not in TONE_MODES: + raise errors.InvalidDataError("Invalid tone mode `%s'" % + self.tmode) + + try: + self.rtone = float(vals[6]) + except: + raise errors.InvalidDataError("rTone is not a valid number") + if self.rtone not in TONES: + raise errors.InvalidDataError("rTone is not valid") + + try: + self.ctone = float(vals[7]) + except: + raise errors.InvalidDataError("cTone is not a valid number") + if self.ctone not in TONES: + raise errors.InvalidDataError("cTone is not valid") + + try: + self.dtcs = int(vals[8], 10) + except: + raise errors.InvalidDataError("DTCS code is not a valid number") + if self.dtcs not in DTCS_CODES: + raise errors.InvalidDataError("DTCS code is not valid") + + try: + self.rx_dtcs = int(vals[8], 10) + except: + raise errors.InvalidDataError("DTCS Rx code is not a valid number") + if self.rx_dtcs not in DTCS_CODES: + raise errors.InvalidDataError("DTCS Rx code is not valid") + + if vals[9] in ["NN", "NR", "RN", "RR"]: + self.dtcs_polarity = vals[9] + else: + raise errors.InvalidDataError("DtcsPolarity is not valid") + + if vals[10] in MODES: + self.mode = vals[10] + else: + raise errors.InvalidDataError("Mode is not valid") + + try: + self.tuning_step = float(vals[11]) + except: + raise errors.InvalidDataError("Tuning step is invalid") + + try: + self.skip = vals[12] + except: + raise errors.InvalidDataError("Skip value is not valid") + + return True + + +class DVMemory(Memory): + """A Memory with D-STAR attributes""" + dv_urcall = "CQCQCQ" + dv_rpt1call = "" + dv_rpt2call = "" + dv_code = 0 + + def __str__(self): + string = Memory.__str__(self) + + string += " <%s,%s,%s>" % (self.dv_urcall, + self.dv_rpt1call, + self.dv_rpt2call) + + return string + + def to_csv(self): + return [ + "%i" % self.number, + "%s" % self.name, + format_freq(self.freq), + "%s" % self.duplex, + format_freq(self.offset), + "%s" % self.tmode, + "%.1f" % self.rtone, + "%.1f" % self.ctone, + "%03i" % self.dtcs, + "%s" % self.dtcs_polarity, + "%s" % self.mode, + "%.2f" % self.tuning_step, + "%s" % self.skip, + "%s" % self.comment, + "%s" % self.dv_urcall, + "%s" % self.dv_rpt1call, + "%s" % self.dv_rpt2call, + "%i" % self.dv_code] + + def really_from_csv(self, vals): + Memory.really_from_csv(self, vals) + + self.dv_urcall = vals[15].rstrip()[:8] + self.dv_rpt1call = vals[16].rstrip()[:8] + self.dv_rpt2call = vals[17].rstrip()[:8] + try: + self.dv_code = int(vals[18].strip()) + except Exception: + self.dv_code = 0 + + +class MemoryMapping(object): + """Base class for a memory mapping""" + + def __init__(self, model, index, name): + self._model = model + self._index = index + self._name = name + + def __str__(self): + return self.get_name() + + def __repr__(self): + return "%s-%s" % (self.__class__.__name__, self._index) + + def get_name(self): + """Returns the mapping name""" + return self._name + + def get_index(self): + """Returns the immutable index (string or int)""" + return self._index + + def __eq__(self, other): + return self.get_index() == other.get_index() + + +class MappingModel(object): + """Base class for a memory mapping model""" + + def __init__(self, radio, name): + self._radio = radio + self._name = name + + def get_name(self): + return self._name + + def get_num_mappings(self): + """Returns the number of mappings in the model (should be + callable without consulting the radio""" + raise NotImplementedError() + + def get_mappings(self): + """Return a list of mappings""" + raise NotImplementedError() + + def add_memory_to_mapping(self, memory, mapping): + """Add @memory to @mapping.""" + raise NotImplementedError() + + def remove_memory_from_mapping(self, memory, mapping): + """Remove @memory from @mapping. + Shall raise exception if @memory is not in @bank""" + raise NotImplementedError() + + def get_mapping_memories(self, mapping): + """Return a list of memories in @mapping""" + raise NotImplementedError() + + def get_memory_mappings(self, memory): + """Return a list of mappings that @memory is in""" + raise NotImplementedError() + + +class Bank(MemoryMapping): + """Base class for a radio's Bank""" + + +class NamedBank(Bank): + """A bank that can have a name""" + + def set_name(self, name): + """Changes the user-adjustable bank name""" + self._name = name + + +class BankModel(MappingModel): + """A bank model where one memory is in zero or one banks at any point""" + + def __init__(self, radio, name='Banks'): + super(BankModel, self).__init__(radio, name) + + +class MappingModelIndexInterface: + """Interface for mappings with index capabilities""" + + def get_index_bounds(self): + """Returns a tuple (lo,hi) of the min and max mapping indices""" + raise NotImplementedError() + + def get_memory_index(self, memory, mapping): + """Returns the index of @memory in @mapping""" + raise NotImplementedError() + + def set_memory_index(self, memory, mapping, index): + """Sets the index of @memory in @mapping to @index""" + raise NotImplementedError() + + def get_next_mapping_index(self, mapping): + """Returns the next available mapping index in @mapping, or raises + Exception if full""" + raise NotImplementedError() + + +class MTOBankModel(BankModel): + """A bank model where one memory can be in multiple banks at once """ + pass + + +def console_status(status): + """Write a status object to the console""" + import logging + from chirp import logger + if not logger.is_visible(logging.WARN): + return + import sys + import os + sys.stdout.write("\r%s" % status) + if status.cur == status.max: + sys.stdout.write(os.linesep) + + +class RadioPrompts: + """Radio prompt strings""" + info = None + display_info = True + experimental = None + pre_download = None + pre_upload = None + display_pre_upload_prompt_before_opening_port = True + + +BOOLEAN = [True, False] + + +class RadioFeatures: + """Radio Feature Flags""" + _valid_map = { + # General + "has_bank_index": BOOLEAN, + "has_dtcs": BOOLEAN, + "has_rx_dtcs": BOOLEAN, + "has_dtcs_polarity": BOOLEAN, + "has_mode": BOOLEAN, + "has_offset": BOOLEAN, + "has_name": BOOLEAN, + "has_bank": BOOLEAN, + "has_bank_names": BOOLEAN, + "has_tuning_step": BOOLEAN, + "has_ctone": BOOLEAN, + "has_cross": BOOLEAN, + "has_infinite_number": BOOLEAN, + "has_nostep_tuning": BOOLEAN, + "has_comment": BOOLEAN, + "has_settings": BOOLEAN, + + # Attributes + "valid_modes": [], + "valid_tmodes": [], + "valid_duplexes": [], + "valid_tuning_steps": [], + "valid_bands": [], + "valid_skips": [], + "valid_power_levels": [], + "valid_characters": "", + "valid_name_length": 0, + "valid_cross_modes": [], + "valid_dtcs_pols": [], + "valid_dtcs_codes": [], + "valid_special_chans": [], + + "has_sub_devices": BOOLEAN, + "memory_bounds": (0, 0), + "can_odd_split": BOOLEAN, + + # D-STAR + "requires_call_lists": BOOLEAN, + "has_implicit_calls": BOOLEAN, + } + + def __setattr__(self, name, val): + if name.startswith("_"): + self.__dict__[name] = val + return + elif name not in self._valid_map.keys(): + raise ValueError("No such attribute `%s'" % name) + + if type(self._valid_map[name]) == tuple: + # Tuple, cardinality must match + if type(val) != tuple or len(val) != len(self._valid_map[name]): + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif type(self._valid_map[name]) == list and not self._valid_map[name]: + # Empty list, must be another list + if type(val) != list: + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif type(self._valid_map[name]) == str: + if type(val) != str: + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif type(self._valid_map[name]) == int: + if type(val) != int: + raise ValueError("Invalid value `%s' for attribute `%s'" % + (val, name)) + elif val not in self._valid_map[name]: + # Value not in the list of valid values + raise ValueError("Invalid value `%s' for attribute `%s'" % (val, + name)) + self.__dict__[name] = val + + def __getattr__(self, name): + raise AttributeError("pylint is confused by RadioFeatures") + + def init(self, attribute, default, doc=None): + """Initialize a feature flag @attribute with default value @default, + and documentation string @doc""" + self.__setattr__(attribute, default) + self.__docs[attribute] = doc + + def get_doc(self, attribute): + """Return the description of @attribute""" + return self.__docs[attribute] + + def __init__(self): + self.__docs = {} + self.init("has_bank_index", False, + "Indicates that memories in a bank can be stored in " + + "an order other than in main memory") + self.init("has_dtcs", True, + "Indicates that DTCS tone mode is available") + self.init("has_rx_dtcs", False, + "Indicates that radio can use two different " + + "DTCS codes for rx and tx") + self.init("has_dtcs_polarity", True, + "Indicates that the DTCS polarity can be changed") + self.init("has_mode", True, + "Indicates that multiple emission modes are supported") + self.init("has_offset", True, + "Indicates that the TX offset memory property is supported") + self.init("has_name", True, + "Indicates that an alphanumeric memory name is supported") + self.init("has_bank", True, + "Indicates that memories may be placed into banks") + self.init("has_bank_names", False, + "Indicates that banks may be named") + self.init("has_tuning_step", True, + "Indicates that memories store their tuning step") + self.init("has_ctone", True, + "Indicates that the radio keeps separate tone frequencies " + + "for repeater and CTCSS operation") + self.init("has_cross", False, + "Indicates that the radios supports different tone modes " + + "on transmit and receive") + self.init("has_infinite_number", False, + "Indicates that the radio is not constrained in the " + + "number of memories that it can store") + self.init("has_nostep_tuning", False, + "Indicates that the radio does not require a valid " + + "tuning step to store a frequency") + self.init("has_comment", False, + "Indicates that the radio supports storing a comment " + + "with each memory") + self.init("has_settings", False, + "Indicates that the radio supports general settings") + + self.init("valid_modes", list(MODES), + "Supported emission (or receive) modes") + self.init("valid_tmodes", [], + "Supported tone squelch modes") + self.init("valid_duplexes", ["", "+", "-"], + "Supported duplex modes") + self.init("valid_tuning_steps", list(TUNING_STEPS), + "Supported tuning steps") + self.init("valid_bands", [], + "Supported frequency ranges") + self.init("valid_skips", ["", "S"], + "Supported memory scan skip settings") + self.init("valid_power_levels", [], + "Supported power levels") + self.init("valid_characters", CHARSET_UPPER_NUMERIC, + "Supported characters for a memory's alphanumeric tag") + self.init("valid_name_length", 6, + "The maximum number of characters in a memory's " + + "alphanumeric tag") + self.init("valid_cross_modes", list(CROSS_MODES), + "Supported tone cross modes") + self.init("valid_dtcs_pols", ["NN", "RN", "NR", "RR"], + "Supported DTCS polarities") + self.init("valid_dtcs_codes", list(DTCS_CODES), + "Supported DTCS codes") + self.init("valid_special_chans", [], + "Supported special channel names") + + self.init("has_sub_devices", False, + "Indicates that the radio behaves as two semi-independent " + + "devices") + self.init("memory_bounds", (0, 1), + "The minimum and maximum channel numbers") + self.init("can_odd_split", False, + "Indicates that the radio can store an independent " + + "transmit frequency") + + self.init("requires_call_lists", True, + "[D-STAR] Indicates that the radio requires all callsigns " + + "to be in the master list and cannot be stored " + + "arbitrarily in each memory channel") + self.init("has_implicit_calls", False, + "[D-STAR] Indicates that the radio has an implied " + + "callsign at the beginning of the master URCALL list") + + def is_a_feature(self, name): + """Returns True if @name is a valid feature flag name""" + return name in self._valid_map.keys() + + def __getitem__(self, name): + return self.__dict__[name] + + def validate_memory(self, mem): + """Return a list of warnings and errors that will be encoundered + if trying to set @mem on the current radio""" + msgs = [] + + lo, hi = self.memory_bounds + if not self.has_infinite_number and \ + (mem.number < lo or mem.number > hi) and \ + mem.extd_number not in self.valid_special_chans: + msg = ValidationWarning("Location %i is out of range" % mem.number) + msgs.append(msg) + + if (self.valid_modes and + mem.mode not in self.valid_modes and + mem.mode != "Auto"): + msg = ValidationError("Mode %s not supported" % mem.mode) + msgs.append(msg) + + if self.valid_tmodes and mem.tmode not in self.valid_tmodes: + msg = ValidationError("Tone mode %s not supported" % mem.tmode) + msgs.append(msg) + else: + if mem.tmode == "Cross": + if self.valid_cross_modes and \ + mem.cross_mode not in self.valid_cross_modes: + msg = ValidationError("Cross tone mode %s not supported" % + mem.cross_mode) + msgs.append(msg) + + if self.has_dtcs_polarity and \ + mem.dtcs_polarity not in self.valid_dtcs_pols: + msg = ValidationError("DTCS Polarity %s not supported" % + mem.dtcs_polarity) + msgs.append(msg) + + if self.valid_dtcs_codes and \ + mem.dtcs not in self.valid_dtcs_codes: + msg = ValidationError("DTCS Code %03i not supported" % mem.dtcs) + if self.valid_dtcs_codes and \ + mem.rx_dtcs not in self.valid_dtcs_codes: + msg = ValidationError("DTCS Code %03i not supported" % mem.rx_dtcs) + + if self.valid_duplexes and mem.duplex not in self.valid_duplexes: + msg = ValidationError("Duplex %s not supported" % mem.duplex) + msgs.append(msg) + + ts = mem.tuning_step + if self.valid_tuning_steps and ts not in self.valid_tuning_steps and \ + not self.has_nostep_tuning: + msg = ValidationError("Tuning step %.2f not supported" % ts) + msgs.append(msg) + + if self.valid_bands: + valid = False + for lo, hi in self.valid_bands: + if lo <= mem.freq < hi: + valid = True + break + if not valid: + msg = ValidationError( + ("Frequency {freq} is out " + "of supported range").format(freq=format_freq(mem.freq))) + msgs.append(msg) + + if self.valid_bands and \ + self.valid_duplexes and \ + mem.duplex in ["split", "-", "+"]: + if mem.duplex == "split": + freq = mem.offset + elif mem.duplex == "-": + freq = mem.freq - mem.offset + elif mem.duplex == "+": + freq = mem.freq + mem.offset + valid = False + for lo, hi in self.valid_bands: + if lo <= freq < hi: + valid = True + break + if not valid: + msg = ValidationError( + ("Tx freq {freq} is out " + "of supported range").format(freq=format_freq(freq))) + msgs.append(msg) + + if mem.power and \ + self.valid_power_levels and \ + mem.power not in self.valid_power_levels: + msg = ValidationWarning("Power level %s not supported" % mem.power) + msgs.append(msg) + + if self.valid_tuning_steps and not self.has_nostep_tuning: + try: + step = required_step(mem.freq) + if step not in self.valid_tuning_steps: + msg = ValidationError("Frequency requires %.2fkHz step" % + required_step(mem.freq)) + msgs.append(msg) + except errors.InvalidDataError, e: + msgs.append(str(e)) + + if self.valid_characters: + for char in mem.name: + if char not in self.valid_characters: + msgs.append(ValidationWarning("Name character " + + "`%s'" % char + + " not supported")) + break + + return msgs + + +class ValidationMessage(str): + """Base class for Validation Errors and Warnings""" + pass + + +class ValidationWarning(ValidationMessage): + """A non-fatal warning during memory validation""" + pass + + +class ValidationError(ValidationMessage): + """A fatal error during memory validation""" + pass + + +class Alias(object): + VENDOR = "Unknown" + MODEL = "Unknown" + VARIANT = "" + + +class Radio(Alias): + """Base class for all Radio drivers""" + BAUD_RATE = 9600 + HARDWARE_FLOW = False + ALIASES = [] + + def status_fn(self, status): + """Deliver @status to the UI""" + console_status(status) + + def __init__(self, pipe): + self.errors = [] + self.pipe = pipe + + def get_features(self): + """Return a RadioFeatures object for this radio""" + return RadioFeatures() + + @classmethod + def get_name(cls): + """Return a printable name for this radio""" + return "%s %s" % (cls.VENDOR, cls.MODEL) + + @classmethod + def get_prompts(cls): + """Return a set of strings for use in prompts""" + return RadioPrompts() + + def set_pipe(self, pipe): + """Set the serial object to be used for communications""" + self.pipe = pipe + + def get_memory(self, number): + """Return a Memory object for the memory at location @number""" + pass + + def erase_memory(self, number): + """Erase memory at location @number""" + mem = Memory() + mem.number = number + mem.empty = True + self.set_memory(mem) + + def get_memories(self, lo=None, hi=None): + """Get all the memories between @lo and @hi""" + pass + + def set_memory(self, memory): + """Set the memory object @memory""" + pass + + def get_mapping_models(self): + """Returns a list of MappingModel objects (or an empty list)""" + if hasattr(self, "get_bank_model"): + # FIXME: Backwards compatibility for old bank models + bank_model = self.get_bank_model() + if bank_model: + return [bank_model] + return [] + + def get_raw_memory(self, number): + """Return a raw string describing the memory at @number""" + pass + + def filter_name(self, name): + """Filter @name to just the length and characters supported""" + rf = self.get_features() + if rf.valid_characters == rf.valid_characters.upper(): + # Radio only supports uppercase, so help out here + name = name.upper() + return "".join([x for x in name[:rf.valid_name_length] + if x in rf.valid_characters]) + + def get_sub_devices(self): + """Return a list of sub-device Radio objects, if + RadioFeatures.has_sub_devices is True""" + return [] + + def validate_memory(self, mem): + """Return a list of warnings and errors that will be encoundered + if trying to set @mem on the current radio""" + rf = self.get_features() + return rf.validate_memory(mem) + + def get_settings(self): + """Returns a RadioSettings list containing one or more + RadioSettingGroup or RadioSetting objects. These represent general + setting knobs and dials that can be adjusted on the radio. If this + function is implemented, the has_settings RadioFeatures flag should + be True and set_settings() must be implemented as well.""" + pass + + def set_settings(self, settings): + """Accepts the top-level RadioSettingGroup returned from get_settings() + and adjusts the values in the radio accordingly. This function expects + the entire RadioSettingGroup hierarchy returned from get_settings(). + If this function is implemented, the has_settings RadioFeatures flag + should be True and get_settings() must be implemented as well.""" + pass + + +class FileBackedRadio(Radio): + """A file-backed radio stores its data in a file""" + FILE_EXTENSION = "img" + + def __init__(self, *args, **kwargs): + Radio.__init__(self, *args, **kwargs) + self._memobj = None + + def save(self, filename): + """Save the radio's memory map to @filename""" + self.save_mmap(filename) + + def load(self, filename): + """Load the radio's memory map object from @filename""" + self.load_mmap(filename) + + def process_mmap(self): + """Process a newly-loaded or downloaded memory map""" + pass + + def load_mmap(self, filename): + """Load the radio's memory map from @filename""" + mapfile = file(filename, "rb") + self._mmap = memmap.MemoryMap(mapfile.read()) + mapfile.close() + self.process_mmap() + + def save_mmap(self, filename): + """ + try to open a file and write to it + If IOError raise a File Access Error Exception + """ + try: + mapfile = file(filename, "wb") + mapfile.write(self._mmap.get_packed()) + mapfile.close() + except IOError: + raise Exception("File Access Error") + + def get_mmap(self): + """Return the radio's memory map object""" + return self._mmap + + +class CloneModeRadio(FileBackedRadio): + """A clone-mode radio does a full memory dump in and out and we store + an image of the radio into an image file""" + + _memsize = 0 + + def __init__(self, pipe): + self.errors = [] + self._mmap = None + + if isinstance(pipe, str): + self.pipe = None + self.load_mmap(pipe) + elif isinstance(pipe, memmap.MemoryMap): + self.pipe = None + self._mmap = pipe + self.process_mmap() + else: + FileBackedRadio.__init__(self, pipe) + + def get_memsize(self): + """Return the radio's memory size""" + return self._memsize + + @classmethod + def match_model(cls, filedata, filename): + """Given contents of a stored file (@filedata), return True if + this radio driver handles the represented model""" + + # Unless the radio driver does something smarter, claim + # support if the data is the same size as our memory. + # Ideally, each radio would perform an intelligent analysis to + # make this determination to avoid model conflicts with + # memories of the same size. + return len(filedata) == cls._memsize + + def sync_in(self): + "Initiate a radio-to-PC clone operation" + pass + + def sync_out(self): + "Initiate a PC-to-radio clone operation" + pass + + +class LiveRadio(Radio): + """Base class for all Live-Mode radios""" + pass + + +class NetworkSourceRadio(Radio): + """Base class for all radios based on a network source""" + + def do_fetch(self): + """Fetch the source data from the network""" + pass + + +class IcomDstarSupport: + """Base interface for radios supporting Icom's D-STAR technology""" + MYCALL_LIMIT = (1, 1) + URCALL_LIMIT = (1, 1) + RPTCALL_LIMIT = (1, 1) + + def get_urcall_list(self): + """Return a list of URCALL callsigns""" + return [] + + def get_repeater_call_list(self): + """Return a list of RPTCALL callsigns""" + return [] + + def get_mycall_list(self): + """Return a list of MYCALL callsigns""" + return [] + + def set_urcall_list(self, calls): + """Set the URCALL callsign list""" + pass + + def set_repeater_call_list(self, calls): + """Set the RPTCALL callsign list""" + pass + + def set_mycall_list(self, calls): + """Set the MYCALL callsign list""" + pass + + +class ExperimentalRadio: + """Interface for experimental radios""" + @classmethod + def get_experimental_warning(cls): + return ("This radio's driver is marked as experimental and may " + + "be unstable or unsafe to use.") + + +class Status: + """Clone status object for conveying clone progress to the UI""" + name = "Job" + msg = "Unknown" + max = 100 + cur = 0 + + def __str__(self): + try: + pct = (self.cur / float(self.max)) * 100 + nticks = int(pct) / 10 + ticks = "=" * nticks + except ValueError: + pct = 0.0 + ticks = "?" * 10 + + return "|%-10s| %2.1f%% %s" % (ticks, pct, self.msg) + + +def is_fractional_step(freq): + """Returns True if @freq requires a 12.5kHz or 6.25kHz step""" + return not is_5_0(freq) and (is_12_5(freq) or is_6_25(freq)) + + +def is_5_0(freq): + """Returns True if @freq is reachable by a 5kHz step""" + return (freq % 5000) == 0 + + +def is_12_5(freq): + """Returns True if @freq is reachable by a 12.5kHz step""" + return (freq % 12500) == 0 + + +def is_6_25(freq): + """Returns True if @freq is reachable by a 6.25kHz step""" + return (freq % 6250) == 0 + + +def is_2_5(freq): + """Returns True if @freq is reachable by a 2.5kHz step""" + return (freq % 2500) == 0 + + +def required_step(freq): + """Returns the simplest tuning step that is required to reach @freq""" + if is_5_0(freq): + return 5.0 + elif is_12_5(freq): + return 12.5 + elif is_6_25(freq): + return 6.25 + elif is_2_5(freq): + return 2.5 + else: + raise errors.InvalidDataError("Unable to calculate the required " + + "tuning step for %i.%5i" % + (freq / 1000000, freq % 1000000)) + + +def fix_rounded_step(freq): + """Some radios imply the last bit of 12.5kHz and 6.25kHz step + frequencies. Take the base @freq and return the corrected one""" + try: + required_step(freq) + return freq + except errors.InvalidDataError: + pass + + try: + required_step(freq + 500) + return freq + 500 + except errors.InvalidDataError: + pass + + try: + required_step(freq + 250) + return freq + 250 + except errors.InvalidDataError: + pass + + try: + required_step(freq + 750) + return float(freq + 750) + except errors.InvalidDataError: + pass + + raise errors.InvalidDataError("Unable to correct rounded frequency " + + format_freq(freq)) + + +def _name(name, len, just_upper): + """Justify @name to @len, optionally converting to all uppercase""" + if just_upper: + name = name.upper() + return name.ljust(len)[:len] + + +def name6(name, just_upper=True): + """6-char name""" + return _name(name, 6, just_upper) + + +def name8(name, just_upper=False): + """8-char name""" + return _name(name, 8, just_upper) + + +def name16(name, just_upper=False): + """16-char name""" + return _name(name, 16, just_upper) + + +def to_GHz(val): + """Convert @val in GHz to Hz""" + return val * 1000000000 + + +def to_MHz(val): + """Convert @val in MHz to Hz""" + return val * 1000000 + + +def to_kHz(val): + """Convert @val in kHz to Hz""" + return val * 1000 + + +def from_GHz(val): + """Convert @val in Hz to GHz""" + return val / 100000000 + + +def from_MHz(val): + """Convert @val in Hz to MHz""" + return val / 100000 + + +def from_kHz(val): + """Convert @val in Hz to kHz""" + return val / 100 + + +def split_tone_decode(mem, txtone, rxtone): + """ + Set tone mode and values on @mem based on txtone and rxtone specs like: + None, None, None + "Tone", 123.0, None + "DTCS", 23, "N" + """ + txmode, txval, txpol = txtone + rxmode, rxval, rxpol = rxtone + + mem.dtcs_polarity = "%s%s" % (txpol or "N", rxpol or "N") + + if not txmode and not rxmode: + # No tone + return + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + mem.rtone = txval + return + + if txmode == rxmode == "Tone" and txval == rxval: + # TX and RX same tone -> TSQL + mem.tmode = "TSQL" + mem.ctone = txval + return + + if txmode == rxmode == "DTCS" and txval == rxval: + mem.tmode = "DTCS" + mem.dtcs = txval + return + + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode or "", rxmode or "") + + if txmode == "Tone": + mem.rtone = txval + elif txmode == "DTCS": + mem.dtcs = txval + + if rxmode == "Tone": + mem.ctone = rxval + elif rxmode == "DTCS": + mem.rx_dtcs = rxval + + +def split_tone_encode(mem): + """ + Returns TX, RX tone specs based on @mem like: + None, None, None + "Tone", 123.0, None + "DTCS", 23, "N" + """ + + txmode = '' + rxmode = '' + txval = None + rxval = None + + if mem.tmode == "Tone": + txmode = "Tone" + txval = mem.rtone + elif mem.tmode == "TSQL": + txmode = rxmode = "Tone" + txval = rxval = mem.ctone + elif mem.tmode == "DTCS": + txmode = rxmode = "DTCS" + txval = rxval = mem.dtcs + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + if txmode == "Tone": + txval = mem.rtone + elif txmode == "DTCS": + txval = mem.dtcs + if rxmode == "Tone": + rxval = mem.ctone + elif rxmode == "DTCS": + rxval = mem.rx_dtcs + + if txmode == "DTCS": + txpol = mem.dtcs_polarity[0] + else: + txpol = None + if rxmode == "DTCS": + rxpol = mem.dtcs_polarity[1] + else: + rxpol = None + + return ((txmode, txval, txpol), + (rxmode, rxval, rxpol)) + + +def sanitize_string(astring, validcharset=CHARSET_ASCII, replacechar='*'): + myfilter = ''.join( + [ + [replacechar, chr(x)][chr(x) in validcharset] + for x in xrange(256) + ]) + return astring.translate(myfilter) diff -r b08fbd75a499 -r ed11ed06d671 chirp/drivers/ft450d.py --- a/chirp/drivers/ft450d.py Wed Jun 13 06:14:11 2018 -0700 +++ b/chirp/drivers/ft450d.py Tue Jun 19 14:27:25 2018 -0700 @@ -370,9 +370,20 @@ @classmethod def get_prompts(cls): rp = chirp_common.RadioPrompts() + rp.info = _(dedent(""" + The FT-450 radio driver loads the 'Special Channels' tab + with the PMS scanning range memories (group 11), 60meter + channels (group 12), the QMB (STO/RCL) memory, the HF and + 50m HOME memories and all the A and B VFO memories. + There are VFO memories for the last frequency dialed in + each band. The last mem-tune config is also stored. + These Special Channels allow limited field editting. + This driver also populates the 'Other' tab in the channel + memory Properties window. This tab contains values for + those channel memory settings that don't fall under the + standard Chirp display columns. + """)) rp.pre_download = _(dedent("""\ - Note that this radio has 'Special Channels' and generates an - 'Other' tab in the channel memory Properties window... 1. Turn radio off. 2. Connect cable to ACC jack. 3. Press and hold in the [MODE <] and [MODE >] keys while diff -r b08fbd75a499 -r ed11ed06d671 chirp/drivers/lt725uv.py --- a/chirp/drivers/lt725uv.py Wed Jun 13 06:14:11 2018 -0700 +++ b/chirp/drivers/lt725uv.py Tue Jun 19 14:27:25 2018 -0700 @@ -1,1446 +1,1445 @@ -# Copyright 2016: -# * Jim Unroe KC9HI, -# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), # -# 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 time -import struct -import logging -import re - -LOG = logging.getLogger(__name__) - -from chirp import chirp_common, directory, memmap -from chirp import bitwise, errors, util -from chirp.settings import RadioSettingGroup, RadioSetting, \ - RadioSettingValueBoolean, RadioSettingValueList, \ - RadioSettingValueString, RadioSettingValueInteger, \ - RadioSettingValueFloat, RadioSettings,InvalidValueError -from textwrap import dedent - -MEM_FORMAT = """ -#seekto 0x0200; -struct { - u8 init_bank; - u8 volume; - u16 fm_freq; - u8 wtled; - u8 rxled; - u8 txled; - u8 ledsw; - u8 beep; - u8 ring; - u8 bcl; - u8 tot; - u16 sig_freq; - u16 dtmf_txms; - u8 init_sql; - u8 rptr_mode; -} settings; - -#seekto 0x0240; -struct { - u8 dtmf1_cnt; - u8 dtmf1[7]; - u8 dtmf2_cnt; - u8 dtmf2[7]; - u8 dtmf3_cnt; - u8 dtmf3[7]; - u8 dtmf4_cnt; - u8 dtmf4[7]; - u8 dtmf5_cnt; - u8 dtmf5[7]; - u8 dtmf6_cnt; - u8 dtmf6[7]; - u8 dtmf7_cnt; - u8 dtmf7[7]; - u8 dtmf8_cnt; - u8 dtmf8[7]; -} dtmf_tab; - -#seekto 0x0280; -struct { - u8 native_id_cnt; - u8 native_id_code[7]; - u8 master_id_cnt; - u8 master_id_code[7]; - u8 alarm_cnt; - u8 alarm_code[5]; - u8 id_disp_cnt; - u8 id_disp_code[5]; - u8 revive_cnt; - u8 revive_code[5]; - u8 stun_cnt; - u8 stun_code[5]; - u8 kill_cnt; - u8 kill_code[5]; - u8 monitor_cnt; - u8 monitor_code[5]; - u8 state_now; -} codes; - -#seekto 0x02d0; -struct { - u8 hello1_cnt; - char hello1[7]; - u8 hello2_cnt; - char hello2[7]; - u32 vhf_low; - u32 vhf_high; - u32 uhf_low; - u32 uhf_high; - u8 lims_on; -} hello_lims; - -struct vfo { - u8 frq_chn_mode; - u8 chan_num; - u32 rxfreq; - u16 is_rxdigtone:1, - rxdtcs_pol:1, - rx_tone:14; - u8 rx_mode; - u8 unknown_ff; - u16 is_txdigtone:1, - txdtcs_pol:1, - tx_tone:14; - u8 launch_sig; - u8 tx_end_sig; - u8 bpower; - u8 fm_bw; - u8 cmp_nder; - u8 scrm_blr; - u8 shift; - u32 offset; - u16 step; - u8 sql; -}; - -#seekto 0x0300; -struct { - struct vfo vfoa; -} upper; - -#seekto 0x0380; -struct { - struct vfo vfob; -} lower; - -struct mem { - u32 rxfreq; - u16 is_rxdigtone:1, - rxdtcs_pol:1, - rxtone:14; - u8 recvmode; - u32 txfreq; - u16 is_txdigtone:1, - txdtcs_pol:1, - txtone:14; - u8 botsignal; - u8 eotsignal; - u8 power:1, - wide:1, - compandor:1 - scrambler:1 - unknown:4; - u8 namelen; - u8 name[7]; -}; - -#seekto 0x0400; -struct mem upper_memory[128]; - -#seekto 0x1000; -struct mem lower_memory[128]; - -#seekto 0x1C00; -struct { - char mod_num[6]; -} mod_id; -""" - -MEM_SIZE = 0x1C00 -BLOCK_SIZE = 0x40 -STIMEOUT = 2 -# Channel power: 2 levels -POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), - chirp_common.PowerLevel("High", watts=30.00)] - -LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"] -LIST_SIGNAL = ["Off"] + ["DTMF%s" % x for x in range(1, 9)] + \ - ["DTMF%s + Identity" % x for x in range(1, 9)] + \ - ["Identity code"] -# Band Power settings, can be different than channel power -LIST_BPOWER = ["Low", "Mid", "High"] # Tri-power models -LIST_COLOR = ["Off", "Orange", "Blue", "Purple"] -LIST_LEDSW = ["Auto", "On"] -LIST_RING = ["Off"] + ["%s" % x for x in range(1, 10)] -LIST_TDR_DEF = ["A-Upper", "B-Lower"] -LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 630, 30)] -LIST_VFOMODE = ["Frequency Mode", "Channel Mode"] -# Tones are numeric, Defined in \chirp\chirp_common.py -TONES_CTCSS = sorted(chirp_common.TONES) -# Converted to strings -LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS] -# Now append the DxxxN and DxxxI DTCS codes from chirp_common -for x in chirp_common.DTCS_CODES: - LIST_CTCSS.append("D{:03d}N".format(x)) -for x in chirp_common.DTCS_CODES: - LIST_CTCSS.append("D{:03d}R".format(x)) -LIST_BW = ["Narrow", "Wide"] -LIST_SHIFT = ["Off"," + ", " - "] -STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] -LIST_STEPS = [str(x) for x in STEPS] -LIST_STATE = ["Normal", "Stun", "Kill"] -LIST_SSF = ["1000", "1450", "1750", "2100"] -LIST_DTMFTX = ["50", "100", "150", "200", "300","500"] - -SETTING_LISTS = { -"init_bank": LIST_TDR_DEF , -"tot": LIST_TIMEOUT, -"wtled": LIST_COLOR, -"rxled": LIST_COLOR, -"txled": LIST_COLOR, -"sig_freq": LIST_SSF, -"dtmf_txms": LIST_DTMFTX, -"ledsw": LIST_LEDSW, -"frq_chn_mode": LIST_VFOMODE, -"rx_tone": LIST_CTCSS, -"tx_tone": LIST_CTCSS, -"rx_mode": LIST_RECVMODE, -"launch_sig": LIST_SIGNAL, -"tx_end_sig": LIST_SIGNAL, -"bpower":LIST_BPOWER, -"fm_bw": LIST_BW, -"shift": LIST_SHIFT, -"step": LIST_STEPS, -"ring": LIST_RING, -"state_now": LIST_STATE -} - -def _clean_buffer(radio): - radio.pipe.timeout = 0.005 - junk = radio.pipe.read(256) - radio.pipe.timeout = STIMEOUT - if junk: - Log.debug("Got %i bytes of junk before starting" % len(junk)) - - -def _rawrecv(radio, amount): - """Raw read from the radio device""" - data = "" - try: - data = radio.pipe.read(amount) - except: - _exit_program_mode(radio) - msg = "Generic error reading data from radio; check your cable." - raise errors.RadioError(msg) - - if len(data) != amount: - _exit_program_mode(radio) - msg = "Error reading from radio: not the amount of data we want." - raise errors.RadioError(msg) - - return data - - -def _rawsend(radio, data): - """Raw send to the radio device""" - try: - radio.pipe.write(data) - except: - raise errors.RadioError("Error sending data to radio") - - -def _make_frame(cmd, addr, length, data=""): - """Pack the info in the headder format""" - frame = struct.pack(">4sHH", cmd, addr, length) - # Add the data if set - if len(data) != 0: - frame += data - # Return the data - return frame - - -def _recv(radio, addr, length): - """Get data from the radio """ - - data = _rawrecv(radio, length) - - # DEBUG - LOG.info("Response:") - LOG.debug(util.hexprint(data)) - - return data - - -def _do_ident(radio): - """Put the radio in PROGRAM mode & identify it""" - # Set the serial discipline - radio.pipe.baudrate = 19200 - radio.pipe.parity = "N" - radio.pipe.timeout = STIMEOUT - - # Flush input buffer - _clean_buffer(radio) - - magic = "PROM_LIN" - - _rawsend(radio, magic) - - ack = _rawrecv(radio, 1) - if ack != "\x06": - _exit_program_mode(radio) - if ack: - LOG.debug(repr(ack)) - raise errors.RadioError("Radio did not respond") - - return True - - -def _exit_program_mode(radio): - endframe = "EXIT" - _rawsend(radio, endframe) - - -def _download(radio): - """Get the memory map""" - - # Put radio in program mode and identify it - _do_ident(radio) - - # UI progress - status = chirp_common.Status() - status.cur = 0 - status.max = MEM_SIZE / BLOCK_SIZE - status.msg = "Cloning from radio..." - radio.status_fn(status) - - data = "" - for addr in range(0, MEM_SIZE, BLOCK_SIZE): - frame = _make_frame("READ", addr, BLOCK_SIZE) - # DEBUG - LOG.info("Request sent:") - LOG.debug(util.hexprint(frame)) - - # Sending the read request - _rawsend(radio, frame) - - # Now we read - d = _recv(radio, addr, BLOCK_SIZE) - - # Aggregate the data - data += d - - # UI Update - status.cur = addr / BLOCK_SIZE - status.msg = "Cloning from radio..." - radio.status_fn(status) - - _exit_program_mode(radio) - - data += radio.MODEL.ljust(8) - - return data - - -def _upload(radio): - """Upload procedure""" - - # Put radio in program mode and identify it - _do_ident(radio) - - # UI progress - status = chirp_common.Status() - status.cur = 0 - status.max = MEM_SIZE / BLOCK_SIZE - status.msg = "Cloning to radio..." - radio.status_fn(status) - - # The fun starts here - for addr in range(0, MEM_SIZE, BLOCK_SIZE): - # Sending the data - data = radio.get_mmap()[addr:addr + BLOCK_SIZE] - - frame = _make_frame("WRIE", addr, BLOCK_SIZE, data) - - _rawsend(radio, frame) - - # Receiving the response - ack = _rawrecv(radio, 1) - if ack != "\x06": - _exit_program_mode(radio) - msg = "Bad ack writing block 0x%04x" % addr - raise errors.RadioError(msg) - - # UI Update - status.cur = addr / BLOCK_SIZE - status.msg = "Cloning to radio..." - radio.status_fn(status) - - _exit_program_mode(radio) - - -def model_match(cls, data): - """Match the opened/downloaded image to the correct version""" - if len(data) == 0x1C08: - rid = data[0x1C00:0x1C08] - return rid.startswith(cls.MODEL) - else: - return False - - -def _split(rf, f1, f2): - """Returns False if the two freqs are in the same band (no split) - or True otherwise""" - - # Determine if the two freqs are in the same band - for low, high in rf.valid_bands: - if f1 >= low and f1 <= high and \ - f2 >= low and f2 <= high: - # If the two freqs are on the same Band this is not a split - return False - - # If you get here is because the freq pairs are split - return True - - -@directory.register -class LT725UV(chirp_common.CloneModeRadio, - chirp_common.ExperimentalRadio): - """LUITON LT-725UV Radio""" - VENDOR = "LUITON" - MODEL = "LT-725UV" - MODES = ["NFM", "FM"] - TONES = chirp_common.TONES - DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) - NAME_LENGTH = 7 - DTMF_CHARS = list("0123456789ABCD*#") - - VALID_BANDS = [(136000000, 176000000), - (400000000, 480000000)] - - # Valid chars on the LCD - VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ - "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" - - @classmethod - def get_prompts(cls): - rp = chirp_common.RadioPrompts() - rp.experimental = \ - ('Some notes about POWER settings:\n' - '- The individual channel power settings are ignored' - ' by the radio.\n' - ' They are allowed to be set (and downloaded) in hopes of' - ' a future firmware update.\n' - '- Power settings done \'Live\' in the radio apply to the' - ' entire upper or lower band.\n' - '- Tri-power radio models will set and download the three' - ' band-power' - ' levels, but they are\n converted to just Low and High at' - ' upload.' - ' The Mid setting reverts to Low.' - ) - - rp.pre_download = _(dedent("""\ - Follow this instructions to download your info: - - 1 - Turn off your radio - 2 - Connect your interface cable - 3 - Turn on your radio - 4 - Do the download of your radio data - """)) - rp.pre_upload = _(dedent("""\ - Follow this instructions to upload your info: - - 1 - Turn off your radio - 2 - Connect your interface cable - 3 - Turn on your radio - 4 - Do the upload of your radio data - """)) - return rp - - def get_features(self): - rf = chirp_common.RadioFeatures() - rf.has_settings = True - rf.has_bank = False - rf.has_tuning_step = False - rf.can_odd_split = True - rf.has_name = True - rf.has_offset = True - rf.has_mode = True - rf.has_dtcs = True - rf.has_rx_dtcs = True - rf.has_dtcs_polarity = True - rf.has_ctone = True - rf.has_cross = True - rf.has_sub_devices = self.VARIANT == "" - rf.valid_modes = self.MODES - rf.valid_characters = self.VALID_CHARS - rf.valid_duplexes = ["", "-", "+", "split", "off"] - rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] - rf.valid_cross_modes = [ - "Tone->Tone", - "DTCS->", - "->DTCS", - "Tone->DTCS", - "DTCS->Tone", - "->Tone", - "DTCS->DTCS"] - rf.valid_skips = [] - rf.valid_power_levels = POWER_LEVELS - rf.valid_name_length = self.NAME_LENGTH - rf.valid_dtcs_codes = self.DTCS_CODES - rf.valid_bands = self.VALID_BANDS - rf.memory_bounds = (1, 128) - return rf - - def get_sub_devices(self): - return [LT725UVUpper(self._mmap), LT725UVLower(self._mmap)] - - def sync_in(self): - """Download from radio""" - try: - data = _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() - - def sync_out(self): - """Upload to radio""" - try: - _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 process_mmap(self): - """Process the mem map into the mem object""" - self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) - - def get_raw_memory(self, number): - return repr(self._memobj.memory[number - 1]) - - def _memory_obj(self, suffix=""): - return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix)) - - def _get_dcs(self, val): - return int(str(val)[2:-18]) - - def _set_dcs(self, val): - return int(str(val), 16) - - def get_memory(self, number): - _mem = self._memory_obj()[number - 1] - - mem = chirp_common.Memory() - mem.number = number - - if _mem.get_raw()[0] == "\xff": - mem.empty = True - return mem - - mem.freq = int(_mem.rxfreq) * 10 - - if _mem.txfreq == 0xFFFFFFFF: - # TX freq not set - mem.duplex = "off" - mem.offset = 0 - elif int(_mem.rxfreq) == int(_mem.txfreq): - mem.duplex = "" - mem.offset = 0 - elif _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10): - mem.duplex = "split" - mem.offset = int(_mem.txfreq) * 10 - else: - mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" - mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 - - for char in _mem.name[:_mem.namelen]: - mem.name += chr(char) - - dtcs_pol = ["N", "N"] - - if _mem.rxtone == 0x3FFF: - rxmode = "" - elif _mem.is_rxdigtone == 0: - # CTCSS - rxmode = "Tone" - mem.ctone = int(_mem.rxtone) / 10.0 - else: - # Digital - rxmode = "DTCS" - mem.rx_dtcs = self._get_dcs(_mem.rxtone) - if _mem.rxdtcs_pol == 1: - dtcs_pol[1] = "R" - - if _mem.txtone == 0x3FFF: - txmode = "" - elif _mem.is_txdigtone == 0: - # CTCSS - txmode = "Tone" - mem.rtone = int(_mem.txtone) / 10.0 - else: - # Digital - txmode = "DTCS" - mem.dtcs = self._get_dcs(_mem.txtone) - if _mem.txdtcs_pol == 1: - dtcs_pol[0] = "R" - - if txmode == "Tone" and not rxmode: - mem.tmode = "Tone" - elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: - mem.tmode = "TSQL" - elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: - mem.tmode = "DTCS" - elif rxmode or txmode: - mem.tmode = "Cross" - mem.cross_mode = "%s->%s" % (txmode, rxmode) - - mem.dtcs_polarity = "".join(dtcs_pol) - - mem.mode = _mem.wide and "FM" or "NFM" - - mem.power = POWER_LEVELS[_mem.power] - - # Extra - mem.extra = RadioSettingGroup("extra", "Extra") - - if _mem.recvmode == 0xFF: - val = 0x00 - else: - val = _mem.recvmode - recvmode = RadioSetting("recvmode", "Receiving mode", - RadioSettingValueList(LIST_RECVMODE, - LIST_RECVMODE[val])) - mem.extra.append(recvmode) - - if _mem.botsignal == 0xFF: - val = 0x00 - else: - val = _mem.botsignal - botsignal = RadioSetting("botsignal", "Launch signaling", - RadioSettingValueList(LIST_SIGNAL, - LIST_SIGNAL[val])) - mem.extra.append(botsignal) - - if _mem.eotsignal == 0xFF: - val = 0x00 - else: - val = _mem.eotsignal - - rx = RadioSettingValueList(LIST_SIGNAL, LIST_SIGNAL[val]) - eotsignal = RadioSetting("eotsignal", "Transmit end signaling", rx) - mem.extra.append(eotsignal) - - rx = RadioSettingValueBoolean(bool(_mem.compandor)) - compandor = RadioSetting("compandor", "Compandor", rx) - mem.extra.append(compandor) - - rx = RadioSettingValueBoolean(bool(_mem.scrambler)) - scrambler = RadioSetting("scrambler", "Scrambler", rx) - mem.extra.append(scrambler) - - return mem - - def set_memory(self, mem): - _mem = self._memory_obj()[mem.number - 1] - - if mem.empty: - _mem.set_raw("\xff" * 24) - _mem.namelen = 0 - return - - _mem.set_raw("\xFF" * 15 + "\x00\x00" + "\xFF" * 7) - - _mem.rxfreq = mem.freq / 10 - if mem.duplex == "off": - _mem.txfreq = 0xFFFFFFFF - 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.namelen = len(mem.name) - _namelength = self.get_features().valid_name_length - for i in range(_namelength): - try: - _mem.name[i] = ord(mem.name[i]) - except IndexError: - _mem.name[i] = 0xFF - - rxmode = "" - txmode = "" - - if mem.tmode == "Tone": - txmode = "Tone" - elif mem.tmode == "TSQL": - rxmode = "Tone" - txmode = "TSQL" - elif mem.tmode == "DTCS": - rxmode = "DTCSSQL" - txmode = "DTCS" - elif mem.tmode == "Cross": - txmode, rxmode = mem.cross_mode.split("->", 1) - - if rxmode == "": - _mem.rxdtcs_pol = 1 - _mem.is_rxdigtone = 1 - _mem.rxtone = 0x3FFF - elif rxmode == "Tone": - _mem.rxdtcs_pol = 0 - _mem.is_rxdigtone = 0 - _mem.rxtone = int(mem.ctone * 10) - elif rxmode == "DTCSSQL": - _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 - _mem.is_rxdigtone = 1 - _mem.rxtone = self._set_dcs(mem.dtcs) - elif rxmode == "DTCS": - _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 - _mem.is_rxdigtone = 1 - _mem.rxtone = self._set_dcs(mem.rx_dtcs) - - if txmode == "": - _mem.txdtcs_pol = 1 - _mem.is_txdigtone = 1 - _mem.txtone = 0x3FFF - elif txmode == "Tone": - _mem.txdtcs_pol = 0 - _mem.is_txdigtone = 0 - _mem.txtone = int(mem.rtone * 10) - elif txmode == "TSQL": - _mem.txdtcs_pol = 0 - _mem.is_txdigtone = 0 - _mem.txtone = int(mem.ctone * 10) - elif txmode == "DTCS": - _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0 - _mem.is_txdigtone = 1 - _mem.txtone = self._set_dcs(mem.dtcs) - - _mem.wide = self.MODES.index(mem.mode) - _mem.power = mem.power == POWER_LEVELS[1] - - # Extra settings - for setting in mem.extra: - setattr(_mem, setting.get_name(), setting.value) - - def get_settings(self): - """Translate the bit in the mem_struct into settings in the UI""" - # Define mem struct write-back shortcuts - _sets = self._memobj.settings - _vfoa = self._memobj.upper.vfoa - _vfob = self._memobj.lower.vfob - _lims = self._memobj.hello_lims - _codes = self._memobj.codes - _dtmf = self._memobj.dtmf_tab - - basic = RadioSettingGroup("basic", "Basic Settings") - a_band = RadioSettingGroup("a_band", "VFO A-Upper Settings") - b_band = RadioSettingGroup("b_band", "VFO B-Lower Settings") - codes = RadioSettingGroup("codes", "Codes & DTMF Groups") - lims = RadioSettingGroup("lims", "PowerOn & Freq Limits") - group = RadioSettings(basic, a_band, b_band, lims, codes) - - # Basic Settings - bnd_mode = RadioSetting("settings.init_bank", "TDR Band Default", - RadioSettingValueList(LIST_TDR_DEF, - LIST_TDR_DEF[ _sets.init_bank])) - basic.append(bnd_mode) - - volume = RadioSetting("settings.volume", "Volume", - RadioSettingValueInteger(0, 20, _sets.volume)) - basic.append(volume) - - val = _vfoa.bpower # 2bits values 0,1,2= Low, Mid, High - rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val]) - powera = RadioSetting("upper.vfoa.bpower", "Power (Upper)", rx) - basic.append(powera) - - val = _vfob.bpower - rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val]) - powerb = RadioSetting("lower.vfob.bpower", "Power (Lower)", rx) - basic.append(powerb) - - def my_word2raw(setting, obj, atrb, mlt=10): - """Callback function to convert UI floating value to u16 int""" - if str(setting.value) == "Off": - frq = 0x0FFFF - else: - frq = int(float(str(setting.value)) * float(mlt)) - if frq == 0: - frq = 0xFFFF - setattr(obj, atrb, frq) - return - - def my_adjraw(setting, obj, atrb, fix): - """Callback: add or subtract fix from value.""" - vx = int(str(setting.value)) - value = vx + int(fix) - if value < 0: - value = 0 - if atrb == "frq_chn_mode" and int(str(setting.value)) == 2: - value = vx * 2 # Special handling for frq_chn_mode - setattr(obj, atrb, value) - return - - def my_dbl2raw(setting, obj, atrb, flg=1): - """Callback: convert from freq 146.7600 to 14760000 U32.""" - value = chirp_common.parse_freq(str(setting.value)) / 10 - # flg=1 means 0 becomes ff, else leave as possible 0 - if flg == 1 and value == 0: - value = 0xFFFFFFFF - setattr(obj, atrb, value) - return - - def my_val_list(setting, obj, atrb): - """Callback:from ValueList with non-sequential, actual values.""" - value = int(str(setting.value)) # Get the integer value - if atrb == "tot": - value = int(value / 30) # 30 second increments - setattr(obj, atrb, value) - return - - def my_spcl(setting, obj, atrb): - """Callback: Special handling based on atrb.""" - if atrb == "frq_chn_mode": - idx = LIST_VFOMODE.index (str(setting.value)) # Returns 0 or 1 - value = idx * 2 # Set bit 1 - setattr(obj, atrb, value) - return - - def my_tone_strn(obj, is_atr, pol_atr, tone_atr): - """Generate the CTCS/DCS tone code string.""" - vx = int(getattr(obj, tone_atr)) - if vx == 16383 or vx == 0: - return "Off" # 16383 is all bits set - if getattr(obj, is_atr) == 0: # Simple CTCSS code - tstr = str(vx / 10.0) - else: # DCS - if getattr(obj, pol_atr) == 0: - tstr = "D{:03x}R".format(vx) - else: - tstr = "D{:03x}N".format(vx) - return tstr - - def my_set_tone(setting, obj, is_atr, pol_atr, tone_atr): - """Callback- create the tone setting from string code.""" - sx = str(setting.value) # '131.8' or 'D231N' or 'Off' - if sx == "Off": - isx = 1 - polx = 1 - tonx = 0x3FFF - elif sx[0] == "D": # DCS - isx = 1 - if sx[4] == "N": - polx = 1 - else: - polx = 0 - tonx = int(sx[1:4], 16) - else: # CTCSS - isx = 0 - polx = 0 - tonx = int(float(sx) * 10.0) - setattr(obj, is_atr, isx) - setattr(obj, pol_atr, polx) - setattr(obj, tone_atr, tonx) - return - - val = _sets.fm_freq / 10.0 - if val == 0: - val = 88.9 # 0 is not valid - rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1) - rs = RadioSetting("settings.fm_freq", "FM Broadcast Freq (MHz)", rx) - rs.set_apply_callback(my_word2raw, _sets, "fm_freq") - basic.append(rs) - - wtled = RadioSetting("settings.wtled", "Standby LED Color", - RadioSettingValueList(LIST_COLOR, LIST_COLOR[ - _sets.wtled])) - basic.append(wtled) - - rxled = RadioSetting("settings.rxled", "RX LED Color", - RadioSettingValueList(LIST_COLOR, LIST_COLOR[ - _sets.rxled])) - basic.append(rxled) - - txled = RadioSetting("settings.txled", "TX LED Color", - RadioSettingValueList(LIST_COLOR, LIST_COLOR[ - _sets.txled])) - basic.append(txled) - - ledsw = RadioSetting("settings.ledsw", "Back light mode", - RadioSettingValueList(LIST_LEDSW, LIST_LEDSW[ - _sets.ledsw])) - basic.append(ledsw) - - beep = RadioSetting("settings.beep", "Beep", - RadioSettingValueBoolean(bool(_sets.beep))) - basic.append(beep) - - ring = RadioSetting("settings.ring", "Ring", - RadioSettingValueList(LIST_RING, LIST_RING[ - _sets.ring])) - basic.append(ring) - - bcl = RadioSetting("settings.bcl", "Busy channel lockout", - RadioSettingValueBoolean(bool(_sets.bcl))) - basic.append(bcl) - - if _vfoa.sql == 0xFF: - val = 0x04 - else: - val = _vfoa.sql - sqla = RadioSetting("upper.vfoa.sql", "Squelch (Upper)", - RadioSettingValueInteger(0, 9, val)) - basic.append(sqla) - - if _vfob.sql == 0xFF: - val = 0x04 - else: - val = _vfob.sql - sqlb = RadioSetting("lower.vfob.sql", "Squelch (Lower)", - RadioSettingValueInteger(0, 9, val)) - basic.append(sqlb) - - tmp = str(int(_sets.tot) * 30) # 30 sec step counter - rs = RadioSetting("settings.tot", "Transmit Timeout (Secs)", - RadioSettingValueList(LIST_TIMEOUT, tmp)) - rs.set_apply_callback(my_val_list, _sets, "tot") - basic.append(rs) - - tmp = str(int(_sets.sig_freq)) - rs = RadioSetting("settings.sig_freq", "Single Signaling Tone (Htz)", - RadioSettingValueList(LIST_SSF, tmp)) - rs.set_apply_callback(my_val_list, _sets, "sig_freq") - basic.append(rs) - - tmp = str(int(_sets.dtmf_txms)) - rs = RadioSetting("settings.dtmf_txms", "DTMF Tx Duration (mSecs)", - RadioSettingValueList(LIST_DTMFTX, tmp)) - rs.set_apply_callback(my_val_list, _sets, "dtmf_txms") - basic.append(rs) - - rs = RadioSetting("settings.rptr_mode", "Repeater Mode", - RadioSettingValueBoolean(bool(_sets.rptr_mode))) - basic.append(rs) - - # UPPER BAND SETTINGS - - # Freq Mode, convert bit 1 state to index pointer - val = _vfoa.frq_chn_mode / 2 - - rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val]) - rs = RadioSetting("upper.vfoa.frq_chn_mode", "Default Mode", rx) - rs.set_apply_callback(my_spcl, _vfoa, "frq_chn_mode") - a_band.append(rs) - - val =_vfoa.chan_num + 1 # Add 1 for 1-128 displayed - rs = RadioSetting("upper.vfoa.chan_num", "Initial Chan", - RadioSettingValueInteger(1, 128, val)) - rs.set_apply_callback(my_adjraw, _vfoa, "chan_num", -1) - a_band.append(rs) - - val = _vfoa.rxfreq / 100000.0 - if (val < 136.0 or val > 176.0): - val = 146.520 # 2m calling - rs = RadioSetting("upper.vfoa.rxfreq ", "Default Recv Freq (MHz)", - RadioSettingValueFloat(136.0, 176.0, val, 0.001, 5)) - rs.set_apply_callback(my_dbl2raw, _vfoa, "rxfreq") - a_band.append(rs) - - tmp = my_tone_strn(_vfoa, "is_rxdigtone", "rxdtcs_pol", "rx_tone") - rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)", - RadioSettingValueList(LIST_CTCSS, tmp)) - rs.set_apply_callback(my_set_tone, _vfoa, "is_rxdigtone", - "rxdtcs_pol", "rx_tone") - a_band.append(rs) - - rx = RadioSettingValueList(LIST_RECVMODE, - LIST_RECVMODE[_vfoa.rx_mode]) - rs = RadioSetting("upper.vfoa.rx_mode", "Default Recv Mode", rx) - a_band.append(rs) - - tmp = my_tone_strn(_vfoa, "is_txdigtone", "txdtcs_pol", "tx_tone") - rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)", - RadioSettingValueList(LIST_CTCSS, tmp)) - rs.set_apply_callback(my_set_tone, _vfoa, "is_txdigtone", - "txdtcs_pol", "tx_tone") - a_band.append(rs) - - rs = RadioSetting("upper.vfoa.launch_sig", "Launch Signaling", - RadioSettingValueList(LIST_SIGNAL, - LIST_SIGNAL[_vfoa.launch_sig])) - a_band.append(rs) - - rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfoa.tx_end_sig]) - rs = RadioSetting("upper.vfoa.tx_end_sig", "Xmit End Signaling", rx) - a_band.append(rs) - - rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfoa.fm_bw]) - rs = RadioSetting("upper.vfoa.fm_bw", "Wide/Narrow Band", rx) - a_band.append(rs) - - rx = RadioSettingValueBoolean(bool(_vfoa.cmp_nder)) - rs = RadioSetting("upper.vfoa.cmp_nder", "Compandor", rx) - a_band.append(rs) - - rs = RadioSetting("upper.vfoa.scrm_blr", "Scrambler", - RadioSettingValueBoolean(bool(_vfoa.scrm_blr))) - a_band.append(rs) - - rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfoa.shift]) - rs = RadioSetting("upper.vfoa.shift", "Xmit Shift", rx) - a_band.append(rs) - - val = _vfoa.offset / 100000.0 - rs = RadioSetting("upper.vfoa.offset", "Xmit Offset (MHz)", - RadioSettingValueFloat(0, 100.0, val, 0.001, 3)) - # Allow zero value - rs.set_apply_callback(my_dbl2raw, _vfoa, "offset", 0) - a_band.append(rs) - - tmp = str(_vfoa.step / 100.0) - rs = RadioSetting("step", "Freq step (KHz)", - RadioSettingValueList(LIST_STEPS, tmp)) - rs.set_apply_callback(my_word2raw, _vfoa,"step", 100) - a_band.append(rs) - - # LOWER BAND SETTINGS - - val = _vfob.frq_chn_mode / 2 - rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val]) - rs = RadioSetting("lower.vfob.frq_chn_mode", "Default Mode", rx) - rs.set_apply_callback(my_spcl, _vfob, "frq_chn_mode") - b_band.append(rs) - - val = _vfob.chan_num + 1 - rs = RadioSetting("lower.vfob.chan_num", "Initial Chan", - RadioSettingValueInteger(0, 127, val)) - rs.set_apply_callback(my_adjraw, _vfob, "chan_num", -1) - b_band.append(rs) - - val = _vfob.rxfreq / 100000.0 - if (val < 400.0 or val > 480.0): - val = 446.0 # UHF calling - rs = RadioSetting("lower.vfob.rxfreq ", "Default Recv Freq (MHz)", - RadioSettingValueFloat(400.0, 480.0, val, 0.001, 5)) - rs.set_apply_callback(my_dbl2raw, _vfob, "rxfreq") - b_band.append(rs) - - tmp = my_tone_strn(_vfob, "is_rxdigtone", "rxdtcs_pol", "rx_tone") - rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)", - RadioSettingValueList(LIST_CTCSS, tmp)) - rs.set_apply_callback(my_set_tone, _vfob, "is_rxdigtone", - "rxdtcs_pol", "rx_tone") - b_band.append(rs) - - rx = RadioSettingValueList(LIST_RECVMODE, LIST_RECVMODE[_vfob.rx_mode]) - rs = RadioSetting("lower.vfob.rx_mode", "Default Recv Mode", rx) - b_band.append(rs) - - tmp = my_tone_strn(_vfob, "is_txdigtone", "txdtcs_pol", "tx_tone") - rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)", - RadioSettingValueList(LIST_CTCSS, tmp)) - rs.set_apply_callback(my_set_tone, _vfob, "is_txdigtone", - "txdtcs_pol", "tx_tone") - b_band.append(rs) - - rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.launch_sig]) - rs = RadioSetting("lower.vfob.launch_sig", "Launch Signaling", rx) - b_band.append(rs) - - rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.tx_end_sig]) - rs = RadioSetting("lower.vfob.tx_end_sig", "Xmit End Signaling", rx) - b_band.append(rs) - - rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfob.fm_bw]) - rs = RadioSetting("lower.vfob.fm_bw", "Wide/Narrow Band", rx) - b_band.append(rs) - - rs = RadioSetting("lower.vfob.cmp_nder", "Compandor", - RadioSettingValueBoolean(bool(_vfob.cmp_nder))) - b_band.append(rs) - - rs = RadioSetting("lower.vfob.scrm_blr", "Scrambler", - RadioSettingValueBoolean(bool(_vfob.scrm_blr))) - b_band.append(rs) - - rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfob.shift]) - rs = RadioSetting("lower.vfob.shift", "Xmit Shift", rx) - b_band.append(rs) - - val = _vfob.offset / 100000.0 - rs = RadioSetting("lower.vfob.offset", "Xmit Offset (MHz)", - RadioSettingValueFloat(0, 100.0, val, 0.001, 3)) - rs.set_apply_callback(my_dbl2raw, _vfob, "offset", 0) - b_band.append(rs) - - tmp = str(_vfob.step / 100.0) - rs = RadioSetting("step", "Freq step (KHz)", - RadioSettingValueList(LIST_STEPS, tmp)) - rs.set_apply_callback(my_word2raw, _vfob, "step", 100) - b_band.append(rs) - - # PowerOn & Freq Limits Settings - - def chars2str(cary, knt): - """Convert raw memory char array to a string: NOT a callback.""" - stx = "" - for char in cary[:knt]: - stx += chr(char) - return stx - - def my_str2ary(setting, obj, atrba, atrbc): - """Callback: convert 7-char string to char array with count.""" - ary = "" - knt = 7 - for j in range (6, -1, -1): # Strip trailing spaces - if str(setting.value)[j] == "" or str(setting.value)[j] == " ": - knt = knt - 1 - else: - break - for j in range(0, 7, 1): - if j < knt: ary += str(setting.value)[j] - else: ary += chr(0xFF) - setattr(obj, atrba, ary) - setattr(obj, atrbc, knt) - return - - tmp = chars2str(_lims.hello1, _lims.hello1_cnt) - rs = RadioSetting("hello_lims.hello1", "Power-On Message 1", - RadioSettingValueString(0, 7, tmp)) - rs.set_apply_callback(my_str2ary, _lims, "hello1", "hello1_cnt") - lims.append(rs) - - tmp = chars2str(_lims.hello2, _lims.hello2_cnt) - rs = RadioSetting("hello_lims.hello2", "Power-On Message 2", - RadioSettingValueString(0, 7, tmp)) - rs.set_apply_callback(my_str2ary, _lims,"hello2", "hello2_cnt") - lims.append(rs) - - # VALID_BANDS = [(136000000, 176000000),400000000, 480000000)] - - lval = _lims.vhf_low / 100000.0 - uval = _lims.vhf_high / 100000.0 - if lval >= uval: - lval = 144.0 - uval = 158.0 - - rs = RadioSetting("hello_lims.vhf_low", "Lower VHF Band Limit (MHz)", - RadioSettingValueFloat(136.0, 176.0, lval, 0.001, 3)) - rs.set_apply_callback(my_dbl2raw, _lims, "vhf_low") - lims.append(rs) - - rs = RadioSetting("hello_lims.vhf_high", "Upper VHF Band Limit (MHz)", - RadioSettingValueFloat(136.0, 176.0, uval, 0.001, 3)) - rs.set_apply_callback(my_dbl2raw, _lims, "vhf_high") - lims.append(rs) - - lval = _lims.uhf_low / 100000.0 - uval = _lims.uhf_high / 100000.0 - if lval >= uval: - lval = 420.0 - uval = 470.0 - - rs = RadioSetting("hello_lims.uhf_low", "Lower UHF Band Limit (MHz)", - RadioSettingValueFloat(400.0, 480.0, lval, 0.001, 3)) - rs.set_apply_callback(my_dbl2raw, _lims, "uhf_low") - lims.append(rs) - - rs = RadioSetting("hello_lims.uhf_high", "Upper UHF Band Limit (MHz)", - RadioSettingValueFloat(400.0, 480.0, uval, 0.001, 3)) - rs.set_apply_callback(my_dbl2raw, _lims, "uhf_high") - lims.append(rs) - - # Codes and DTMF Groups Settings - - def make_dtmf(ary, knt): - """Generate the DTMF code 1-8, NOT a callback.""" - tmp = "" - if knt > 0 and knt != 0xff: - for val in ary[:knt]: - if val > 0 and val <= 9: - tmp += chr(val + 48) - elif val == 0x0a: - tmp += "0" - elif val == 0x0d: - tmp += "A" - elif val == 0x0e: - tmp += "B" - elif val == 0x0f: - tmp += "C" - elif val == 0x00: - tmp += "D" - elif val == 0x0b: - tmp += "*" - elif val == 0x0c: - tmp += "#" - else: - msg = ("Invalid Character. Must be: 0-9,A,B,C,D,*,#") - raise InvalidValueError(msg) - return tmp - - def my_dtmf2raw(setting, obj, atrba, atrbc, syz=7): - """Callback: DTMF Code; sends 5 or 7-byte string.""" - draw = [] - knt = syz - for j in range (syz - 1, -1, -1): # Strip trailing spaces - if str(setting.value)[j] == "" or str(setting.value)[j] == " ": - knt = knt - 1 - else: - break - for j in range(0, syz): - bx = str(setting.value)[j] - obx = ord(bx) - dig = 0x0ff - if j < knt and knt > 0: # (Else) is pads - if bx == "0": - dig = 0x0a - elif bx == "A": - dig = 0x0d - elif bx == "B": - dig = 0x0e - elif bx == "C": - dig = 0x0f - elif bx == "D": - dig = 0x00 - elif bx == "*": - dig = 0x0b - elif bx == "#": - dig = 0x0c - elif obx >= 49 and obx <= 57: - dig = obx - 48 - else: - msg = ("Must be: 0-9,A,B,C,D,*,#") - raise InvalidValueError(msg) - # - End if/elif/else for bx - # - End if J<=knt - draw.append(dig) # Generate string of bytes - # - End for j - setattr(obj, atrba, draw) - setattr(obj, atrbc, knt) - return - - tmp = make_dtmf(_codes.native_id_code, _codes.native_id_cnt) - rs = RadioSetting("codes.native_id_code", "Native ID Code", - RadioSettingValueString(0, 7, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "native_id_code", - "native_id_cnt", 7) - codes.append(rs) - - tmp = make_dtmf(_codes.master_id_code, _codes.master_id_cnt) - rs = RadioSetting("codes.master_id_code", "Master Control ID Code", - RadioSettingValueString(0, 7, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "master_id_code", - "master_id_cnt",7) - codes.append(rs) - - tmp = make_dtmf(_codes.alarm_code, _codes.alarm_cnt) - rs = RadioSetting("codes.alarm_code", "Alarm Code", - RadioSettingValueString(0, 5, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "alarm_code", - "alarm_cnt", 5) - codes.append(rs) - - tmp = make_dtmf(_codes.id_disp_code, _codes.id_disp_cnt) - rs = RadioSetting("codes.id_disp_code", "Identify Display Code", - RadioSettingValueString(0, 5, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "id_disp_code", - "id_disp_cnt", 5) - codes.append(rs) - - tmp = make_dtmf(_codes.revive_code, _codes.revive_cnt) - rs = RadioSetting("codes.revive_code", "Revive Code", - RadioSettingValueString(0, 5, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes,"revive_code", - "revive_cnt", 5) - codes.append(rs) - - tmp = make_dtmf(_codes.stun_code, _codes.stun_cnt) - rs = RadioSetting("codes.stun_code", "Remote Stun Code", - RadioSettingValueString(0, 5, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "stun_code", - "stun_cnt", 5) - codes.append(rs) - - tmp = make_dtmf(_codes.kill_code, _codes.kill_cnt) - rs = RadioSetting("codes.kill_code", "Remote KILL Code", - RadioSettingValueString(0, 5, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "kill_code", - "kill_cnt", 5) - codes.append(rs) - - tmp = make_dtmf(_codes.monitor_code, _codes.monitor_cnt) - rs = RadioSetting("codes.monitor_code", "Monitor Code", - RadioSettingValueString(0, 5, tmp)) - rs.set_apply_callback(my_dtmf2raw, _codes, "monitor_code", - "monitor_cnt", 5) - codes.append(rs) - - val = _codes.state_now - if val > 2: - val = 0 - - rx = RadioSettingValueList(LIST_STATE, LIST_STATE[val]) - rs = RadioSetting("codes.state_now", "Current State", rx) - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf1, _dtmf.dtmf1_cnt) - rs = RadioSetting("dtmf_tab.dtmf1", "DTMF1 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf1", "dtmf1_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf2, _dtmf.dtmf2_cnt) - rs = RadioSetting("dtmf_tab.dtmf2", "DTMF2 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf2", "dtmf2_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf3, _dtmf.dtmf3_cnt) - rs = RadioSetting("dtmf_tab.dtmf3", "DTMF3 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf3", "dtmf3_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf4, _dtmf.dtmf4_cnt) - rs = RadioSetting("dtmf_tab.dtmf4", "DTMF4 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf4", "dtmf4_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf5, _dtmf.dtmf5_cnt) - rs = RadioSetting("dtmf_tab.dtmf5", "DTMF5 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf5", "dtmf5_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf6, _dtmf.dtmf6_cnt) - rs = RadioSetting("dtmf_tab.dtmf6", "DTMF6 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf6", "dtmf6_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf7, _dtmf.dtmf7_cnt) - rs = RadioSetting("dtmf_tab.dtmf7", "DTMF7 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf7", "dtmf7_cnt") - codes.append(rs) - - dtm = make_dtmf(_dtmf.dtmf8, _dtmf.dtmf8_cnt) - rs = RadioSetting("dtmf_tab.dtmf8", "DTMF8 String", - RadioSettingValueString(0, 7, dtm)) - rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf8", "dtmf8_cnt") - codes.append(rs) - - return group # END get_settings() - - - def set_settings(self, settings): - _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): - match_size = False - match_model = False - - # Testing the file data size - if len(filedata) == MEM_SIZE + 8: - match_size = True - - # Testing the firmware model fingerprint - match_model = model_match(cls, filedata) - - if match_size and match_model: - return True - else: - return False - - -class LT725UVUpper(LT725UV): - VARIANT = "Upper" - _vfo = "upper" - - -class LT725UVLower(LT725UV): - VARIANT = "Lower" - _vfo = "lower" - - -class Zastone(chirp_common.Alias): - """Declare BJ-218 alias for Zastone BJ-218.""" - VENDOR = "Zastone" - MODEL = "BJ-218" - - -class Hesenate(chirp_common.Alias): - """Declare BJ-218 alias for Hesenate BJ-218.""" - VENDOR = "Hesenate" - MODEL = "BJ-218" - - -@directory.register -class Baojie218(LT725UV): - """Baojie BJ-218""" - VENDOR = "Baojie" - MODEL = "BJ-218" - ALIASES = [Zastone, Hesenate, ] +# Copyright 2016: +# * Jim Unroe KC9HI, +# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), # +# 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 time +import struct +import logging +import re + +LOG = logging.getLogger(__name__) + +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings,InvalidValueError +from textwrap import dedent + +MEM_FORMAT = """ +#seekto 0x0200; +struct { + u8 init_bank; + u8 volume; + u16 fm_freq; + u8 wtled; + u8 rxled; + u8 txled; + u8 ledsw; + u8 beep; + u8 ring; + u8 bcl; + u8 tot; + u16 sig_freq; + u16 dtmf_txms; + u8 init_sql; + u8 rptr_mode; +} settings; + +#seekto 0x0240; +struct { + u8 dtmf1_cnt; + u8 dtmf1[7]; + u8 dtmf2_cnt; + u8 dtmf2[7]; + u8 dtmf3_cnt; + u8 dtmf3[7]; + u8 dtmf4_cnt; + u8 dtmf4[7]; + u8 dtmf5_cnt; + u8 dtmf5[7]; + u8 dtmf6_cnt; + u8 dtmf6[7]; + u8 dtmf7_cnt; + u8 dtmf7[7]; + u8 dtmf8_cnt; + u8 dtmf8[7]; +} dtmf_tab; + +#seekto 0x0280; +struct { + u8 native_id_cnt; + u8 native_id_code[7]; + u8 master_id_cnt; + u8 master_id_code[7]; + u8 alarm_cnt; + u8 alarm_code[5]; + u8 id_disp_cnt; + u8 id_disp_code[5]; + u8 revive_cnt; + u8 revive_code[5]; + u8 stun_cnt; + u8 stun_code[5]; + u8 kill_cnt; + u8 kill_code[5]; + u8 monitor_cnt; + u8 monitor_code[5]; + u8 state_now; +} codes; + +#seekto 0x02d0; +struct { + u8 hello1_cnt; + char hello1[7]; + u8 hello2_cnt; + char hello2[7]; + u32 vhf_low; + u32 vhf_high; + u32 uhf_low; + u32 uhf_high; + u8 lims_on; +} hello_lims; + +struct vfo { + u8 frq_chn_mode; + u8 chan_num; + u32 rxfreq; + u16 is_rxdigtone:1, + rxdtcs_pol:1, + rx_tone:14; + u8 rx_mode; + u8 unknown_ff; + u16 is_txdigtone:1, + txdtcs_pol:1, + tx_tone:14; + u8 launch_sig; + u8 tx_end_sig; + u8 bpower; + u8 fm_bw; + u8 cmp_nder; + u8 scrm_blr; + u8 shift; + u32 offset; + u16 step; + u8 sql; +}; + +#seekto 0x0300; +struct { + struct vfo vfoa; +} upper; + +#seekto 0x0380; +struct { + struct vfo vfob; +} lower; + +struct mem { + u32 rxfreq; + u16 is_rxdigtone:1, + rxdtcs_pol:1, + rxtone:14; + u8 recvmode; + u32 txfreq; + u16 is_txdigtone:1, + txdtcs_pol:1, + txtone:14; + u8 botsignal; + u8 eotsignal; + u8 power:1, + wide:1, + compandor:1 + scrambler:1 + unknown:4; + u8 namelen; + u8 name[7]; +}; + +#seekto 0x0400; +struct mem upper_memory[128]; + +#seekto 0x1000; +struct mem lower_memory[128]; + +#seekto 0x1C00; +struct { + char mod_num[6]; +} mod_id; +""" + +MEM_SIZE = 0x1C00 +BLOCK_SIZE = 0x40 +STIMEOUT = 2 +# Channel power: 2 levels +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("High", watts=30.00)] + +LIST_RECVMODE = ["QT/DQT", "QT/DQT + Signaling"] +LIST_SIGNAL = ["Off"] + ["DTMF%s" % x for x in range(1, 9)] + \ + ["DTMF%s + Identity" % x for x in range(1, 9)] + \ + ["Identity code"] +# Band Power settings, can be different than channel power +LIST_BPOWER = ["Low", "Mid", "High"] # Tri-power models +LIST_COLOR = ["Off", "Orange", "Blue", "Purple"] +LIST_LEDSW = ["Auto", "On"] +LIST_RING = ["Off"] + ["%s" % x for x in range(1, 10)] +LIST_TDR_DEF = ["A-Upper", "B-Lower"] +LIST_TIMEOUT = ["Off"] + ["%s" % x for x in range(30, 630, 30)] +LIST_VFOMODE = ["Frequency Mode", "Channel Mode"] +# Tones are numeric, Defined in \chirp\chirp_common.py +TONES_CTCSS = sorted(chirp_common.TONES) +# Converted to strings +LIST_CTCSS = ["Off"] + [str(x) for x in TONES_CTCSS] +# Now append the DxxxN and DxxxI DTCS codes from chirp_common +for x in chirp_common.DTCS_CODES: + LIST_CTCSS.append("D{:03d}N".format(x)) +for x in chirp_common.DTCS_CODES: + LIST_CTCSS.append("D{:03d}R".format(x)) +LIST_BW = ["Narrow", "Wide"] +LIST_SHIFT = ["Off"," + ", " - "] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0] +LIST_STEPS = [str(x) for x in STEPS] +LIST_STATE = ["Normal", "Stun", "Kill"] +LIST_SSF = ["1000", "1450", "1750", "2100"] +LIST_DTMFTX = ["50", "100", "150", "200", "300","500"] + +SETTING_LISTS = { +"init_bank": LIST_TDR_DEF , +"tot": LIST_TIMEOUT, +"wtled": LIST_COLOR, +"rxled": LIST_COLOR, +"txled": LIST_COLOR, +"sig_freq": LIST_SSF, +"dtmf_txms": LIST_DTMFTX, +"ledsw": LIST_LEDSW, +"frq_chn_mode": LIST_VFOMODE, +"rx_tone": LIST_CTCSS, +"tx_tone": LIST_CTCSS, +"rx_mode": LIST_RECVMODE, +"launch_sig": LIST_SIGNAL, +"tx_end_sig": LIST_SIGNAL, +"bpower":LIST_BPOWER, +"fm_bw": LIST_BW, +"shift": LIST_SHIFT, +"step": LIST_STEPS, +"ring": LIST_RING, +"state_now": LIST_STATE +} + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + Log.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except: + _exit_program_mode(radio) + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + _exit_program_mode(radio) + msg = "Error reading from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except: + raise errors.RadioError("Error sending data to radio") + + +def _make_frame(cmd, addr, length, data=""): + """Pack the info in the headder format""" + frame = struct.pack(">4sHH", cmd, addr, length) + # Add the data if set + if len(data) != 0: + frame += data + # Return the data + return frame + + +def _recv(radio, addr, length): + """Get data from the radio """ + + data = _rawrecv(radio, length) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(data)) + + return data + + +def _do_ident(radio): + """Put the radio in PROGRAM mode & identify it""" + # Set the serial discipline + radio.pipe.baudrate = 19200 + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # Flush input buffer + _clean_buffer(radio) + + magic = "PROM_LIN" + + _rawsend(radio, magic) + + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond") + + return True + + +def _exit_program_mode(radio): + endframe = "EXIT" + _rawsend(radio, endframe) + + +def _download(radio): + """Get the memory map""" + + # Put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + frame = _make_frame("READ", addr, BLOCK_SIZE) + # DEBUG + LOG.info("Request sent:") + LOG.debug(util.hexprint(frame)) + + # Sending the read request + _rawsend(radio, frame) + + # Now we read + d = _recv(radio, addr, BLOCK_SIZE) + + # Aggregate the data + data += d + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + data += radio.MODEL.ljust(8) + + return data + + +def _upload(radio): + """Upload procedure""" + + # Put radio in program mode and identify it + _do_ident(radio) + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # The fun starts here + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # Sending the data + data = radio.get_mmap()[addr:addr + BLOCK_SIZE] + + frame = _make_frame("WRIE", addr, BLOCK_SIZE, data) + + _rawsend(radio, frame) + + # Receiving the response + ack = _rawrecv(radio, 1) + if ack != "\x06": + _exit_program_mode(radio) + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + +def model_match(cls, data): + """Match the opened/downloaded image to the correct version""" + if len(data) == 0x1C08: + rid = data[0x1C00:0x1C08] + return rid.startswith(cls.MODEL) + else: + return False + + +def _split(rf, f1, f2): + """Returns False if the two freqs are in the same band (no split) + or True otherwise""" + + # Determine if the two freqs are in the same band + for low, high in rf.valid_bands: + if f1 >= low and f1 <= high and \ + f2 >= low and f2 <= high: + # If the two freqs are on the same Band this is not a split + return False + + # If you get here is because the freq pairs are split + return True + + +@directory.register +class LT725UV(chirp_common.CloneModeRadio): + """LUITON LT-725UV Radio""" + VENDOR = "LUITON" + MODEL = "LT-725UV" + MODES = ["NFM", "FM"] + TONES = chirp_common.TONES + DTCS_CODES = sorted(chirp_common.DTCS_CODES + [645]) + NAME_LENGTH = 7 + DTMF_CHARS = list("0123456789ABCD*#") + + VALID_BANDS = [(136000000, 176000000), + (400000000, 480000000)] + + # Valid chars on the LCD + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = \ + ('Some notes about POWER settings:\n' + '- The individual channel power settings are ignored' + ' by the radio.\n' + ' They are allowed to be set (and downloaded) in hopes of' + ' a future firmware update.\n' + '- Power settings done \'Live\' in the radio apply to the' + ' entire upper or lower band.\n' + '- Tri-power radio models will set and download the three' + ' band-power' + ' levels, but they are converted to just Low and High at' + ' upload.' + ' The Mid setting reverts to Low.' + ) + + rp.pre_download = _(dedent("""\ + Follow this instructions to download your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the download of your radio data + """)) + rp.pre_upload = _(dedent("""\ + Follow this instructions to upload your info: + + 1 - Turn off your radio + 2 - Connect your interface cable + 3 - Turn on your radio + 4 - Do the upload of your radio data + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.has_sub_devices = self.VARIANT == "" + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_duplexes = ["", "-", "+", "split", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_skips = [] + rf.valid_power_levels = POWER_LEVELS + rf.valid_name_length = self.NAME_LENGTH + rf.valid_dtcs_codes = self.DTCS_CODES + rf.valid_bands = self.VALID_BANDS + rf.memory_bounds = (1, 128) + return rf + + def get_sub_devices(self): + return [LT725UVUpper(self._mmap), LT725UVLower(self._mmap)] + + def sync_in(self): + """Download from radio""" + try: + data = _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() + + def sync_out(self): + """Upload to radio""" + try: + _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 process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _memory_obj(self, suffix=""): + return getattr(self._memobj, "%s_memory%s" % (self._vfo, suffix)) + + def _get_dcs(self, val): + return int(str(val)[2:-18]) + + def _set_dcs(self, val): + return int(str(val), 16) + + def get_memory(self, number): + _mem = self._memory_obj()[number - 1] + + mem = chirp_common.Memory() + mem.number = number + + if _mem.get_raw()[0] == "\xff": + mem.empty = True + return mem + + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + elif _split(self.get_features(), mem.freq, int(_mem.txfreq) * 10): + mem.duplex = "split" + mem.offset = int(_mem.txfreq) * 10 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + for char in _mem.name[:_mem.namelen]: + mem.name += chr(char) + + dtcs_pol = ["N", "N"] + + if _mem.rxtone == 0x3FFF: + rxmode = "" + elif _mem.is_rxdigtone == 0: + # CTCSS + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + else: + # Digital + rxmode = "DTCS" + mem.rx_dtcs = self._get_dcs(_mem.rxtone) + if _mem.rxdtcs_pol == 1: + dtcs_pol[1] = "R" + + if _mem.txtone == 0x3FFF: + txmode = "" + elif _mem.is_txdigtone == 0: + # CTCSS + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + else: + # Digital + txmode = "DTCS" + mem.dtcs = self._get_dcs(_mem.txtone) + if _mem.txdtcs_pol == 1: + dtcs_pol[0] = "R" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + mem.dtcs_polarity = "".join(dtcs_pol) + + mem.mode = _mem.wide and "FM" or "NFM" + + mem.power = POWER_LEVELS[_mem.power] + + # Extra + mem.extra = RadioSettingGroup("extra", "Extra") + + if _mem.recvmode == 0xFF: + val = 0x00 + else: + val = _mem.recvmode + recvmode = RadioSetting("recvmode", "Receiving mode", + RadioSettingValueList(LIST_RECVMODE, + LIST_RECVMODE[val])) + mem.extra.append(recvmode) + + if _mem.botsignal == 0xFF: + val = 0x00 + else: + val = _mem.botsignal + botsignal = RadioSetting("botsignal", "Launch signaling", + RadioSettingValueList(LIST_SIGNAL, + LIST_SIGNAL[val])) + mem.extra.append(botsignal) + + if _mem.eotsignal == 0xFF: + val = 0x00 + else: + val = _mem.eotsignal + + rx = RadioSettingValueList(LIST_SIGNAL, LIST_SIGNAL[val]) + eotsignal = RadioSetting("eotsignal", "Transmit end signaling", rx) + mem.extra.append(eotsignal) + + rx = RadioSettingValueBoolean(bool(_mem.compandor)) + compandor = RadioSetting("compandor", "Compandor", rx) + mem.extra.append(compandor) + + rx = RadioSettingValueBoolean(bool(_mem.scrambler)) + scrambler = RadioSetting("scrambler", "Scrambler", rx) + mem.extra.append(scrambler) + + return mem + + def set_memory(self, mem): + _mem = self._memory_obj()[mem.number - 1] + + if mem.empty: + _mem.set_raw("\xff" * 24) + _mem.namelen = 0 + return + + _mem.set_raw("\xFF" * 15 + "\x00\x00" + "\xFF" * 7) + + _mem.rxfreq = mem.freq / 10 + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + 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.namelen = len(mem.name) + _namelength = self.get_features().valid_name_length + for i in range(_namelength): + try: + _mem.name[i] = ord(mem.name[i]) + except IndexError: + _mem.name[i] = 0xFF + + rxmode = "" + txmode = "" + + if mem.tmode == "Tone": + txmode = "Tone" + elif mem.tmode == "TSQL": + rxmode = "Tone" + txmode = "TSQL" + elif mem.tmode == "DTCS": + rxmode = "DTCSSQL" + txmode = "DTCS" + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + + if rxmode == "": + _mem.rxdtcs_pol = 1 + _mem.is_rxdigtone = 1 + _mem.rxtone = 0x3FFF + elif rxmode == "Tone": + _mem.rxdtcs_pol = 0 + _mem.is_rxdigtone = 0 + _mem.rxtone = int(mem.ctone * 10) + elif rxmode == "DTCSSQL": + _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 + _mem.is_rxdigtone = 1 + _mem.rxtone = self._set_dcs(mem.dtcs) + elif rxmode == "DTCS": + _mem.rxdtcs_pol = 1 if mem.dtcs_polarity[1] == "R" else 0 + _mem.is_rxdigtone = 1 + _mem.rxtone = self._set_dcs(mem.rx_dtcs) + + if txmode == "": + _mem.txdtcs_pol = 1 + _mem.is_txdigtone = 1 + _mem.txtone = 0x3FFF + elif txmode == "Tone": + _mem.txdtcs_pol = 0 + _mem.is_txdigtone = 0 + _mem.txtone = int(mem.rtone * 10) + elif txmode == "TSQL": + _mem.txdtcs_pol = 0 + _mem.is_txdigtone = 0 + _mem.txtone = int(mem.ctone * 10) + elif txmode == "DTCS": + _mem.txdtcs_pol = 1 if mem.dtcs_polarity[0] == "R" else 0 + _mem.is_txdigtone = 1 + _mem.txtone = self._set_dcs(mem.dtcs) + + _mem.wide = self.MODES.index(mem.mode) + _mem.power = mem.power == POWER_LEVELS[1] + + # Extra settings + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + # Define mem struct write-back shortcuts + _sets = self._memobj.settings + _vfoa = self._memobj.upper.vfoa + _vfob = self._memobj.lower.vfob + _lims = self._memobj.hello_lims + _codes = self._memobj.codes + _dtmf = self._memobj.dtmf_tab + + basic = RadioSettingGroup("basic", "Basic Settings") + a_band = RadioSettingGroup("a_band", "VFO A-Upper Settings") + b_band = RadioSettingGroup("b_band", "VFO B-Lower Settings") + codes = RadioSettingGroup("codes", "Codes & DTMF Groups") + lims = RadioSettingGroup("lims", "PowerOn & Freq Limits") + group = RadioSettings(basic, a_band, b_band, lims, codes) + + # Basic Settings + bnd_mode = RadioSetting("settings.init_bank", "TDR Band Default", + RadioSettingValueList(LIST_TDR_DEF, + LIST_TDR_DEF[ _sets.init_bank])) + basic.append(bnd_mode) + + volume = RadioSetting("settings.volume", "Volume", + RadioSettingValueInteger(0, 20, _sets.volume)) + basic.append(volume) + + val = _vfoa.bpower # 2bits values 0,1,2= Low, Mid, High + rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val]) + powera = RadioSetting("upper.vfoa.bpower", "Power (Upper)", rx) + basic.append(powera) + + val = _vfob.bpower + rx = RadioSettingValueList(LIST_BPOWER, LIST_BPOWER[val]) + powerb = RadioSetting("lower.vfob.bpower", "Power (Lower)", rx) + basic.append(powerb) + + def my_word2raw(setting, obj, atrb, mlt=10): + """Callback function to convert UI floating value to u16 int""" + if str(setting.value) == "Off": + frq = 0x0FFFF + else: + frq = int(float(str(setting.value)) * float(mlt)) + if frq == 0: + frq = 0xFFFF + setattr(obj, atrb, frq) + return + + def my_adjraw(setting, obj, atrb, fix): + """Callback: add or subtract fix from value.""" + vx = int(str(setting.value)) + value = vx + int(fix) + if value < 0: + value = 0 + if atrb == "frq_chn_mode" and int(str(setting.value)) == 2: + value = vx * 2 # Special handling for frq_chn_mode + setattr(obj, atrb, value) + return + + def my_dbl2raw(setting, obj, atrb, flg=1): + """Callback: convert from freq 146.7600 to 14760000 U32.""" + value = chirp_common.parse_freq(str(setting.value)) / 10 + # flg=1 means 0 becomes ff, else leave as possible 0 + if flg == 1 and value == 0: + value = 0xFFFFFFFF + setattr(obj, atrb, value) + return + + def my_val_list(setting, obj, atrb): + """Callback:from ValueList with non-sequential, actual values.""" + value = int(str(setting.value)) # Get the integer value + if atrb == "tot": + value = int(value / 30) # 30 second increments + setattr(obj, atrb, value) + return + + def my_spcl(setting, obj, atrb): + """Callback: Special handling based on atrb.""" + if atrb == "frq_chn_mode": + idx = LIST_VFOMODE.index (str(setting.value)) # Returns 0 or 1 + value = idx * 2 # Set bit 1 + setattr(obj, atrb, value) + return + + def my_tone_strn(obj, is_atr, pol_atr, tone_atr): + """Generate the CTCS/DCS tone code string.""" + vx = int(getattr(obj, tone_atr)) + if vx == 16383 or vx == 0: + return "Off" # 16383 is all bits set + if getattr(obj, is_atr) == 0: # Simple CTCSS code + tstr = str(vx / 10.0) + else: # DCS + if getattr(obj, pol_atr) == 0: + tstr = "D{:03x}R".format(vx) + else: + tstr = "D{:03x}N".format(vx) + return tstr + + def my_set_tone(setting, obj, is_atr, pol_atr, tone_atr): + """Callback- create the tone setting from string code.""" + sx = str(setting.value) # '131.8' or 'D231N' or 'Off' + if sx == "Off": + isx = 1 + polx = 1 + tonx = 0x3FFF + elif sx[0] == "D": # DCS + isx = 1 + if sx[4] == "N": + polx = 1 + else: + polx = 0 + tonx = int(sx[1:4], 16) + else: # CTCSS + isx = 0 + polx = 0 + tonx = int(float(sx) * 10.0) + setattr(obj, is_atr, isx) + setattr(obj, pol_atr, polx) + setattr(obj, tone_atr, tonx) + return + + val = _sets.fm_freq / 10.0 + if val == 0: + val = 88.9 # 0 is not valid + rx = RadioSettingValueFloat(65, 108.0, val, 0.1, 1) + rs = RadioSetting("settings.fm_freq", "FM Broadcast Freq (MHz)", rx) + rs.set_apply_callback(my_word2raw, _sets, "fm_freq") + basic.append(rs) + + wtled = RadioSetting("settings.wtled", "Standby LED Color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _sets.wtled])) + basic.append(wtled) + + rxled = RadioSetting("settings.rxled", "RX LED Color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _sets.rxled])) + basic.append(rxled) + + txled = RadioSetting("settings.txled", "TX LED Color", + RadioSettingValueList(LIST_COLOR, LIST_COLOR[ + _sets.txled])) + basic.append(txled) + + ledsw = RadioSetting("settings.ledsw", "Back light mode", + RadioSettingValueList(LIST_LEDSW, LIST_LEDSW[ + _sets.ledsw])) + basic.append(ledsw) + + beep = RadioSetting("settings.beep", "Beep", + RadioSettingValueBoolean(bool(_sets.beep))) + basic.append(beep) + + ring = RadioSetting("settings.ring", "Ring", + RadioSettingValueList(LIST_RING, LIST_RING[ + _sets.ring])) + basic.append(ring) + + bcl = RadioSetting("settings.bcl", "Busy channel lockout", + RadioSettingValueBoolean(bool(_sets.bcl))) + basic.append(bcl) + + if _vfoa.sql == 0xFF: + val = 0x04 + else: + val = _vfoa.sql + sqla = RadioSetting("upper.vfoa.sql", "Squelch (Upper)", + RadioSettingValueInteger(0, 9, val)) + basic.append(sqla) + + if _vfob.sql == 0xFF: + val = 0x04 + else: + val = _vfob.sql + sqlb = RadioSetting("lower.vfob.sql", "Squelch (Lower)", + RadioSettingValueInteger(0, 9, val)) + basic.append(sqlb) + + tmp = str(int(_sets.tot) * 30) # 30 sec step counter + rs = RadioSetting("settings.tot", "Transmit Timeout (Secs)", + RadioSettingValueList(LIST_TIMEOUT, tmp)) + rs.set_apply_callback(my_val_list, _sets, "tot") + basic.append(rs) + + tmp = str(int(_sets.sig_freq)) + rs = RadioSetting("settings.sig_freq", "Single Signaling Tone (Htz)", + RadioSettingValueList(LIST_SSF, tmp)) + rs.set_apply_callback(my_val_list, _sets, "sig_freq") + basic.append(rs) + + tmp = str(int(_sets.dtmf_txms)) + rs = RadioSetting("settings.dtmf_txms", "DTMF Tx Duration (mSecs)", + RadioSettingValueList(LIST_DTMFTX, tmp)) + rs.set_apply_callback(my_val_list, _sets, "dtmf_txms") + basic.append(rs) + + rs = RadioSetting("settings.rptr_mode", "Repeater Mode", + RadioSettingValueBoolean(bool(_sets.rptr_mode))) + basic.append(rs) + + # UPPER BAND SETTINGS + + # Freq Mode, convert bit 1 state to index pointer + val = _vfoa.frq_chn_mode / 2 + + rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val]) + rs = RadioSetting("upper.vfoa.frq_chn_mode", "Default Mode", rx) + rs.set_apply_callback(my_spcl, _vfoa, "frq_chn_mode") + a_band.append(rs) + + val =_vfoa.chan_num + 1 # Add 1 for 1-128 displayed + rs = RadioSetting("upper.vfoa.chan_num", "Initial Chan", + RadioSettingValueInteger(1, 128, val)) + rs.set_apply_callback(my_adjraw, _vfoa, "chan_num", -1) + a_band.append(rs) + + val = _vfoa.rxfreq / 100000.0 + if (val < 136.0 or val > 176.0): + val = 146.520 # 2m calling + rs = RadioSetting("upper.vfoa.rxfreq ", "Default Recv Freq (MHz)", + RadioSettingValueFloat(136.0, 176.0, val, 0.001, 5)) + rs.set_apply_callback(my_dbl2raw, _vfoa, "rxfreq") + a_band.append(rs) + + tmp = my_tone_strn(_vfoa, "is_rxdigtone", "rxdtcs_pol", "rx_tone") + rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfoa, "is_rxdigtone", + "rxdtcs_pol", "rx_tone") + a_band.append(rs) + + rx = RadioSettingValueList(LIST_RECVMODE, + LIST_RECVMODE[_vfoa.rx_mode]) + rs = RadioSetting("upper.vfoa.rx_mode", "Default Recv Mode", rx) + a_band.append(rs) + + tmp = my_tone_strn(_vfoa, "is_txdigtone", "txdtcs_pol", "tx_tone") + rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfoa, "is_txdigtone", + "txdtcs_pol", "tx_tone") + a_band.append(rs) + + rs = RadioSetting("upper.vfoa.launch_sig", "Launch Signaling", + RadioSettingValueList(LIST_SIGNAL, + LIST_SIGNAL[_vfoa.launch_sig])) + a_band.append(rs) + + rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfoa.tx_end_sig]) + rs = RadioSetting("upper.vfoa.tx_end_sig", "Xmit End Signaling", rx) + a_band.append(rs) + + rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfoa.fm_bw]) + rs = RadioSetting("upper.vfoa.fm_bw", "Wide/Narrow Band", rx) + a_band.append(rs) + + rx = RadioSettingValueBoolean(bool(_vfoa.cmp_nder)) + rs = RadioSetting("upper.vfoa.cmp_nder", "Compandor", rx) + a_band.append(rs) + + rs = RadioSetting("upper.vfoa.scrm_blr", "Scrambler", + RadioSettingValueBoolean(bool(_vfoa.scrm_blr))) + a_band.append(rs) + + rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfoa.shift]) + rs = RadioSetting("upper.vfoa.shift", "Xmit Shift", rx) + a_band.append(rs) + + val = _vfoa.offset / 100000.0 + rs = RadioSetting("upper.vfoa.offset", "Xmit Offset (MHz)", + RadioSettingValueFloat(0, 100.0, val, 0.001, 3)) + # Allow zero value + rs.set_apply_callback(my_dbl2raw, _vfoa, "offset", 0) + a_band.append(rs) + + tmp = str(_vfoa.step / 100.0) + rs = RadioSetting("step", "Freq step (KHz)", + RadioSettingValueList(LIST_STEPS, tmp)) + rs.set_apply_callback(my_word2raw, _vfoa,"step", 100) + a_band.append(rs) + + # LOWER BAND SETTINGS + + val = _vfob.frq_chn_mode / 2 + rx = RadioSettingValueList(LIST_VFOMODE, LIST_VFOMODE[val]) + rs = RadioSetting("lower.vfob.frq_chn_mode", "Default Mode", rx) + rs.set_apply_callback(my_spcl, _vfob, "frq_chn_mode") + b_band.append(rs) + + val = _vfob.chan_num + 1 + rs = RadioSetting("lower.vfob.chan_num", "Initial Chan", + RadioSettingValueInteger(0, 127, val)) + rs.set_apply_callback(my_adjraw, _vfob, "chan_num", -1) + b_band.append(rs) + + val = _vfob.rxfreq / 100000.0 + if (val < 400.0 or val > 480.0): + val = 446.0 # UHF calling + rs = RadioSetting("lower.vfob.rxfreq ", "Default Recv Freq (MHz)", + RadioSettingValueFloat(400.0, 480.0, val, 0.001, 5)) + rs.set_apply_callback(my_dbl2raw, _vfob, "rxfreq") + b_band.append(rs) + + tmp = my_tone_strn(_vfob, "is_rxdigtone", "rxdtcs_pol", "rx_tone") + rs = RadioSetting("rx_tone", "Default Recv CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfob, "is_rxdigtone", + "rxdtcs_pol", "rx_tone") + b_band.append(rs) + + rx = RadioSettingValueList(LIST_RECVMODE, LIST_RECVMODE[_vfob.rx_mode]) + rs = RadioSetting("lower.vfob.rx_mode", "Default Recv Mode", rx) + b_band.append(rs) + + tmp = my_tone_strn(_vfob, "is_txdigtone", "txdtcs_pol", "tx_tone") + rs = RadioSetting("tx_tone", "Default Xmit CTCSS (Htz)", + RadioSettingValueList(LIST_CTCSS, tmp)) + rs.set_apply_callback(my_set_tone, _vfob, "is_txdigtone", + "txdtcs_pol", "tx_tone") + b_band.append(rs) + + rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.launch_sig]) + rs = RadioSetting("lower.vfob.launch_sig", "Launch Signaling", rx) + b_band.append(rs) + + rx = RadioSettingValueList(LIST_SIGNAL,LIST_SIGNAL[_vfob.tx_end_sig]) + rs = RadioSetting("lower.vfob.tx_end_sig", "Xmit End Signaling", rx) + b_band.append(rs) + + rx = RadioSettingValueList(LIST_BW, LIST_BW[_vfob.fm_bw]) + rs = RadioSetting("lower.vfob.fm_bw", "Wide/Narrow Band", rx) + b_band.append(rs) + + rs = RadioSetting("lower.vfob.cmp_nder", "Compandor", + RadioSettingValueBoolean(bool(_vfob.cmp_nder))) + b_band.append(rs) + + rs = RadioSetting("lower.vfob.scrm_blr", "Scrambler", + RadioSettingValueBoolean(bool(_vfob.scrm_blr))) + b_band.append(rs) + + rx = RadioSettingValueList(LIST_SHIFT, LIST_SHIFT[_vfob.shift]) + rs = RadioSetting("lower.vfob.shift", "Xmit Shift", rx) + b_band.append(rs) + + val = _vfob.offset / 100000.0 + rs = RadioSetting("lower.vfob.offset", "Xmit Offset (MHz)", + RadioSettingValueFloat(0, 100.0, val, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _vfob, "offset", 0) + b_band.append(rs) + + tmp = str(_vfob.step / 100.0) + rs = RadioSetting("step", "Freq step (KHz)", + RadioSettingValueList(LIST_STEPS, tmp)) + rs.set_apply_callback(my_word2raw, _vfob, "step", 100) + b_band.append(rs) + + # PowerOn & Freq Limits Settings + + def chars2str(cary, knt): + """Convert raw memory char array to a string: NOT a callback.""" + stx = "" + for char in cary[:knt]: + stx += chr(char) + return stx + + def my_str2ary(setting, obj, atrba, atrbc): + """Callback: convert 7-char string to char array with count.""" + ary = "" + knt = 7 + for j in range (6, -1, -1): # Strip trailing spaces + if str(setting.value)[j] == "" or str(setting.value)[j] == " ": + knt = knt - 1 + else: + break + for j in range(0, 7, 1): + if j < knt: ary += str(setting.value)[j] + else: ary += chr(0xFF) + setattr(obj, atrba, ary) + setattr(obj, atrbc, knt) + return + + tmp = chars2str(_lims.hello1, _lims.hello1_cnt) + rs = RadioSetting("hello_lims.hello1", "Power-On Message 1", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_str2ary, _lims, "hello1", "hello1_cnt") + lims.append(rs) + + tmp = chars2str(_lims.hello2, _lims.hello2_cnt) + rs = RadioSetting("hello_lims.hello2", "Power-On Message 2", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_str2ary, _lims,"hello2", "hello2_cnt") + lims.append(rs) + + # VALID_BANDS = [(136000000, 176000000),400000000, 480000000)] + + lval = _lims.vhf_low / 100000.0 + uval = _lims.vhf_high / 100000.0 + if lval >= uval: + lval = 144.0 + uval = 158.0 + + rs = RadioSetting("hello_lims.vhf_low", "Lower VHF Band Limit (MHz)", + RadioSettingValueFloat(136.0, 176.0, lval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "vhf_low") + lims.append(rs) + + rs = RadioSetting("hello_lims.vhf_high", "Upper VHF Band Limit (MHz)", + RadioSettingValueFloat(136.0, 176.0, uval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "vhf_high") + lims.append(rs) + + lval = _lims.uhf_low / 100000.0 + uval = _lims.uhf_high / 100000.0 + if lval >= uval: + lval = 420.0 + uval = 470.0 + + rs = RadioSetting("hello_lims.uhf_low", "Lower UHF Band Limit (MHz)", + RadioSettingValueFloat(400.0, 480.0, lval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "uhf_low") + lims.append(rs) + + rs = RadioSetting("hello_lims.uhf_high", "Upper UHF Band Limit (MHz)", + RadioSettingValueFloat(400.0, 480.0, uval, 0.001, 3)) + rs.set_apply_callback(my_dbl2raw, _lims, "uhf_high") + lims.append(rs) + + # Codes and DTMF Groups Settings + + def make_dtmf(ary, knt): + """Generate the DTMF code 1-8, NOT a callback.""" + tmp = "" + if knt > 0 and knt != 0xff: + for val in ary[:knt]: + if val > 0 and val <= 9: + tmp += chr(val + 48) + elif val == 0x0a: + tmp += "0" + elif val == 0x0d: + tmp += "A" + elif val == 0x0e: + tmp += "B" + elif val == 0x0f: + tmp += "C" + elif val == 0x00: + tmp += "D" + elif val == 0x0b: + tmp += "*" + elif val == 0x0c: + tmp += "#" + else: + msg = ("Invalid Character. Must be: 0-9,A,B,C,D,*,#") + raise InvalidValueError(msg) + return tmp + + def my_dtmf2raw(setting, obj, atrba, atrbc, syz=7): + """Callback: DTMF Code; sends 5 or 7-byte string.""" + draw = [] + knt = syz + for j in range (syz - 1, -1, -1): # Strip trailing spaces + if str(setting.value)[j] == "" or str(setting.value)[j] == " ": + knt = knt - 1 + else: + break + for j in range(0, syz): + bx = str(setting.value)[j] + obx = ord(bx) + dig = 0x0ff + if j < knt and knt > 0: # (Else) is pads + if bx == "0": + dig = 0x0a + elif bx == "A": + dig = 0x0d + elif bx == "B": + dig = 0x0e + elif bx == "C": + dig = 0x0f + elif bx == "D": + dig = 0x00 + elif bx == "*": + dig = 0x0b + elif bx == "#": + dig = 0x0c + elif obx >= 49 and obx <= 57: + dig = obx - 48 + else: + msg = ("Must be: 0-9,A,B,C,D,*,#") + raise InvalidValueError(msg) + # - End if/elif/else for bx + # - End if J<=knt + draw.append(dig) # Generate string of bytes + # - End for j + setattr(obj, atrba, draw) + setattr(obj, atrbc, knt) + return + + tmp = make_dtmf(_codes.native_id_code, _codes.native_id_cnt) + rs = RadioSetting("codes.native_id_code", "Native ID Code", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "native_id_code", + "native_id_cnt", 7) + codes.append(rs) + + tmp = make_dtmf(_codes.master_id_code, _codes.master_id_cnt) + rs = RadioSetting("codes.master_id_code", "Master Control ID Code", + RadioSettingValueString(0, 7, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "master_id_code", + "master_id_cnt",7) + codes.append(rs) + + tmp = make_dtmf(_codes.alarm_code, _codes.alarm_cnt) + rs = RadioSetting("codes.alarm_code", "Alarm Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "alarm_code", + "alarm_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.id_disp_code, _codes.id_disp_cnt) + rs = RadioSetting("codes.id_disp_code", "Identify Display Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "id_disp_code", + "id_disp_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.revive_code, _codes.revive_cnt) + rs = RadioSetting("codes.revive_code", "Revive Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes,"revive_code", + "revive_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.stun_code, _codes.stun_cnt) + rs = RadioSetting("codes.stun_code", "Remote Stun Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "stun_code", + "stun_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.kill_code, _codes.kill_cnt) + rs = RadioSetting("codes.kill_code", "Remote KILL Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "kill_code", + "kill_cnt", 5) + codes.append(rs) + + tmp = make_dtmf(_codes.monitor_code, _codes.monitor_cnt) + rs = RadioSetting("codes.monitor_code", "Monitor Code", + RadioSettingValueString(0, 5, tmp)) + rs.set_apply_callback(my_dtmf2raw, _codes, "monitor_code", + "monitor_cnt", 5) + codes.append(rs) + + val = _codes.state_now + if val > 2: + val = 0 + + rx = RadioSettingValueList(LIST_STATE, LIST_STATE[val]) + rs = RadioSetting("codes.state_now", "Current State", rx) + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf1, _dtmf.dtmf1_cnt) + rs = RadioSetting("dtmf_tab.dtmf1", "DTMF1 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf1", "dtmf1_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf2, _dtmf.dtmf2_cnt) + rs = RadioSetting("dtmf_tab.dtmf2", "DTMF2 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf2", "dtmf2_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf3, _dtmf.dtmf3_cnt) + rs = RadioSetting("dtmf_tab.dtmf3", "DTMF3 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf3", "dtmf3_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf4, _dtmf.dtmf4_cnt) + rs = RadioSetting("dtmf_tab.dtmf4", "DTMF4 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf4", "dtmf4_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf5, _dtmf.dtmf5_cnt) + rs = RadioSetting("dtmf_tab.dtmf5", "DTMF5 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf5", "dtmf5_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf6, _dtmf.dtmf6_cnt) + rs = RadioSetting("dtmf_tab.dtmf6", "DTMF6 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf6", "dtmf6_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf7, _dtmf.dtmf7_cnt) + rs = RadioSetting("dtmf_tab.dtmf7", "DTMF7 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf7", "dtmf7_cnt") + codes.append(rs) + + dtm = make_dtmf(_dtmf.dtmf8, _dtmf.dtmf8_cnt) + rs = RadioSetting("dtmf_tab.dtmf8", "DTMF8 String", + RadioSettingValueString(0, 7, dtm)) + rs.set_apply_callback(my_dtmf2raw, _dtmf, "dtmf8", "dtmf8_cnt") + codes.append(rs) + + return group # END get_settings() + + + def set_settings(self, settings): + _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): + match_size = False + match_model = False + + # Testing the file data size + if len(filedata) == MEM_SIZE + 8: + match_size = True + + # Testing the firmware model fingerprint + match_model = model_match(cls, filedata) + + if match_size and match_model: + return True + else: + return False + + +class LT725UVUpper(LT725UV): + VARIANT = "Upper" + _vfo = "upper" + + +class LT725UVLower(LT725UV): + VARIANT = "Lower" + _vfo = "lower" + + +class Zastone(chirp_common.Alias): + """Declare BJ-218 alias for Zastone BJ-218.""" + VENDOR = "Zastone" + MODEL = "BJ-218" + + +class Hesenate(chirp_common.Alias): + """Declare BJ-218 alias for Hesenate BJ-218.""" + VENDOR = "Hesenate" + MODEL = "BJ-218" + + +@directory.register +class Baojie218(LT725UV): + """Baojie BJ-218""" + VENDOR = "Baojie" + MODEL = "BJ-218" + ALIASES = [Zastone, Hesenate, ] diff -r b08fbd75a499 -r ed11ed06d671 chirp/ui/mainapp.py --- a/chirp/ui/mainapp.py Wed Jun 13 06:14:11 2018 -0700 +++ b/chirp/ui/mainapp.py Tue Jun 19 14:27:25 2018 -0700 @@ -1,2082 +1,2130 @@ -# Copyright 2008 Dan Smith -# Copyright 2012 Tom Hayward -# -# 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 . - -from datetime import datetime -import os -import tempfile -import urllib -import webbrowser -from glob import glob -import shutil -import time -import logging -import gtk -import gobject -import sys - -from chirp.ui import inputdialog, common -from chirp import platform, directory, util -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 -from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips -from chirp.ui import bandplans - -gobject.threads_init() - -LOG = logging.getLogger(__name__) - -if __name__ == "__main__": - sys.path.insert(0, "..") - -try: - import serial -except ImportError, e: - common.log_exception() - common.show_error("\nThe Pyserial module is not installed!") - - -CONF = config.get() - -KEEP_RECENT = 8 - -RB_BANDS = { - "--All--": 0, - "10 meters (29MHz)": 29, - "6 meters (54MHz)": 5, - "2 meters (144MHz)": 14, - "1.25 meters (220MHz)": 22, - "70 centimeters (440MHz)": 4, - "33 centimeters (900MHz)": 9, - "23 centimeters (1.2GHz)": 12, -} - - -def key_bands(band): - if band.startswith("-"): - return -1 - - amount, units, mhz = band.split(" ") - scale = units == "meters" and 100 or 1 - - return 100000 - (float(amount) * scale) - - -class ModifiedError(Exception): - pass - - -class ChirpMain(gtk.Window): - - def get_current_editorset(self): - page = self.tabs.get_current_page() - if page is not None: - return self.tabs.get_nth_page(page) - else: - return None - - def ev_tab_switched(self, pagenum=None): - def set_action_sensitive(action, sensitive): - self.menu_ag.get_action(action).set_sensitive(sensitive) - - if pagenum is not None: - eset = self.tabs.get_nth_page(pagenum) - else: - eset = self.get_current_editorset() - - upload_sens = bool(eset and - isinstance(eset.radio, chirp_common.CloneModeRadio)) - - if not eset or isinstance(eset.radio, chirp_common.LiveRadio): - save_sens = False - elif isinstance(eset.radio, chirp_common.NetworkSourceRadio): - save_sens = False - else: - save_sens = True - - for i in ["import", "importsrc", "stock"]: - set_action_sensitive(i, - eset is not None and not eset.get_read_only()) - - for i in ["save", "saveas"]: - set_action_sensitive(i, save_sens) - - for i in ["upload"]: - set_action_sensitive(i, upload_sens) - - for i in ["cancelq"]: - set_action_sensitive(i, eset is not None and not save_sens) - - for i in ["export", "close", "columns", "irbook", "irfinder", - "move_up", "move_dn", "exchange", "iradioreference", - "cut", "copy", "paste", "delete", "viewdeveloper", - "all", "properties"]: - set_action_sensitive(i, eset is not None) - - def ev_status(self, editorset, msg): - self.sb_radio.pop(0) - self.sb_radio.push(0, msg) - - def ev_usermsg(self, editorset, msg): - self.sb_general.pop(0) - self.sb_general.push(0, msg) - - def ev_editor_selected(self, editorset, editortype): - mappings = { - "memedit": ["view", "edit"], - } - - for _editortype, actions in mappings.items(): - for _action in actions: - action = self.menu_ag.get_action(_action) - action.set_sensitive(editortype.startswith(_editortype)) - - def _connect_editorset(self, eset): - eset.connect("want-close", self.do_close) - eset.connect("status", self.ev_status) - eset.connect("usermsg", self.ev_usermsg) - eset.connect("editor-selected", self.ev_editor_selected) - - def do_diff_radio(self): - if self.tabs.get_n_pages() < 2: - common.show_error("Diff tabs requires at least two open tabs!") - return - - esets = [] - for i in range(0, self.tabs.get_n_pages()): - esets.append(self.tabs.get_nth_page(i)) - - d = gtk.Dialog(title="Diff Radios", - buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, - gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), - parent=self) - - label = gtk.Label("") - label.set_markup("-1 for either Mem # does a full-file hex " + - "dump with diffs highlighted.\n" + - "-2 for first Mem # shows " + - "only the diffs.") - d.vbox.pack_start(label, True, True, 0) - label.show() - - choices = [] - for eset in esets: - choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR, - eset.rthread.radio.MODEL, - eset.filename)) - choice_a = miscwidgets.make_choice(choices, False, choices[0]) - choice_a.show() - chan_a = gtk.SpinButton() - chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0) - chan_a.show() - hbox = gtk.HBox(False, 3) - hbox.pack_start(choice_a, 1, 1, 1) - hbox.pack_start(chan_a, 0, 0, 0) - hbox.show() - d.vbox.pack_start(hbox, 0, 0, 0) - - choice_b = miscwidgets.make_choice(choices, False, choices[1]) - choice_b.show() - chan_b = gtk.SpinButton() - chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0) - chan_b.show() - hbox = gtk.HBox(False, 3) - hbox.pack_start(choice_b, 1, 1, 1) - hbox.pack_start(chan_b, 0, 0, 0) - hbox.show() - d.vbox.pack_start(hbox, 0, 0, 0) - - r = d.run() - sel_a = choice_a.get_active_text() - sel_chan_a = chan_a.get_value() - sel_b = choice_b.get_active_text() - sel_chan_b = chan_b.get_value() - d.destroy() - if r == gtk.RESPONSE_CANCEL: - return - - if sel_a == sel_b: - common.show_error("Can't diff the same tab!") - return - - LOG.debug("Selected %s@%i and %s@%i" % - (sel_a, sel_chan_a, sel_b, sel_chan_b)) - name_a = os.path.basename(sel_a) - name_a = name_a[:name_a.rindex(")")] - name_b = os.path.basename(sel_b) - name_b = name_b[:name_b.rindex(")")] - diffwintitle = "%s@%i diff %s@%i" % ( - name_a, sel_chan_a, name_b, sel_chan_b) - - eset_a = esets[choices.index(sel_a)] - eset_b = esets[choices.index(sel_b)] - - def _show_diff(mem_b, mem_a): - # Step 3: Show the diff - diff = common.simple_diff(mem_a, mem_b) - common.show_diff_blob(diffwintitle, diff) - - def _get_mem_b(mem_a): - # Step 2: Get memory b - job = common.RadioJob(_show_diff, "get_raw_memory", - int(sel_chan_b)) - job.set_cb_args(mem_a) - eset_b.rthread.submit(job) - - if sel_chan_a >= 0 and sel_chan_b >= 0: - # Diff numbered memory - # Step 1: Get memory a - job = common.RadioJob(_get_mem_b, "get_raw_memory", - int(sel_chan_a)) - eset_a.rthread.submit(job) - elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\ - isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio): - # Diff whole (can do this without a job, since both are clone-mode) - try: - addrfmt = CONF.get('hexdump_addrfmt', section='developer', - raw=True) - except: - pass - a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(), - addrfmt=addrfmt) - b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(), - addrfmt=addrfmt) - if sel_chan_a == -2: - diffsonly = True - else: - diffsonly = False - common.show_diff_blob(diffwintitle, - common.simple_diff(a, b, diffsonly)) - else: - common.show_error("Cannot diff whole live-mode radios!") - - def do_new(self): - eset = editorset.EditorSet(_("Untitled") + ".csv", self) - self._connect_editorset(eset) - eset.prime() - eset.show() - - tab = self.tabs.append_page(eset, eset.get_tab_label()) - self.tabs.set_current_page(tab) - - def _do_manual_select(self, filename): - radiolist = {} - for drv, radio in directory.DRV_TO_RADIO.items(): - if not issubclass(radio, chirp_common.CloneModeRadio): - continue - radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv - - lab = gtk.Label("""Unable to detect model! - -If you think that it is valid, you can select a radio model below to -force an open attempt. If selecting the model manually works, please -file a bug on the website and attach your image. If selecting the model -does not work, it is likely that you are trying to open some other type -of file. -""") - - lab.set_justify(gtk.JUSTIFY_FILL) - lab.set_line_wrap(True) - lab.set_use_markup(True) - lab.show() - choice = miscwidgets.make_choice(sorted(radiolist.keys()), False, - sorted(radiolist.keys())[0]) - d = gtk.Dialog(title="Detection Failed", - buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, - gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) - d.vbox.pack_start(lab, 0, 0, 0) - d.vbox.pack_start(choice, 0, 0, 0) - d.vbox.set_spacing(5) - choice.show() - d.set_default_size(400, 200) - # d.set_resizable(False) - r = d.run() - d.destroy() - if r != gtk.RESPONSE_OK: - return - try: - rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]] - return rc(filename) - except: - return - - def do_open(self, fname=None, tempname=None): - if not fname: - types = [(_("All files") + " (*.*)", "*"), - (_("CHIRP Radio Images") + " (*.img)", "*.img"), - (_("CHIRP Files") + " (*.chirp)", "*.chirp"), - (_("CSV Files") + " (*.csv)", "*.csv"), - (_("DAT Files") + " (*.dat)", "*.dat"), - (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), - (_("ICF Files") + " (*.icf)", "*.icf"), - (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), - (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), - (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"), - ] - fname = platform.get_platform().gui_open_file(types=types) - if not fname: - return - - self.record_recent_file(fname) - - if icf.is_icf_file(fname): - a = common.ask_yesno_question( - _("ICF files cannot be edited, only displayed or imported " - "into another file. Open in read-only mode?"), - self) - if not a: - return - read_only = True - else: - read_only = False - - if icf.is_9x_icf(fname): - # We have to actually instantiate the IC9xICFRadio to get its - # sub-devices - radio = ic9x_icf.IC9xICFRadio(fname) - else: - try: - radio = directory.get_radio_by_image(fname) - except errors.ImageDetectFailed: - radio = self._do_manual_select(fname) - if not radio: - return - LOG.debug("Manually selected %s" % radio) - except Exception, e: - common.log_exception() - common.show_error(os.path.basename(fname) + ": " + str(e)) - return - - first_tab = False - try: - eset = editorset.EditorSet(radio, self, - filename=fname, - tempname=tempname) - except Exception, e: - common.log_exception() - common.show_error( - _("There was an error opening {fname}: {error}").format( - fname=fname, - error=e)) - return - - eset.set_read_only(read_only) - self._connect_editorset(eset) - eset.show() - self.tabs.append_page(eset, eset.get_tab_label()) - - if hasattr(eset.rthread.radio, "errors") and \ - eset.rthread.radio.errors: - msg = _("{num} errors during open:").format( - num=len(eset.rthread.radio.errors)) - common.show_error_text(msg, - "\r\n".join(eset.rthread.radio.errors)) - - def do_live_warning(self, radio): - d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) - d.set_markup("" + _("Note:") + "") - msg = _("The {vendor} {model} operates in live mode. " - "This means that any changes you make are immediately sent " - "to the radio. Because of this, you cannot perform the " - "Save or Upload operations. If you wish to " - "edit the contents offline, please Export to a CSV " - "file, using the File menu.") - msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL) - d.format_secondary_markup(msg) - - again = gtk.CheckButton(_("Don't show this again")) - again.show() - d.vbox.pack_start(again, 0, 0, 0) - d.run() - CONF.set_bool("live_mode", again.get_active(), "noconfirm") - d.destroy() - - def do_open_live(self, radio, tempname=None, read_only=False): - eset = editorset.EditorSet(radio, self, tempname=tempname) - eset.connect("want-close", self.do_close) - eset.connect("status", self.ev_status) - eset.set_read_only(read_only) - eset.show() - self.tabs.append_page(eset, eset.get_tab_label()) - - if isinstance(radio, chirp_common.LiveRadio): - reporting.report_model_usage(radio, "live", True) - if not CONF.get_bool("live_mode", "noconfirm"): - self.do_live_warning(radio) - - def do_save(self, eset=None): - if not eset: - eset = self.get_current_editorset() - - # For usability, allow Ctrl-S to short-circuit to Save-As if - # we are working on a yet-to-be-saved image - if not os.path.exists(eset.filename): - return self.do_saveas() - - eset.save() - - def do_saveas(self): - eset = self.get_current_editorset() - - label = _("{vendor} {model} image file").format( - vendor=eset.radio.VENDOR, - model=eset.radio.MODEL) - - defname_format = CONF.get("default_filename", "global") or \ - "{vendor}_{model}_{date}" - defname = defname_format.format( - vendor=eset.radio.VENDOR, - model=eset.radio.MODEL, - date=datetime.now().strftime('%Y%m%d') - ).replace('/', '_') - - types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION, - eset.radio.FILE_EXTENSION)] - - if isinstance(eset.radio, vx7.VX7Radio): - types += [(_("VX7 Commander") + " (*.vx7)", "vx7")] - elif isinstance(eset.radio, vx6.VX6Radio): - types += [(_("VX6 Commander") + " (*.vx6)", "vx6")] - elif isinstance(eset.radio, vx5.VX5Radio): - types += [(_("EVE") + " (*.eve)", "eve")] - types += [(_("VX5 Commander") + " (*.vx5)", "vx5")] - - while True: - fname = platform.get_platform().gui_save_file(default_name=defname, - types=types) - if not fname: - return - - if os.path.exists(fname): - dlg = inputdialog.OverwriteDialog(fname) - owrite = dlg.run() - dlg.destroy() - if owrite == gtk.RESPONSE_OK: - break - else: - break - - try: - eset.save(fname) - except Exception, e: - d = inputdialog.ExceptionDialog(e) - d.run() - d.destroy() - - def cb_clonein(self, radio, emsg=None): - radio.pipe.close() - reporting.report_model_usage(radio, "download", bool(emsg)) - if not emsg: - self.do_open_live(radio, tempname="(" + _("Untitled") + ")") - else: - d = inputdialog.ExceptionDialog(emsg) - d.run() - d.destroy() - - def cb_cloneout(self, radio, emsg=None): - radio.pipe.close() - reporting.report_model_usage(radio, "upload", True) - if emsg: - d = inputdialog.ExceptionDialog(emsg) - d.run() - d.destroy() - - def _get_recent_list(self): - recent = [] - for i in range(0, KEEP_RECENT): - fn = CONF.get("recent%i" % i, "state") - if fn: - recent.append(fn) - return recent - - def _set_recent_list(self, recent): - for fn in recent: - CONF.set("recent%i" % recent.index(fn), fn, "state") - - def update_recent_files(self): - i = 0 - for fname in self._get_recent_list(): - action_name = "recent%i" % i - path = "/MenuBar/file/recent" - - old_action = self.menu_ag.get_action(action_name) - if old_action: - self.menu_ag.remove_action(old_action) - - file_basename = os.path.basename(fname).replace("_", "__") - action = gtk.Action( - action_name, "_%i. %s" % (i + 1, file_basename), - _("Open recent file {name}").format(name=fname), "") - action.connect("activate", lambda a, f: self.do_open(f), fname) - mid = self.menu_uim.new_merge_id() - self.menu_uim.add_ui(mid, path, - action_name, action_name, - gtk.UI_MANAGER_MENUITEM, False) - self.menu_ag.add_action(action) - i += 1 - - def record_recent_file(self, filename): - - recent_files = self._get_recent_list() - if filename not in recent_files: - if len(recent_files) == KEEP_RECENT: - del recent_files[-1] - recent_files.insert(0, filename) - self._set_recent_list(recent_files) - - self.update_recent_files() - - def import_stock_config(self, action, config): - eset = self.get_current_editorset() - count = eset.do_import(config) - - def copy_shipped_stock_configs(self, stock_dir): - basepath = platform.get_platform().find_resource("stock_configs") - - files = glob(os.path.join(basepath, "*.csv")) - for fn in files: - if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))): - LOG.info("Skipping existing stock config") - continue - try: - shutil.copy(fn, stock_dir) - LOG.debug("Copying %s -> %s" % (fn, stock_dir)) - except Exception, e: - LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e)) - return False - return True - - def update_stock_configs(self): - stock_dir = platform.get_platform().config_file("stock_configs") - if not os.path.isdir(stock_dir): - try: - os.mkdir(stock_dir) - except Exception, e: - LOG.error("Unable to create directory: %s" % stock_dir) - return - if not self.copy_shipped_stock_configs(stock_dir): - return - - def _do_import_action(config): - name = os.path.splitext(os.path.basename(config))[0] - action_name = "stock-%i" % configs.index(config) - path = "/MenuBar/radio/stock" - action = gtk.Action(action_name, - name, - _("Import stock " - "configuration {name}").format(name=name), - "") - action.connect("activate", self.import_stock_config, config) - mid = self.menu_uim.new_merge_id() - mid = self.menu_uim.add_ui(mid, path, - action_name, action_name, - gtk.UI_MANAGER_MENUITEM, False) - self.menu_ag.add_action(action) - - def _do_open_action(config): - name = os.path.splitext(os.path.basename(config))[0] - action_name = "openstock-%i" % configs.index(config) - path = "/MenuBar/file/openstock" - action = gtk.Action(action_name, - name, - _("Open stock " - "configuration {name}").format(name=name), - "") - action.connect("activate", lambda a, c: self.do_open(c), config) - mid = self.menu_uim.new_merge_id() - mid = self.menu_uim.add_ui(mid, path, - action_name, action_name, - gtk.UI_MANAGER_MENUITEM, False) - self.menu_ag.add_action(action) - - configs = glob(os.path.join(stock_dir, "*.csv")) - for config in configs: - _do_import_action(config) - _do_open_action(config) - - def _confirm_experimental(self, rclass): - sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass) - if CONF.is_defined(sql_key, "state") and \ - not CONF.get_bool(sql_key, "state"): - return True - - title = _("Proceed with experimental driver?") - text = rclass.get_prompts().experimental - msg = _("This radio's driver is experimental. " - "Do you want to proceed?") - resp, squelch = common.show_warning(msg, text, - title=title, - buttons=gtk.BUTTONS_YES_NO, - can_squelch=True) - if resp == gtk.RESPONSE_YES: - CONF.set_bool(sql_key, not squelch, "state") - return resp == gtk.RESPONSE_YES - - def _show_instructions(self, radio, message): - if message is None: - return - - if CONF.get_bool("clone_instructions", "noconfirm"): - return - - d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) - d.set_markup("" + _("{name} Instructions").format( - name=radio.get_name()) + "") - msg = _("{instructions}").format(instructions=message) - d.format_secondary_markup(msg) - - again = gtk.CheckButton( - _("Don't show instructions for any radio again")) - again.show() - again.connect("toggled", lambda action: - self.clonemenu.set_active(not action.get_active())) - d.vbox.pack_start(again, 0, 0, 0) - h_button_box = d.vbox.get_children()[2] - try: - ok_button = h_button_box.get_children()[0] - ok_button.grab_default() - ok_button.grab_focus() - except AttributeError: - # don't grab focus on GTK+ 2.0 - pass - d.run() - d.destroy() - - def do_download(self, port=None, rtype=None): - d = clone.CloneSettingsDialog(parent=self) - settings = d.run() - d.destroy() - if not settings: - return - - rclass = settings.radio_class - if issubclass(rclass, chirp_common.ExperimentalRadio) and \ - not self._confirm_experimental(rclass): - # User does not want to proceed with experimental driver - return - - self._show_instructions(rclass, rclass.get_prompts().pre_download) - - LOG.debug("User selected %s %s on port %s" % - (rclass.VENDOR, rclass.MODEL, settings.port)) - - try: - ser = serial.Serial(port=settings.port, - baudrate=rclass.BAUD_RATE, - rtscts=rclass.HARDWARE_FLOW, - timeout=0.25) - ser.flushInput() - except serial.SerialException, e: - d = inputdialog.ExceptionDialog(e) - d.run() - d.destroy() - return - - radio = settings.radio_class(ser) - - fn = tempfile.mktemp() - if isinstance(radio, chirp_common.CloneModeRadio): - ct = clone.CloneThread(radio, "in", cb=self.cb_clonein, - parent=self) - ct.start() - else: - self.do_open_live(radio) - - def do_upload(self, port=None, rtype=None): - eset = self.get_current_editorset() - radio = eset.radio - - settings = clone.CloneSettings() - settings.radio_class = radio.__class__ - - d = clone.CloneSettingsDialog(settings, parent=self) - settings = d.run() - d.destroy() - if not settings: - return - prompts = radio.get_prompts() - - if prompts.display_pre_upload_prompt_before_opening_port is True: - LOG.debug("Opening port after pre_upload prompt.") - self._show_instructions(radio, prompts.pre_upload) - - if isinstance(radio, chirp_common.ExperimentalRadio) and \ - not self._confirm_experimental(radio.__class__): - # User does not want to proceed with experimental driver - return - - try: - ser = serial.Serial(port=settings.port, - baudrate=radio.BAUD_RATE, - rtscts=radio.HARDWARE_FLOW, - timeout=0.25) - ser.flushInput() - except serial.SerialException, e: - d = inputdialog.ExceptionDialog(e) - d.run() - d.destroy() - return - - if prompts.display_pre_upload_prompt_before_opening_port is False: - LOG.debug("Opening port before pre_upload prompt.") - self._show_instructions(radio, prompts.pre_upload) - - radio.set_pipe(ser) - - ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self) - ct.start() - - def do_close(self, tab_child=None): - if tab_child: - eset = tab_child - else: - eset = self.get_current_editorset() - - if not eset: - return False - - if eset.is_modified(): - dlg = miscwidgets.YesNoDialog( - title=_("Save Changes?"), parent=self, - buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES, - gtk.STOCK_NO, gtk.RESPONSE_NO, - gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) - dlg.set_text(_("File is modified, save changes before closing?")) - res = dlg.run() - dlg.destroy() - - if res == gtk.RESPONSE_YES: - self.do_save(eset) - elif res != gtk.RESPONSE_NO: - raise ModifiedError() - - eset.rthread.stop() - eset.rthread.join() - - eset.prepare_close() - - if eset.radio.pipe: - eset.radio.pipe.close() - - if isinstance(eset.radio, chirp_common.LiveRadio): - action = self.menu_ag.get_action("openlive") - if action: - action.set_sensitive(True) - - page = self.tabs.page_num(eset) - if page is not None: - self.tabs.remove_page(page) - - return True - - def do_import(self): - types = [(_("All files") + " (*.*)", "*"), - (_("CHIRP Files") + " (*.chirp)", "*.chirp"), - (_("CHIRP Radio Images") + " (*.img)", "*.img"), - (_("CSV Files") + " (*.csv)", "*.csv"), - (_("DAT Files") + " (*.dat)", "*.dat"), - (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), - (_("ICF Files") + " (*.icf)", "*.icf"), - (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"), - (_("Kenwood ITM Files") + " (*.itm)", "*.itm"), - (_("Travel Plus Files") + " (*.tpe)", "*.tpe"), - (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), - (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), - (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")] - filen = platform.get_platform().gui_open_file(types=types) - if not filen: - return - - eset = self.get_current_editorset() - count = eset.do_import(filen) - reporting.report_model_usage(eset.rthread.radio, "import", count > 0) - - 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("RepeaterBook\r\n" + - "North American Repeater Directory") - d.format_secondary_markup("For more information about this " + - "free service, please go to\r\n" + - "http://www.repeaterbook.com") - d.run() - d.destroy() - CONF.set_bool("has_seen_credit", True, "repeaterbook") - - default_state = "Oregon" - default_county = "--All--" - default_band = "--All--" - try: - try: - code = int(CONF.get("state", "repeaterbook")) - except: - code = CONF.get("state", "repeaterbook") - for k, v in fips.FIPS_STATES.items(): - if code == v: - default_state = k - break - - code = CONF.get("county", "repeaterbook") - items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items() - for k, v in items: - if code == v: - default_county = k - break - - code = int(CONF.get("band", "repeaterbook")) - for k, v in RB_BANDS.items(): - if code == v: - default_band = k - break - except: - pass - - state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()), - False, default_state) - county = miscwidgets.make_choice( - sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()), - False, default_county) - band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands), - False, default_band) - - def _changed(box, county): - state = fips.FIPS_STATES[box.get_active_text()] - county.get_model().clear() - for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()): - county.append_text(fips_county) - county.set_active(0) - - state.connect("changed", _changed, county) - - d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self) - d.add_field("State", state) - d.add_field("County", county) - d.add_field("Band", band) - - r = d.run() - d.destroy() - if r != gtk.RESPONSE_OK: - return False - - code = fips.FIPS_STATES[state.get_active_text()] - county_id = fips.FIPS_COUNTIES[code][county.get_active_text()] - freq = RB_BANDS[band.get_active_text()] - CONF.set("state", str(code), "repeaterbook") - CONF.set("county", str(county_id), "repeaterbook") - CONF.set("band", str(freq), "repeaterbook") - - return True - - def do_repeaterbook_political(self, do_import): - self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) - if not self.do_repeaterbook_political_prompt(): - self.window.set_cursor(None) - return - - try: - code = "%02i" % int(CONF.get("state", "repeaterbook")) - except: - try: - code = CONF.get("state", "repeaterbook") - except: - code = '41' # Oregon default - - try: - county = CONF.get("county", "repeaterbook") - except: - county = '%' # --All-- default - - try: - band = int(CONF.get("band", "repeaterbook")) - except: - band = 14 # 2m default - - query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \ - "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \ - "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%" - 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 - gtk.gdk.window_process_all_updates() - while gtk.events_pending(): - gtk.main_iteration(False) - - 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 = 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 = '%' - - 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) - - 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 = 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_przemienniki_prompt(self): - d = inputdialog.FieldDialog(title='przemienniki.net query', - parent=self) - fields = { - "Country": - (miscwidgets.make_choice( - ['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi', - 'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se', - 'sk', 'ua', 'uk'], False), - lambda x: str(x.get_active_text())), - "Band": - (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm', - '23cm', '13cm', '3cm'], False, '2m'), - lambda x: str(x.get_active_text())), - "Mode": - (miscwidgets.make_choice(['fm', 'dv'], False), - lambda x: str(x.get_active_text())), - "Only Working": - (miscwidgets.make_choice(['', 'yes'], False), - lambda x: str(x.get_active_text())), - "Latitude": (gtk.Entry(), lambda x: float(x.get_text())), - "Longitude": (gtk.Entry(), lambda x: float(x.get_text())), - "Range": (gtk.Entry(), lambda x: int(x.get_text())), - } - for name in sorted(fields.keys()): - value, fn = fields[name] - d.add_field(name, value) - while d.run() == gtk.RESPONSE_OK: - query = "http://przemienniki.net/export/chirp.csv?" - args = [] - for name, (value, fn) in fields.items(): - if isinstance(value, gtk.Entry): - contents = value.get_text() - else: - contents = value.get_active_text() - if contents: - try: - _value = fn(value) - except ValueError: - common.show_error(_("Invalid value for %s") % name) - query = None - continue - - args.append("=".join((name.replace(" ", "").lower(), - contents))) - query += "&".join(args) - LOG.debug(query) - d.destroy() - return query - - d.destroy() - return query - - def do_przemienniki(self, do_import): - url = self.do_przemienniki_prompt() - if not url: - return - - fn = tempfile.mktemp(".csv") - filename, headers = urllib.urlretrieve(url, fn) - if not os.path.exists(filename): - LOG.error("Failed, headers were: %s", str(headers)) - common.show_error(_("Query failed")) - return - - class PRRadio(generic_csv.CSVRadio, - chirp_common.NetworkSourceRadio): - VENDOR = "przemienniki.net" - MODEL = "" - - try: - radio = PRRadio(filename) - except Exception, e: - common.show_error(str(e)) - return - - if do_import: - eset = self.get_current_editorset() - count = eset.do_import(filename) - else: - self.do_open_live(radio, read_only=True) - - def do_rfinder_prompt(self): - fields = {"1Email": (gtk.Entry(), lambda x: "@" in x), - "2Password": (gtk.Entry(), lambda x: x), - "3Latitude": (gtk.Entry(), - lambda x: float(x) < 90 and float(x) > -90), - "4Longitude": (gtk.Entry(), - lambda x: float(x) < 180 and float(x) > -180), - "5Range_in_Miles": (gtk.Entry(), - lambda x: int(x) > 0 and int(x) < 5000), - } - - d = inputdialog.FieldDialog(title="RFinder Login", parent=self) - for k in sorted(fields.keys()): - d.add_field(k[1:].replace("_", " "), fields[k][0]) - fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "") - fields[k][0].set_visibility(k != "2Password") - - while d.run() == gtk.RESPONSE_OK: - valid = True - for k in sorted(fields.keys()): - widget, validator = fields[k] - try: - if validator(widget.get_text()): - CONF.set(k[1:], widget.get_text(), "rfinder") - continue - except Exception: - 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_rfinder(self, do_import): - self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) - if not self.do_rfinder_prompt(): - self.window.set_cursor(None) - return - - lat = CONF.get_float("Latitude", "rfinder") - lon = CONF.get_float("Longitude", "rfinder") - passwd = CONF.get("Password", "rfinder") - email = CONF.get("Email", "rfinder") - miles = CONF.get_int("Range_in_Miles", "rfinder") - - # 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() - rfstr = "rfinder://%s/%s/%f/%f/%i" % \ - (email, passwd, lat, lon, miles) - count = eset.do_import(rfstr) - else: - from chirp.drivers import rfinder - radio = rfinder.RFinderRadio(None) - radio.set_params((lat, lon), miles, email, passwd) - self.do_open_live(radio, read_only=True) - - self.window.set_cursor(None) - - def do_radioreference_prompt(self): - fields = {"1Username": (gtk.Entry(), lambda x: x), - "2Password": (gtk.Entry(), lambda x: x), - "3Zipcode": (gtk.Entry(), lambda x: x), - } - - d = inputdialog.FieldDialog(title=_("RadioReference.com Query"), - 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:], "radioreference") or "") - fields[k][0].set_visibility(k != "2Password") - - while d.run() == gtk.RESPONSE_OK: - valid = True - for k in sorted(fields.keys()): - widget, validator = fields[k] - try: - if validator(widget.get_text()): - CONF.set(k[1:], widget.get_text(), "radioreference") - continue - except Exception: - 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_radioreference(self, do_import): - self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) - if not self.do_radioreference_prompt(): - self.window.set_cursor(None) - return - - username = CONF.get("Username", "radioreference") - passwd = CONF.get("Password", "radioreference") - zipcode = CONF.get("Zipcode", "radioreference") - - # 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() - rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd) - count = eset.do_import(rrstr) - else: - try: - from chirp import radioreference - radio = radioreference.RadioReferenceRadio(None) - radio.set_params(zipcode, username, passwd) - self.do_open_live(radio, read_only=True) - except errors.RadioError, e: - common.show_error(e) - - self.window.set_cursor(None) - - def do_export(self): - types = [(_("CSV Files") + " (*.csv)", "csv"), - ] - - eset = self.get_current_editorset() - - if os.path.exists(eset.filename): - base = os.path.basename(eset.filename) - if "." in base: - base = base[:base.rindex(".")] - defname = base - else: - defname = "radio" - - filen = platform.get_platform().gui_save_file(default_name=defname, - types=types) - if not filen: - return - - if os.path.exists(filen): - dlg = inputdialog.OverwriteDialog(filen) - owrite = dlg.run() - dlg.destroy() - if owrite != gtk.RESPONSE_OK: - return - os.remove(filen) - - count = eset.do_export(filen) - reporting.report_model_usage(eset.rthread.radio, "export", count > 0) - - def do_about(self): - d = gtk.AboutDialog() - d.set_transient_for(self) - import sys - verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % ( - ".".join([str(x) for x in gtk.gtk_version]), - ".".join([str(x) for x in gtk.pygtk_version]), - sys.version.split()[0]) - - # Set url hook to handle user activating a URL link in the about dialog - gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url)) - - d.set_name("CHIRP") - d.set_version(CHIRP_VERSION) - d.set_copyright("Copyright 2015 Dan Smith (KK7DS)") - d.set_website("http://chirp.danplanet.com") - d.set_authors(("Dan Smith KK7DS ", - _("With significant contributions from:"), - "Tom KD7LXL", - "Marco IZ3GME", - "Jim KC9HI" - )) - d.set_translator_credits("Polish: Grzegorz SQ2RBY" + - os.linesep + - "Italian: Fabio IZ2QDH" + - os.linesep + - "Dutch: Michael PD4MT" + - os.linesep + - "German: Benjamin HB9EUK" + - os.linesep + - "Hungarian: Attila HA5JA" + - os.linesep + - "Russian: Dmitry Slukin" + - os.linesep + - "Portuguese (BR): Crezivando PP7CJ") - d.set_comments(verinfo) - - d.run() - d.destroy() - - def do_gethelp(self): - webbrowser.open("http://chirp.danplanet.com") - - def do_columns(self): - eset = self.get_current_editorset() - driver = directory.get_driver(eset.rthread.radio.__class__) - radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR, - eset.rthread.radio.MODEL, - eset.rthread.radio.VARIANT) - d = gtk.Dialog(title=_("Select Columns"), - parent=self, - buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, - gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) - - vbox = gtk.VBox() - vbox.show() - sw = gtk.ScrolledWindow() - sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - sw.add_with_viewport(vbox) - sw.show() - d.vbox.pack_start(sw, 1, 1, 1) - d.set_size_request(-1, 300) - d.set_resizable(False) - - labelstr = _("Visible columns for {radio}").format(radio=radio_name) - label = gtk.Label(labelstr) - label.show() - vbox.pack_start(label) - - fields = [] - memedit = eset.get_current_editor() # .editors["memedit"] - unsupported = memedit.get_unsupported_columns() - for colspec in memedit.cols: - if colspec[0].startswith("_"): - continue - elif colspec[0] in unsupported: - continue - label = colspec[0] - visible = memedit.get_column_visible(memedit.col(label)) - widget = gtk.CheckButton(label) - widget.set_active(visible) - fields.append(widget) - vbox.pack_start(widget, 1, 1, 1) - widget.show() - - res = d.run() - selected_columns = [] - if res == gtk.RESPONSE_OK: - for widget in fields: - colnum = memedit.col(widget.get_label()) - memedit.set_column_visible(colnum, widget.get_active()) - if widget.get_active(): - selected_columns.append(widget.get_label()) - - d.destroy() - - CONF.set(driver, ",".join(selected_columns), "memedit_columns") - - def do_hide_unused(self, action): - eset = self.get_current_editorset() - if eset is None: - conf = config.get("memedit") - conf.set_bool("hide_unused", action.get_active()) - else: - for editortype, editor in eset.editors.iteritems(): - if "memedit" in editortype: - editor.set_hide_unused(action.get_active()) - - def do_clearq(self): - eset = self.get_current_editorset() - eset.rthread.flush() - - def do_copy(self, cut): - eset = self.get_current_editorset() - eset.get_current_editor().copy_selection(cut) - - def do_paste(self): - eset = self.get_current_editorset() - eset.get_current_editor().paste_selection() - - def do_delete(self): - eset = self.get_current_editorset() - eset.get_current_editor().copy_selection(True) - - def do_toggle_report(self, action): - if not action.get_active(): - d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self) - markup = "" + _("Reporting is disabled") + "" - d.set_markup(markup) - msg = _("The reporting feature of CHIRP is designed to help " - "improve quality by allowing the authors to focus " - "on the radio drivers used most often and errors " - "experienced by the users. The reports contain no " - "identifying information and are used only for " - "statistical purposes by the authors. Your privacy is " - "extremely important, but please consider leaving " - "this feature enabled to help make CHIRP better!\n\n" - "Are you sure you want to disable this feature?") - d.format_secondary_markup(msg.replace("\n", "\r\n")) - r = d.run() - d.destroy() - if r == gtk.RESPONSE_NO: - action.set_active(not action.get_active()) - - conf = config.get() - conf.set_bool("no_report", not action.get_active()) - - def do_toggle_no_smart_tmode(self, action): - CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit") - - def do_toggle_developer(self, action): - conf = config.get() - conf.set_bool("developer", action.get_active(), "state") - - for name in ["viewdeveloper", "loadmod"]: - devaction = self.menu_ag.get_action(name) - devaction.set_visible(action.get_active()) - - def do_toggle_clone_instructions(self, action): - CONF.set_bool("clone_instructions", - not action.get_active(), "noconfirm") - - def do_change_language(self): - langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German", - "Hungarian", "Russian", "Portuguese (BR)", "French", - "Spanish"] - d = inputdialog.ChoiceDialog(langs, parent=self, - title="Choose Language") - d.label.set_text(_("Choose a language or Auto to use the " - "operating system default. You will need to " - "restart the application before the change " - "will take effect")) - d.label.set_line_wrap(True) - r = d.run() - if r == gtk.RESPONSE_OK: - LOG.debug("Chose language %s" % d.choice.get_active_text()) - conf = config.get() - conf.set("language", d.choice.get_active_text(), "state") - d.destroy() - - def load_module(self): - types = [(_("Python Modules") + "*.py", "*.py")] - filen = platform.get_platform().gui_open_file(types=types) - if not filen: - return - - # We're in development mode, so we need to tell the directory to - # allow a loaded module to override an existing driver, against - # its normal better judgement - directory.enable_reregistrations() - - try: - module = file(filen) - code = module.read() - module.close() - pyc = compile(code, filen, 'exec') - # See this for why: - # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec - exec(pyc, globals(), globals()) - except Exception, e: - common.log_exception() - common.show_error("Unable to load module: %s" % e) - - def mh(self, _action, *args): - action = _action.get_name() - - if action == "quit": - gtk.main_quit() - elif action == "new": - self.do_new() - elif action == "open": - self.do_open() - elif action == "save": - self.do_save() - elif action == "saveas": - self.do_saveas() - elif action.startswith("download"): - self.do_download(*args) - elif action.startswith("upload"): - self.do_upload(*args) - elif action == "close": - 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 ["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": - self.do_about() - elif action == "gethelp": - self.do_gethelp() - elif action == "columns": - self.do_columns() - elif action == "hide_unused": - self.do_hide_unused(_action) - elif action == "cancelq": - self.do_clearq() - elif action == "report": - self.do_toggle_report(_action) - elif action == "channel_defaults": - # The memedit thread also has an instance of bandplans. - bp = bandplans.BandPlans(CONF) - bp.select_bandplan(self) - elif action == "no_smart_tmode": - self.do_toggle_no_smart_tmode(_action) - elif action == "developer": - self.do_toggle_developer(_action) - elif action == "clone_instructions": - self.do_toggle_clone_instructions(_action) - elif action in ["cut", "copy", "paste", "delete", - "move_up", "move_dn", "exchange", "all", - "devshowraw", "devdiffraw", "properties"]: - self.get_current_editorset().get_current_editor().hotkey(_action) - elif action == "devdifftab": - self.do_diff_radio() - elif action == "language": - self.do_change_language() - elif action == "loadmod": - self.load_module() - else: - return - - self.ev_tab_switched() - - def make_menubar(self): - menu_xml = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -""" - ALT_KEY = "" - CTRL_KEY = "" - if sys.platform == 'darwin': - ALT_KEY = "" - CTRL_KEY = "" - actions = [ - ('file', None, _("_File"), None, None, self.mh), - ('new', gtk.STOCK_NEW, None, None, None, self.mh), - ('open', gtk.STOCK_OPEN, None, None, None, self.mh), - ('openstock', None, _("Open stock config"), None, None, self.mh), - ('recent', None, _("_Recent"), None, None, self.mh), - ('save', gtk.STOCK_SAVE, None, None, None, self.mh), - ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh), - ('loadmod', None, _("Load Module"), None, None, self.mh), - ('close', gtk.STOCK_CLOSE, None, None, None, self.mh), - ('quit', gtk.STOCK_QUIT, None, None, None, self.mh), - ('edit', None, _("_Edit"), None, None, self.mh), - ('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh), - ('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh), - ('paste', None, _("_Paste"), - "%sv" % CTRL_KEY, None, self.mh), - ('delete', None, _("_Delete"), "Delete", None, self.mh), - ('all', None, _("Select _All"), None, None, self.mh), - ('move_up', None, _("Move _Up"), - "%sUp" % CTRL_KEY, None, self.mh), - ('move_dn', None, _("Move Dow_n"), - "%sDown" % CTRL_KEY, None, self.mh), - ('exchange', None, _("E_xchange"), - "%sx" % CTRL_KEY, None, self.mh), - ('properties', None, _("P_roperties"), None, None, self.mh), - ('view', None, _("_View"), None, None, self.mh), - ('columns', None, _("Columns"), None, None, self.mh), - ('viewdeveloper', None, _("Developer"), None, None, self.mh), - ('devshowraw', None, _('Show raw memory'), - "%sr" % CTRL_KEY, None, self.mh), - ('devdiffraw', None, _("Diff raw memories"), - "%sd" % CTRL_KEY, None, self.mh), - ('devdifftab', None, _("Diff tabs"), - "%st" % CTRL_KEY, None, self.mh), - ('language', None, _("Change language"), None, None, self.mh), - ('radio', None, _("_Radio"), None, None, self.mh), - ('download', None, _("Download From Radio"), - "%sd" % ALT_KEY, None, self.mh), - ('upload', None, _("Upload To Radio"), - "%su" % ALT_KEY, None, self.mh), - ('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh), - ('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), - ('stock', None, _("Import from stock config"), - None, None, self.mh), - ('channel_defaults', None, _("Channel defaults"), - None, None, self.mh), - ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh), - ('help', None, _('Help'), None, None, self.mh), - ('about', gtk.STOCK_ABOUT, None, None, None, self.mh), - ('gethelp', None, _("Get Help Online..."), None, None, self.mh), - ] - - conf = config.get() - re = not conf.get_bool("no_report") - hu = conf.get_bool("hide_unused", "memedit", default=True) - dv = conf.get_bool("developer", "state") - ci = not conf.get_bool("clone_instructions", "noconfirm") - st = not conf.get_bool("no_smart_tmode", "memedit") - - toggles = [('report', None, _("Report Statistics"), - None, None, self.mh, re), - ('hide_unused', None, _("Hide Unused Fields"), - None, None, self.mh, hu), - ('no_smart_tmode', None, _("Smart Tone Modes"), - None, None, self.mh, st), - ('clone_instructions', None, _("Show Instructions"), - None, None, self.mh, ci), - ('developer', None, _("Enable Developer Functions"), - None, None, self.mh, dv), - ] - - self.menu_uim = gtk.UIManager() - self.menu_ag = gtk.ActionGroup("MenuBar") - self.menu_ag.add_actions(actions) - self.menu_ag.add_toggle_actions(toggles) - - self.menu_uim.insert_action_group(self.menu_ag, 0) - self.menu_uim.add_ui_from_string(menu_xml) - - self.add_accel_group(self.menu_uim.get_accel_group()) - - self.clonemenu = self.menu_uim.get_widget( - "/MenuBar/help/clone_instructions") - - # Initialize - self.do_toggle_developer(self.menu_ag.get_action("developer")) - - return self.menu_uim.get_widget("/MenuBar") - - def make_tabs(self): - self.tabs = gtk.Notebook() - self.tabs.set_scrollable(True) - - return self.tabs - - def close_out(self): - num = self.tabs.get_n_pages() - while num > 0: - num -= 1 - LOG.debug("Closing %i" % num) - try: - self.do_close(self.tabs.get_nth_page(num)) - except ModifiedError: - return False - - gtk.main_quit() - - return True - - def make_status_bar(self): - box = gtk.HBox(False, 2) - - self.sb_general = gtk.Statusbar() - self.sb_general.set_has_resize_grip(False) - self.sb_general.show() - box.pack_start(self.sb_general, 1, 1, 1) - - self.sb_radio = gtk.Statusbar() - self.sb_radio.set_has_resize_grip(True) - self.sb_radio.show() - box.pack_start(self.sb_radio, 1, 1, 1) - - box.show() - return box - - def ev_delete(self, window, event): - if not self.close_out(): - return True # Don't exit - - def ev_destroy(self, window): - if not self.close_out(): - return True # Don't exit - - def setup_extra_hotkeys(self): - accelg = self.menu_uim.get_accel_group() - - def memedit(a): - self.get_current_editorset().editors["memedit"].hotkey(a) - - actions = [ - # ("action_name", "key", function) - ] - - for name, key, fn in actions: - a = gtk.Action(name, name, name, "") - a.connect("activate", fn) - self.menu_ag.add_action_with_accel(a, key) - a.set_accel_group(accelg) - a.connect_accelerator() - - def _set_icon(self): - this_platform = platform.get_platform() - path = (this_platform.find_resource("chirp.png") or - this_platform.find_resource(os.path.join("pixmaps", - "chirp.png"))) - if os.path.exists(path): - self.set_icon_from_file(path) - else: - LOG.warn("Icon %s not found" % path) - - def _updates(self, version): - if not version: - return - - if version == CHIRP_VERSION: - return - - LOG.info("Server reports version %s is available" % version) - - # Report new updates every three days - intv = 3600 * 24 * 3 - - if CONF.is_defined("last_update_check", "state") and \ - (time.time() - CONF.get_int("last_update_check", "state")) < intv: - return - - CONF.set_int("last_update_check", int(time.time()), "state") - d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=self, - type=gtk.MESSAGE_INFO) - d.label.set_markup( - _('A new version of CHIRP is available: ' + - '{ver}. '.format(ver=version) + - 'It is recommended that you upgrade as soon as possible. ' - 'Please go to: \r\n\r\n' + - 'http://chirp.danplanet.com')) - response = d.run() - d.destroy() - if response == gtk.RESPONSE_OK: - webbrowser.open('http://chirp.danplanet.com/' - 'projects/chirp/wiki/Download') - - def _init_macos(self, menu_bar): - macapp = None - - # for KK7DS runtime <= R10 - try: - import gtk_osxapplication - macapp = gtk_osxapplication.OSXApplication() - except ImportError: - pass - - # for gtk-mac-integration >= 2.0.7 - try: - import gtkosx_application - macapp = gtkosx_application.Application() - except ImportError: - pass - - if macapp is None: - LOG.error("No MacOS support: %s" % e) - return - - this_platform = platform.get_platform() - icon = (this_platform.find_resource("chirp.png") or - this_platform.find_resource(os.path.join("pixmaps", - "chirp.png"))) - if os.path.exists(icon): - icon_pixmap = gtk.gdk.pixbuf_new_from_file(icon) - macapp.set_dock_icon_pixbuf(icon_pixmap) - - menu_bar.hide() - macapp.set_menu_bar(menu_bar) - - quititem = self.menu_uim.get_widget("/MenuBar/file/quit") - quititem.hide() - - aboutitem = self.menu_uim.get_widget("/MenuBar/help/about") - macapp.insert_app_menu_item(aboutitem, 0) - - documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp") - macapp.insert_app_menu_item(documentationitem, 0) - - macapp.set_use_quartz_accelerators(False) - macapp.ready() - - LOG.debug("Initialized MacOS support") - - def __init__(self, *args, **kwargs): - gtk.Window.__init__(self, *args, **kwargs) - - def expose(window, event): - allocation = window.get_allocation() - CONF.set_int("window_w", allocation.width, "state") - CONF.set_int("window_h", allocation.height, "state") - self.connect("expose_event", expose) - - def state_change(window, event): - CONF.set_bool( - "window_maximized", - event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED, - "state") - self.connect("window-state-event", state_change) - - d = CONF.get("last_dir", "state") - if d and os.path.isdir(d): - platform.get_platform().set_last_dir(d) - - vbox = gtk.VBox(False, 2) - - self._recent = [] - - self.menu_ag = None - mbar = self.make_menubar() - - if os.name != "nt": - self._set_icon() # Windows gets the icon from the exe - if os.uname()[0] == "Darwin": - self._init_macos(mbar) - - vbox.pack_start(mbar, 0, 0, 0) - - self.tabs = None - tabs = self.make_tabs() - tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p)) - tabs.connect("page-removed", lambda *a: self.ev_tab_switched()) - tabs.show() - self.ev_tab_switched() - vbox.pack_start(tabs, 1, 1, 1) - - vbox.pack_start(self.make_status_bar(), 0, 0, 0) - - vbox.show() - - self.add(vbox) - - try: - width = CONF.get_int("window_w", "state") - height = CONF.get_int("window_h", "state") - except Exception: - width = 800 - height = 600 - - self.set_default_size(width, height) - if CONF.get_bool("window_maximized", "state"): - self.maximize() - self.set_title("CHIRP") - - self.connect("delete_event", self.ev_delete) - self.connect("destroy", self.ev_destroy) - - if not CONF.get_bool("warned_about_reporting") and \ - not CONF.get_bool("no_report"): - d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) - d.set_markup("" + - _("Error reporting is enabled") + - "") - d.format_secondary_markup( - _("If you wish to disable this feature you may do so in " - "the Help menu")) - d.run() - d.destroy() - CONF.set_bool("warned_about_reporting", True) - - self.update_recent_files() - try: - self.update_stock_configs() - except UnicodeDecodeError: - LOG.exception('We hit bug #272 while working with unicode paths. ' - 'Not copying stock configs so we can continue ' - 'startup.') - self.setup_extra_hotkeys() - - def updates_callback(ver): - gobject.idle_add(self._updates, ver) - - if not CONF.get_bool("skip_update_check", "state"): - reporting.check_for_updates(updates_callback) +# Copyright 2008 Dan Smith +# Copyright 2012 Tom Hayward +# +# 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 . + +from datetime import datetime +import os +import tempfile +import urllib +import webbrowser +from glob import glob +import shutil +import time +import logging +import gtk +import gobject +import sys + +from chirp.ui import inputdialog, common +from chirp import platform, directory, util +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 +from chirp.ui import editorset, clone, miscwidgets, config, reporting, fips +from chirp.ui import bandplans + +gobject.threads_init() + +LOG = logging.getLogger(__name__) + +if __name__ == "__main__": + sys.path.insert(0, "..") + +try: + import serial +except ImportError, e: + common.log_exception() + common.show_error("\nThe Pyserial module is not installed!") + + +CONF = config.get() + +KEEP_RECENT = 8 + +RB_BANDS = { + "--All--": 0, + "10 meters (29MHz)": 29, + "6 meters (54MHz)": 5, + "2 meters (144MHz)": 14, + "1.25 meters (220MHz)": 22, + "70 centimeters (440MHz)": 4, + "33 centimeters (900MHz)": 9, + "23 centimeters (1.2GHz)": 12, +} + + +def key_bands(band): + if band.startswith("-"): + return -1 + + amount, units, mhz = band.split(" ") + scale = units == "meters" and 100 or 1 + + return 100000 - (float(amount) * scale) + + +class ModifiedError(Exception): + pass + + +class ChirpMain(gtk.Window): + + def get_current_editorset(self): + page = self.tabs.get_current_page() + if page is not None: + return self.tabs.get_nth_page(page) + else: + return None + + def ev_tab_switched(self, pagenum=None): + def set_action_sensitive(action, sensitive): + self.menu_ag.get_action(action).set_sensitive(sensitive) + + if pagenum is not None: + eset = self.tabs.get_nth_page(pagenum) + else: + eset = self.get_current_editorset() + + upload_sens = bool(eset and + isinstance(eset.radio, chirp_common.CloneModeRadio)) + + if not eset or isinstance(eset.radio, chirp_common.LiveRadio): + save_sens = False + elif isinstance(eset.radio, chirp_common.NetworkSourceRadio): + save_sens = False + else: + save_sens = True + + for i in ["import", "importsrc", "stock"]: + set_action_sensitive(i, + eset is not None and not eset.get_read_only()) + + for i in ["save", "saveas"]: + set_action_sensitive(i, save_sens) + + for i in ["upload"]: + set_action_sensitive(i, upload_sens) + + for i in ["cancelq"]: + set_action_sensitive(i, eset is not None and not save_sens) + + for i in ["export", "close", "columns", "irbook", "irfinder", + "move_up", "move_dn", "exchange", "iradioreference", + "cut", "copy", "paste", "delete", "viewdeveloper", + "all", "properties"]: + set_action_sensitive(i, eset is not None) + + def ev_status(self, editorset, msg): + self.sb_radio.pop(0) + self.sb_radio.push(0, msg) + + def ev_usermsg(self, editorset, msg): + self.sb_general.pop(0) + self.sb_general.push(0, msg) + + def ev_editor_selected(self, editorset, editortype): + mappings = { + "memedit": ["view", "edit"], + } + + for _editortype, actions in mappings.items(): + for _action in actions: + action = self.menu_ag.get_action(_action) + action.set_sensitive(editortype.startswith(_editortype)) + + def _connect_editorset(self, eset): + eset.connect("want-close", self.do_close) + eset.connect("status", self.ev_status) + eset.connect("usermsg", self.ev_usermsg) + eset.connect("editor-selected", self.ev_editor_selected) + + def do_diff_radio(self): + if self.tabs.get_n_pages() < 2: + common.show_error("Diff tabs requires at least two open tabs!") + return + + esets = [] + for i in range(0, self.tabs.get_n_pages()): + esets.append(self.tabs.get_nth_page(i)) + + d = gtk.Dialog(title="Diff Radios", + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), + parent=self) + + label = gtk.Label("") + label.set_markup("-1 for either Mem # does a full-file hex " + + "dump with diffs highlighted.\n" + + "-2 for first Mem # shows " + + "only the diffs.") + d.vbox.pack_start(label, True, True, 0) + label.show() + + choices = [] + for eset in esets: + choices.append("%s %s (%s)" % (eset.rthread.radio.VENDOR, + eset.rthread.radio.MODEL, + eset.filename)) + choice_a = miscwidgets.make_choice(choices, False, choices[0]) + choice_a.show() + chan_a = gtk.SpinButton() + chan_a.get_adjustment().set_all(1, -2, 999, 1, 10, 0) + chan_a.show() + hbox = gtk.HBox(False, 3) + hbox.pack_start(choice_a, 1, 1, 1) + hbox.pack_start(chan_a, 0, 0, 0) + hbox.show() + d.vbox.pack_start(hbox, 0, 0, 0) + + choice_b = miscwidgets.make_choice(choices, False, choices[1]) + choice_b.show() + chan_b = gtk.SpinButton() + chan_b.get_adjustment().set_all(1, -1, 999, 1, 10, 0) + chan_b.show() + hbox = gtk.HBox(False, 3) + hbox.pack_start(choice_b, 1, 1, 1) + hbox.pack_start(chan_b, 0, 0, 0) + hbox.show() + d.vbox.pack_start(hbox, 0, 0, 0) + + r = d.run() + sel_a = choice_a.get_active_text() + sel_chan_a = chan_a.get_value() + sel_b = choice_b.get_active_text() + sel_chan_b = chan_b.get_value() + d.destroy() + if r == gtk.RESPONSE_CANCEL: + return + + if sel_a == sel_b: + common.show_error("Can't diff the same tab!") + return + + LOG.debug("Selected %s@%i and %s@%i" % + (sel_a, sel_chan_a, sel_b, sel_chan_b)) + name_a = os.path.basename(sel_a) + name_a = name_a[:name_a.rindex(")")] + name_b = os.path.basename(sel_b) + name_b = name_b[:name_b.rindex(")")] + diffwintitle = "%s@%i diff %s@%i" % ( + name_a, sel_chan_a, name_b, sel_chan_b) + + eset_a = esets[choices.index(sel_a)] + eset_b = esets[choices.index(sel_b)] + + def _show_diff(mem_b, mem_a): + # Step 3: Show the diff + diff = common.simple_diff(mem_a, mem_b) + common.show_diff_blob(diffwintitle, diff) + + def _get_mem_b(mem_a): + # Step 2: Get memory b + job = common.RadioJob(_show_diff, "get_raw_memory", + int(sel_chan_b)) + job.set_cb_args(mem_a) + eset_b.rthread.submit(job) + + if sel_chan_a >= 0 and sel_chan_b >= 0: + # Diff numbered memory + # Step 1: Get memory a + job = common.RadioJob(_get_mem_b, "get_raw_memory", + int(sel_chan_a)) + eset_a.rthread.submit(job) + elif isinstance(eset_a.rthread.radio, chirp_common.CloneModeRadio) and\ + isinstance(eset_b.rthread.radio, chirp_common.CloneModeRadio): + # Diff whole (can do this without a job, since both are clone-mode) + try: + addrfmt = CONF.get('hexdump_addrfmt', section='developer', + raw=True) + except: + pass + a = util.hexprint(eset_a.rthread.radio._mmap.get_packed(), + addrfmt=addrfmt) + b = util.hexprint(eset_b.rthread.radio._mmap.get_packed(), + addrfmt=addrfmt) + if sel_chan_a == -2: + diffsonly = True + else: + diffsonly = False + common.show_diff_blob(diffwintitle, + common.simple_diff(a, b, diffsonly)) + else: + common.show_error("Cannot diff whole live-mode radios!") + + def do_new(self): + eset = editorset.EditorSet(_("Untitled") + ".csv", self) + self._connect_editorset(eset) + eset.prime() + eset.show() + + tab = self.tabs.append_page(eset, eset.get_tab_label()) + self.tabs.set_current_page(tab) + + def _do_manual_select(self, filename): + radiolist = {} + for drv, radio in directory.DRV_TO_RADIO.items(): + if not issubclass(radio, chirp_common.CloneModeRadio): + continue + radiolist["%s %s" % (radio.VENDOR, radio.MODEL)] = drv + + lab = gtk.Label("""Unable to detect model! + +If you think that it is valid, you can select a radio model below to +force an open attempt. If selecting the model manually works, please +file a bug on the website and attach your image. If selecting the model +does not work, it is likely that you are trying to open some other type +of file. +""") + + lab.set_justify(gtk.JUSTIFY_FILL) + lab.set_line_wrap(True) + lab.set_use_markup(True) + lab.show() + choice = miscwidgets.make_choice(sorted(radiolist.keys()), False, + sorted(radiolist.keys())[0]) + d = gtk.Dialog(title="Detection Failed", + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + d.vbox.pack_start(lab, 0, 0, 0) + d.vbox.pack_start(choice, 0, 0, 0) + d.vbox.set_spacing(5) + choice.show() + d.set_default_size(400, 200) + # d.set_resizable(False) + r = d.run() + d.destroy() + if r != gtk.RESPONSE_OK: + return + try: + rc = directory.DRV_TO_RADIO[radiolist[choice.get_active_text()]] + return rc(filename) + except: + return + + def do_open(self, fname=None, tempname=None): + if not fname: + types = [(_("All files") + " (*.*)", "*"), + (_("CHIRP Radio Images") + " (*.img)", "*.img"), + (_("CHIRP Files") + " (*.chirp)", "*.chirp"), + (_("CSV Files") + " (*.csv)", "*.csv"), + (_("DAT Files") + " (*.dat)", "*.dat"), + (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), + (_("ICF Files") + " (*.icf)", "*.icf"), + (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), + (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), + (_("VX7 Commander Files") + " (*.vx7)", "*.vx7"), + ] + fname = platform.get_platform().gui_open_file(types=types) + if not fname: + return + + self.record_recent_file(fname) + + if icf.is_icf_file(fname): + a = common.ask_yesno_question( + _("ICF files cannot be edited, only displayed or imported " + "into another file. Open in read-only mode?"), + self) + if not a: + return + read_only = True + else: + read_only = False + + if icf.is_9x_icf(fname): + # We have to actually instantiate the IC9xICFRadio to get its + # sub-devices + radio = ic9x_icf.IC9xICFRadio(fname) + else: + try: + radio = directory.get_radio_by_image(fname) + except errors.ImageDetectFailed: + radio = self._do_manual_select(fname) + if not radio: + return + LOG.debug("Manually selected %s" % radio) + except Exception, e: + common.log_exception() + common.show_error(os.path.basename(fname) + ": " + str(e)) + return + + first_tab = False + try: + eset = editorset.EditorSet(radio, self, + filename=fname, + tempname=tempname) + except Exception, e: + common.log_exception() + common.show_error( + _("There was an error opening {fname}: {error}").format( + fname=fname, + error=e)) + return + + eset.set_read_only(read_only) + self._connect_editorset(eset) + eset.show() + self.tabs.append_page(eset, eset.get_tab_label()) + + if hasattr(eset.rthread.radio, "errors") and \ + eset.rthread.radio.errors: + msg = _("{num} errors during open:").format( + num=len(eset.rthread.radio.errors)) + common.show_error_text(msg, + "\r\n".join(eset.rthread.radio.errors)) + + def do_live_warning(self, radio): + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("" + _("Note:") + "") + msg = _("The {vendor} {model} operates in live mode. " + "This means that any changes you make are immediately sent " + "to the radio. Because of this, you cannot perform the " + "Save or Upload operations. If you wish to " + "edit the contents offline, please Export to a CSV " + "file, using the File menu.") + msg = msg.format(vendor=radio.VENDOR, model=radio.MODEL) + d.format_secondary_markup(msg) + + again = gtk.CheckButton(_("Don't show this again")) + again.show() + d.vbox.pack_start(again, 0, 0, 0) + d.run() + CONF.set_bool("live_mode", again.get_active(), "noconfirm") + d.destroy() + + def do_open_live(self, radio, tempname=None, read_only=False): + eset = editorset.EditorSet(radio, self, tempname=tempname) + eset.connect("want-close", self.do_close) + eset.connect("status", self.ev_status) + eset.set_read_only(read_only) + eset.show() + self.tabs.append_page(eset, eset.get_tab_label()) + + if isinstance(radio, chirp_common.LiveRadio): + reporting.report_model_usage(radio, "live", True) + if not CONF.get_bool("live_mode", "noconfirm"): + self.do_live_warning(radio) + + def do_save(self, eset=None): + if not eset: + eset = self.get_current_editorset() + + # For usability, allow Ctrl-S to short-circuit to Save-As if + # we are working on a yet-to-be-saved image + if not os.path.exists(eset.filename): + return self.do_saveas() + + eset.save() + + def do_saveas(self): + eset = self.get_current_editorset() + + label = _("{vendor} {model} image file").format( + vendor=eset.radio.VENDOR, + model=eset.radio.MODEL) + + defname_format = CONF.get("default_filename", "global") or \ + "{vendor}_{model}_{date}" + defname = defname_format.format( + vendor=eset.radio.VENDOR, + model=eset.radio.MODEL, + date=datetime.now().strftime('%Y%m%d') + ).replace('/', '_') + + types = [(label + " (*.%s)" % eset.radio.FILE_EXTENSION, + eset.radio.FILE_EXTENSION)] + + if isinstance(eset.radio, vx7.VX7Radio): + types += [(_("VX7 Commander") + " (*.vx7)", "vx7")] + elif isinstance(eset.radio, vx6.VX6Radio): + types += [(_("VX6 Commander") + " (*.vx6)", "vx6")] + elif isinstance(eset.radio, vx5.VX5Radio): + types += [(_("EVE") + " (*.eve)", "eve")] + types += [(_("VX5 Commander") + " (*.vx5)", "vx5")] + + while True: + fname = platform.get_platform().gui_save_file(default_name=defname, + types=types) + if not fname: + return + + if os.path.exists(fname): + dlg = inputdialog.OverwriteDialog(fname) + owrite = dlg.run() + dlg.destroy() + if owrite == gtk.RESPONSE_OK: + break + else: + break + + try: + eset.save(fname) + except Exception, e: + d = inputdialog.ExceptionDialog(e) + d.run() + d.destroy() + + def cb_clonein(self, radio, emsg=None): + radio.pipe.close() + reporting.report_model_usage(radio, "download", bool(emsg)) + if not emsg: + self.do_open_live(radio, tempname="(" + _("Untitled") + ")") + else: + d = inputdialog.ExceptionDialog(emsg) + d.run() + d.destroy() + + def cb_cloneout(self, radio, emsg=None): + radio.pipe.close() + reporting.report_model_usage(radio, "upload", True) + if emsg: + d = inputdialog.ExceptionDialog(emsg) + d.run() + d.destroy() + + def _get_recent_list(self): + recent = [] + for i in range(0, KEEP_RECENT): + fn = CONF.get("recent%i" % i, "state") + if fn: + recent.append(fn) + return recent + + def _set_recent_list(self, recent): + for fn in recent: + CONF.set("recent%i" % recent.index(fn), fn, "state") + + def update_recent_files(self): + i = 0 + for fname in self._get_recent_list(): + action_name = "recent%i" % i + path = "/MenuBar/file/recent" + + old_action = self.menu_ag.get_action(action_name) + if old_action: + self.menu_ag.remove_action(old_action) + + file_basename = os.path.basename(fname).replace("_", "__") + action = gtk.Action( + action_name, "_%i. %s" % (i + 1, file_basename), + _("Open recent file {name}").format(name=fname), "") + action.connect("activate", lambda a, f: self.do_open(f), fname) + mid = self.menu_uim.new_merge_id() + self.menu_uim.add_ui(mid, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + self.menu_ag.add_action(action) + i += 1 + + def record_recent_file(self, filename): + + recent_files = self._get_recent_list() + if filename not in recent_files: + if len(recent_files) == KEEP_RECENT: + del recent_files[-1] + recent_files.insert(0, filename) + self._set_recent_list(recent_files) + + self.update_recent_files() + + def import_stock_config(self, action, config): + eset = self.get_current_editorset() + count = eset.do_import(config) + + def copy_shipped_stock_configs(self, stock_dir): + basepath = platform.get_platform().find_resource("stock_configs") + + files = glob(os.path.join(basepath, "*.csv")) + for fn in files: + if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))): + LOG.info("Skipping existing stock config") + continue + try: + shutil.copy(fn, stock_dir) + LOG.debug("Copying %s -> %s" % (fn, stock_dir)) + except Exception, e: + LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e)) + return False + return True + + def update_stock_configs(self): + stock_dir = platform.get_platform().config_file("stock_configs") + if not os.path.isdir(stock_dir): + try: + os.mkdir(stock_dir) + except Exception, e: + LOG.error("Unable to create directory: %s" % stock_dir) + return + if not self.copy_shipped_stock_configs(stock_dir): + return + + def _do_import_action(config): + name = os.path.splitext(os.path.basename(config))[0] + action_name = "stock-%i" % configs.index(config) + path = "/MenuBar/radio/stock" + action = gtk.Action(action_name, + name, + _("Import stock " + "configuration {name}").format(name=name), + "") + action.connect("activate", self.import_stock_config, config) + mid = self.menu_uim.new_merge_id() + mid = self.menu_uim.add_ui(mid, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + self.menu_ag.add_action(action) + + def _do_open_action(config): + name = os.path.splitext(os.path.basename(config))[0] + action_name = "openstock-%i" % configs.index(config) + path = "/MenuBar/file/openstock" + action = gtk.Action(action_name, + name, + _("Open stock " + "configuration {name}").format(name=name), + "") + action.connect("activate", lambda a, c: self.do_open(c), config) + mid = self.menu_uim.new_merge_id() + mid = self.menu_uim.add_ui(mid, path, + action_name, action_name, + gtk.UI_MANAGER_MENUITEM, False) + self.menu_ag.add_action(action) + + configs = glob(os.path.join(stock_dir, "*.csv")) + for config in configs: + _do_import_action(config) + _do_open_action(config) + + def _confirm_experimental(self, rclass): + sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass) + if CONF.is_defined(sql_key, "state") and \ + not CONF.get_bool(sql_key, "state"): + return True + + title = _("Proceed with experimental driver?") + text = rclass.get_prompts().experimental + msg = _("This radio's driver is experimental. " + "Do you want to proceed?") + resp, squelch = common.show_warning(msg, text, + title=title, + buttons=gtk.BUTTONS_YES_NO, + can_squelch=True) + if resp == gtk.RESPONSE_YES: + CONF.set_bool(sql_key, not squelch, "state") + return resp == gtk.RESPONSE_YES + + def _show_information(self, radio, message): + if message is None: + return + + if CONF.get_bool("clone_information", "noconfirm"): + return + + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("" + _("{name} Information").format( + name=radio.get_name()) + "") + msg = _("{information}").format(information=message) + _again_msg = "Don't show information for any radio again" + + d.format_secondary_markup(msg) + + again = gtk.CheckButton(_(_again_msg)) + again.show() + again.connect("toggled", lambda action: + self.infomenu.set_active(not action.get_active())) + d.vbox.pack_start(again, 0, 0, 0) + h_button_box = d.vbox.get_children()[2] + try: + ok_button = h_button_box.get_children()[0] + ok_button.grab_default() + ok_button.grab_focus() + except AttributeError: + # don't grab focus on GTK+ 2.0 + pass + d.run() + d.destroy() + + def _show_instructions(self, radio, message): + if message is None: + return + + if CONF.get_bool("clone_instructions", "noconfirm"): + return + + d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) + d.set_markup("" + _("{name} Instructions").format( + name=radio.get_name()) + "") + msg = _("{instructions}").format(instructions=message) + _again_msg = "Don't show instructions for any radio again" + + d.format_secondary_markup(msg) + + again = gtk.CheckButton(_(_again_msg)) + again.show() + again.connect("toggled", lambda action: + self.clonemenu.set_active(not action.get_active())) + d.vbox.pack_start(again, 0, 0, 0) + h_button_box = d.vbox.get_children()[2] + try: + ok_button = h_button_box.get_children()[0] + ok_button.grab_default() + ok_button.grab_focus() + except AttributeError: + # don't grab focus on GTK+ 2.0 + pass + d.run() + d.destroy() + + def do_download(self, port=None, rtype=None): + d = clone.CloneSettingsDialog(parent=self) + settings = d.run() + d.destroy() + if not settings: + return + + rclass = settings.radio_class + if issubclass(rclass, chirp_common.ExperimentalRadio) and \ + not self._confirm_experimental(rclass): + # User does not want to proceed with experimental driver + return + + if rclass.get_prompts().display_info is True: + self._show_information(rclass, rclass.get_prompts().info) + + self._show_instructions(rclass, rclass.get_prompts().pre_download) + + LOG.debug("User selected %s %s on port %s" % + (rclass.VENDOR, rclass.MODEL, settings.port)) + + try: + ser = serial.Serial(port=settings.port, + baudrate=rclass.BAUD_RATE, + rtscts=rclass.HARDWARE_FLOW, + timeout=0.25) + ser.flushInput() + except serial.SerialException, e: + d = inputdialog.ExceptionDialog(e) + d.run() + d.destroy() + return + + radio = settings.radio_class(ser) + + fn = tempfile.mktemp() + if isinstance(radio, chirp_common.CloneModeRadio): + ct = clone.CloneThread(radio, "in", cb=self.cb_clonein, + parent=self) + ct.start() + else: + self.do_open_live(radio) + + def do_upload(self, port=None, rtype=None): + eset = self.get_current_editorset() + radio = eset.radio + + settings = clone.CloneSettings() + settings.radio_class = radio.__class__ + + d = clone.CloneSettingsDialog(settings, parent=self) + settings = d.run() + d.destroy() + if not settings: + return + prompts = radio.get_prompts() + + if prompts.display_pre_upload_prompt_before_opening_port is True: + LOG.debug("Opening port after pre_upload prompt.") + self._show_instructions(radio, prompts.pre_upload) + + if isinstance(radio, chirp_common.ExperimentalRadio) and \ + not self._confirm_experimental(radio.__class__): + # User does not want to proceed with experimental driver + return + + try: + ser = serial.Serial(port=settings.port, + baudrate=radio.BAUD_RATE, + rtscts=radio.HARDWARE_FLOW, + timeout=0.25) + ser.flushInput() + except serial.SerialException, e: + d = inputdialog.ExceptionDialog(e) + d.run() + d.destroy() + return + + if prompts.display_pre_upload_prompt_before_opening_port is False: + LOG.debug("Opening port before pre_upload prompt.") + self._show_instructions(radio, prompts.pre_upload) + + radio.set_pipe(ser) + + ct = clone.CloneThread(radio, "out", cb=self.cb_cloneout, parent=self) + ct.start() + + def do_close(self, tab_child=None): + if tab_child: + eset = tab_child + else: + eset = self.get_current_editorset() + + if not eset: + return False + + if eset.is_modified(): + dlg = miscwidgets.YesNoDialog( + title=_("Save Changes?"), parent=self, + buttons=(gtk.STOCK_YES, gtk.RESPONSE_YES, + gtk.STOCK_NO, gtk.RESPONSE_NO, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + dlg.set_text(_("File is modified, save changes before closing?")) + res = dlg.run() + dlg.destroy() + + if res == gtk.RESPONSE_YES: + self.do_save(eset) + elif res != gtk.RESPONSE_NO: + raise ModifiedError() + + eset.rthread.stop() + eset.rthread.join() + + eset.prepare_close() + + if eset.radio.pipe: + eset.radio.pipe.close() + + if isinstance(eset.radio, chirp_common.LiveRadio): + action = self.menu_ag.get_action("openlive") + if action: + action.set_sensitive(True) + + page = self.tabs.page_num(eset) + if page is not None: + self.tabs.remove_page(page) + + return True + + def do_import(self): + types = [(_("All files") + " (*.*)", "*"), + (_("CHIRP Files") + " (*.chirp)", "*.chirp"), + (_("CHIRP Radio Images") + " (*.img)", "*.img"), + (_("CSV Files") + " (*.csv)", "*.csv"), + (_("DAT Files") + " (*.dat)", "*.dat"), + (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), + (_("ICF Files") + " (*.icf)", "*.icf"), + (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"), + (_("Kenwood ITM Files") + " (*.itm)", "*.itm"), + (_("Travel Plus Files") + " (*.tpe)", "*.tpe"), + (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), + (_("VX6 Commander Files") + " (*.vx6)", "*.vx6"), + (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")] + filen = platform.get_platform().gui_open_file(types=types) + if not filen: + return + + eset = self.get_current_editorset() + count = eset.do_import(filen) + reporting.report_model_usage(eset.rthread.radio, "import", count > 0) + + 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("RepeaterBook\r\n" + + "North American Repeater Directory") + d.format_secondary_markup("For more information about this " + + "free service, please go to\r\n" + + "http://www.repeaterbook.com") + d.run() + d.destroy() + CONF.set_bool("has_seen_credit", True, "repeaterbook") + + default_state = "Oregon" + default_county = "--All--" + default_band = "--All--" + try: + try: + code = int(CONF.get("state", "repeaterbook")) + except: + code = CONF.get("state", "repeaterbook") + for k, v in fips.FIPS_STATES.items(): + if code == v: + default_state = k + break + + code = CONF.get("county", "repeaterbook") + items = fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].items() + for k, v in items: + if code == v: + default_county = k + break + + code = int(CONF.get("band", "repeaterbook")) + for k, v in RB_BANDS.items(): + if code == v: + default_band = k + break + except: + pass + + state = miscwidgets.make_choice(sorted(fips.FIPS_STATES.keys()), + False, default_state) + county = miscwidgets.make_choice( + sorted(fips.FIPS_COUNTIES[fips.FIPS_STATES[default_state]].keys()), + False, default_county) + band = miscwidgets.make_choice(sorted(RB_BANDS.keys(), key=key_bands), + False, default_band) + + def _changed(box, county): + state = fips.FIPS_STATES[box.get_active_text()] + county.get_model().clear() + for fips_county in sorted(fips.FIPS_COUNTIES[state].keys()): + county.append_text(fips_county) + county.set_active(0) + + state.connect("changed", _changed, county) + + d = inputdialog.FieldDialog(title=_("RepeaterBook Query"), parent=self) + d.add_field("State", state) + d.add_field("County", county) + d.add_field("Band", band) + + r = d.run() + d.destroy() + if r != gtk.RESPONSE_OK: + return False + + code = fips.FIPS_STATES[state.get_active_text()] + county_id = fips.FIPS_COUNTIES[code][county.get_active_text()] + freq = RB_BANDS[band.get_active_text()] + CONF.set("state", str(code), "repeaterbook") + CONF.set("county", str(county_id), "repeaterbook") + CONF.set("band", str(freq), "repeaterbook") + + return True + + def do_repeaterbook_political(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_repeaterbook_political_prompt(): + self.window.set_cursor(None) + return + + try: + code = "%02i" % int(CONF.get("state", "repeaterbook")) + except: + try: + code = CONF.get("state", "repeaterbook") + except: + code = '41' # Oregon default + + try: + county = CONF.get("county", "repeaterbook") + except: + county = '%' # --All-- default + + try: + band = int(CONF.get("band", "repeaterbook")) + except: + band = 14 # 2m default + + query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \ + "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \ + "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%" + 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 + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + 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 = 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 = '%' + + 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) + + 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 = 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_przemienniki_prompt(self): + d = inputdialog.FieldDialog(title='przemienniki.net query', + parent=self) + fields = { + "Country": + (miscwidgets.make_choice( + ['at', 'bg', 'by', 'ch', 'cz', 'de', 'dk', 'es', 'fi', + 'fr', 'hu', 'it', 'lt', 'lv', 'no', 'pl', 'ro', 'se', + 'sk', 'ua', 'uk'], False), + lambda x: str(x.get_active_text())), + "Band": + (miscwidgets.make_choice(['10m', '4m', '6m', '2m', '70cm', + '23cm', '13cm', '3cm'], False, '2m'), + lambda x: str(x.get_active_text())), + "Mode": + (miscwidgets.make_choice(['fm', 'dv'], False), + lambda x: str(x.get_active_text())), + "Only Working": + (miscwidgets.make_choice(['', 'yes'], False), + lambda x: str(x.get_active_text())), + "Latitude": (gtk.Entry(), lambda x: float(x.get_text())), + "Longitude": (gtk.Entry(), lambda x: float(x.get_text())), + "Range": (gtk.Entry(), lambda x: int(x.get_text())), + } + for name in sorted(fields.keys()): + value, fn = fields[name] + d.add_field(name, value) + while d.run() == gtk.RESPONSE_OK: + query = "http://przemienniki.net/export/chirp.csv?" + args = [] + for name, (value, fn) in fields.items(): + if isinstance(value, gtk.Entry): + contents = value.get_text() + else: + contents = value.get_active_text() + if contents: + try: + _value = fn(value) + except ValueError: + common.show_error(_("Invalid value for %s") % name) + query = None + continue + + args.append("=".join((name.replace(" ", "").lower(), + contents))) + query += "&".join(args) + LOG.debug(query) + d.destroy() + return query + + d.destroy() + return query + + def do_przemienniki(self, do_import): + url = self.do_przemienniki_prompt() + if not url: + return + + fn = tempfile.mktemp(".csv") + filename, headers = urllib.urlretrieve(url, fn) + if not os.path.exists(filename): + LOG.error("Failed, headers were: %s", str(headers)) + common.show_error(_("Query failed")) + return + + class PRRadio(generic_csv.CSVRadio, + chirp_common.NetworkSourceRadio): + VENDOR = "przemienniki.net" + MODEL = "" + + try: + radio = PRRadio(filename) + except Exception, e: + common.show_error(str(e)) + return + + if do_import: + eset = self.get_current_editorset() + count = eset.do_import(filename) + else: + self.do_open_live(radio, read_only=True) + + def do_rfinder_prompt(self): + fields = {"1Email": (gtk.Entry(), lambda x: "@" in x), + "2Password": (gtk.Entry(), lambda x: x), + "3Latitude": (gtk.Entry(), + lambda x: float(x) < 90 and float(x) > -90), + "4Longitude": (gtk.Entry(), + lambda x: float(x) < 180 and float(x) > -180), + "5Range_in_Miles": (gtk.Entry(), + lambda x: int(x) > 0 and int(x) < 5000), + } + + d = inputdialog.FieldDialog(title="RFinder Login", parent=self) + for k in sorted(fields.keys()): + d.add_field(k[1:].replace("_", " "), fields[k][0]) + fields[k][0].set_text(CONF.get(k[1:], "rfinder") or "") + fields[k][0].set_visibility(k != "2Password") + + while d.run() == gtk.RESPONSE_OK: + valid = True + for k in sorted(fields.keys()): + widget, validator = fields[k] + try: + if validator(widget.get_text()): + CONF.set(k[1:], widget.get_text(), "rfinder") + continue + except Exception: + 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_rfinder(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_rfinder_prompt(): + self.window.set_cursor(None) + return + + lat = CONF.get_float("Latitude", "rfinder") + lon = CONF.get_float("Longitude", "rfinder") + passwd = CONF.get("Password", "rfinder") + email = CONF.get("Email", "rfinder") + miles = CONF.get_int("Range_in_Miles", "rfinder") + + # 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() + rfstr = "rfinder://%s/%s/%f/%f/%i" % \ + (email, passwd, lat, lon, miles) + count = eset.do_import(rfstr) + else: + from chirp.drivers import rfinder + radio = rfinder.RFinderRadio(None) + radio.set_params((lat, lon), miles, email, passwd) + self.do_open_live(radio, read_only=True) + + self.window.set_cursor(None) + + def do_radioreference_prompt(self): + fields = {"1Username": (gtk.Entry(), lambda x: x), + "2Password": (gtk.Entry(), lambda x: x), + "3Zipcode": (gtk.Entry(), lambda x: x), + } + + d = inputdialog.FieldDialog(title=_("RadioReference.com Query"), + 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:], "radioreference") or "") + fields[k][0].set_visibility(k != "2Password") + + while d.run() == gtk.RESPONSE_OK: + valid = True + for k in sorted(fields.keys()): + widget, validator = fields[k] + try: + if validator(widget.get_text()): + CONF.set(k[1:], widget.get_text(), "radioreference") + continue + except Exception: + 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_radioreference(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_radioreference_prompt(): + self.window.set_cursor(None) + return + + username = CONF.get("Username", "radioreference") + passwd = CONF.get("Password", "radioreference") + zipcode = CONF.get("Zipcode", "radioreference") + + # 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() + rrstr = "radioreference://%s/%s/%s" % (zipcode, username, passwd) + count = eset.do_import(rrstr) + else: + try: + from chirp import radioreference + radio = radioreference.RadioReferenceRadio(None) + radio.set_params(zipcode, username, passwd) + self.do_open_live(radio, read_only=True) + except errors.RadioError, e: + common.show_error(e) + + self.window.set_cursor(None) + + def do_export(self): + types = [(_("CSV Files") + " (*.csv)", "csv"), + ] + + eset = self.get_current_editorset() + + if os.path.exists(eset.filename): + base = os.path.basename(eset.filename) + if "." in base: + base = base[:base.rindex(".")] + defname = base + else: + defname = "radio" + + filen = platform.get_platform().gui_save_file(default_name=defname, + types=types) + if not filen: + return + + if os.path.exists(filen): + dlg = inputdialog.OverwriteDialog(filen) + owrite = dlg.run() + dlg.destroy() + if owrite != gtk.RESPONSE_OK: + return + os.remove(filen) + + count = eset.do_export(filen) + reporting.report_model_usage(eset.rthread.radio, "export", count > 0) + + def do_about(self): + d = gtk.AboutDialog() + d.set_transient_for(self) + import sys + verinfo = "GTK %s\nPyGTK %s\nPython %s\n" % ( + ".".join([str(x) for x in gtk.gtk_version]), + ".".join([str(x) for x in gtk.pygtk_version]), + sys.version.split()[0]) + + # Set url hook to handle user activating a URL link in the about dialog + gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url)) + + d.set_name("CHIRP") + d.set_version(CHIRP_VERSION) + d.set_copyright("Copyright 2015 Dan Smith (KK7DS)") + d.set_website("http://chirp.danplanet.com") + d.set_authors(("Dan Smith KK7DS ", + _("With significant contributions from:"), + "Tom KD7LXL", + "Marco IZ3GME", + "Jim KC9HI" + )) + d.set_translator_credits("Polish: Grzegorz SQ2RBY" + + os.linesep + + "Italian: Fabio IZ2QDH" + + os.linesep + + "Dutch: Michael PD4MT" + + os.linesep + + "German: Benjamin HB9EUK" + + os.linesep + + "Hungarian: Attila HA5JA" + + os.linesep + + "Russian: Dmitry Slukin" + + os.linesep + + "Portuguese (BR): Crezivando PP7CJ") + d.set_comments(verinfo) + + d.run() + d.destroy() + + def do_gethelp(self): + webbrowser.open("http://chirp.danplanet.com") + + def do_columns(self): + eset = self.get_current_editorset() + driver = directory.get_driver(eset.rthread.radio.__class__) + radio_name = "%s %s %s" % (eset.rthread.radio.VENDOR, + eset.rthread.radio.MODEL, + eset.rthread.radio.VARIANT) + d = gtk.Dialog(title=_("Select Columns"), + parent=self, + buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, + gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) + + vbox = gtk.VBox() + vbox.show() + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.add_with_viewport(vbox) + sw.show() + d.vbox.pack_start(sw, 1, 1, 1) + d.set_size_request(-1, 300) + d.set_resizable(False) + + labelstr = _("Visible columns for {radio}").format(radio=radio_name) + label = gtk.Label(labelstr) + label.show() + vbox.pack_start(label) + + fields = [] + memedit = eset.get_current_editor() # .editors["memedit"] + unsupported = memedit.get_unsupported_columns() + for colspec in memedit.cols: + if colspec[0].startswith("_"): + continue + elif colspec[0] in unsupported: + continue + label = colspec[0] + visible = memedit.get_column_visible(memedit.col(label)) + widget = gtk.CheckButton(label) + widget.set_active(visible) + fields.append(widget) + vbox.pack_start(widget, 1, 1, 1) + widget.show() + + res = d.run() + selected_columns = [] + if res == gtk.RESPONSE_OK: + for widget in fields: + colnum = memedit.col(widget.get_label()) + memedit.set_column_visible(colnum, widget.get_active()) + if widget.get_active(): + selected_columns.append(widget.get_label()) + + d.destroy() + + CONF.set(driver, ",".join(selected_columns), "memedit_columns") + + def do_hide_unused(self, action): + eset = self.get_current_editorset() + if eset is None: + conf = config.get("memedit") + conf.set_bool("hide_unused", action.get_active()) + else: + for editortype, editor in eset.editors.iteritems(): + if "memedit" in editortype: + editor.set_hide_unused(action.get_active()) + + def do_clearq(self): + eset = self.get_current_editorset() + eset.rthread.flush() + + def do_copy(self, cut): + eset = self.get_current_editorset() + eset.get_current_editor().copy_selection(cut) + + def do_paste(self): + eset = self.get_current_editorset() + eset.get_current_editor().paste_selection() + + def do_delete(self): + eset = self.get_current_editorset() + eset.get_current_editor().copy_selection(True) + + def do_toggle_report(self, action): + if not action.get_active(): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO, parent=self) + markup = "" + _("Reporting is disabled") + "" + d.set_markup(markup) + msg = _("The reporting feature of CHIRP is designed to help " + "improve quality by allowing the authors to focus " + "on the radio drivers used most often and errors " + "experienced by the users. The reports contain no " + "identifying information and are used only for " + "statistical purposes by the authors. Your privacy is " + "extremely important, but please consider leaving " + "this feature enabled to help make CHIRP better!\n\n" + "Are you sure you want to disable this feature?") + d.format_secondary_markup(msg.replace("\n", "\r\n")) + r = d.run() + d.destroy() + if r == gtk.RESPONSE_NO: + action.set_active(not action.get_active()) + + conf = config.get() + conf.set_bool("no_report", not action.get_active()) + + def do_toggle_no_smart_tmode(self, action): + CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit") + + def do_toggle_developer(self, action): + conf = config.get() + conf.set_bool("developer", action.get_active(), "state") + + for name in ["viewdeveloper", "loadmod"]: + devaction = self.menu_ag.get_action(name) + devaction.set_visible(action.get_active()) + + def do_toggle_clone_information(self, action): + CONF.set_bool("clone_information", + not action.get_active(), "noconfirm") + + def do_toggle_clone_instructions(self, action): + CONF.set_bool("clone_instructions", + not action.get_active(), "noconfirm") + + def do_change_language(self): + langs = ["Auto", "English", "Polish", "Italian", "Dutch", "German", + "Hungarian", "Russian", "Portuguese (BR)", "French", + "Spanish"] + d = inputdialog.ChoiceDialog(langs, parent=self, + title="Choose Language") + d.label.set_text(_("Choose a language or Auto to use the " + "operating system default. You will need to " + "restart the application before the change " + "will take effect")) + d.label.set_line_wrap(True) + r = d.run() + if r == gtk.RESPONSE_OK: + LOG.debug("Chose language %s" % d.choice.get_active_text()) + conf = config.get() + conf.set("language", d.choice.get_active_text(), "state") + d.destroy() + + def load_module(self): + types = [(_("Python Modules") + "*.py", "*.py")] + filen = platform.get_platform().gui_open_file(types=types) + if not filen: + return + + # We're in development mode, so we need to tell the directory to + # allow a loaded module to override an existing driver, against + # its normal better judgement + directory.enable_reregistrations() + + try: + module = file(filen) + code = module.read() + module.close() + pyc = compile(code, filen, 'exec') + # See this for why: + # http://stackoverflow.com/questions/2904274/globals-and-locals-in-python-exec + exec(pyc, globals(), globals()) + except Exception, e: + common.log_exception() + common.show_error("Unable to load module: %s" % e) + + def mh(self, _action, *args): + action = _action.get_name() + + if action == "quit": + gtk.main_quit() + elif action == "new": + self.do_new() + elif action == "open": + self.do_open() + elif action == "save": + self.do_save() + elif action == "saveas": + self.do_saveas() + elif action.startswith("download"): + self.do_download(*args) + elif action.startswith("upload"): + self.do_upload(*args) + elif action == "close": + 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 ["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": + self.do_about() + elif action == "gethelp": + self.do_gethelp() + elif action == "columns": + self.do_columns() + elif action == "hide_unused": + self.do_hide_unused(_action) + elif action == "cancelq": + self.do_clearq() + elif action == "report": + self.do_toggle_report(_action) + elif action == "channel_defaults": + # The memedit thread also has an instance of bandplans. + bp = bandplans.BandPlans(CONF) + bp.select_bandplan(self) + elif action == "no_smart_tmode": + self.do_toggle_no_smart_tmode(_action) + elif action == "developer": + self.do_toggle_developer(_action) + elif action == "clone_information": + self.do_toggle_clone_information(_action) + elif action == "clone_instructions": + self.do_toggle_clone_instructions(_action) + elif action in ["cut", "copy", "paste", "delete", + "move_up", "move_dn", "exchange", "all", + "devshowraw", "devdiffraw", "properties"]: + self.get_current_editorset().get_current_editor().hotkey(_action) + elif action == "devdifftab": + self.do_diff_radio() + elif action == "language": + self.do_change_language() + elif action == "loadmod": + self.load_module() + else: + return + + self.ev_tab_switched() + + def make_menubar(self): + menu_xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + ALT_KEY = "" + CTRL_KEY = "" + if sys.platform == 'darwin': + ALT_KEY = "" + CTRL_KEY = "" + actions = [ + ('file', None, _("_File"), None, None, self.mh), + ('new', gtk.STOCK_NEW, None, None, None, self.mh), + ('open', gtk.STOCK_OPEN, None, None, None, self.mh), + ('openstock', None, _("Open stock config"), None, None, self.mh), + ('recent', None, _("_Recent"), None, None, self.mh), + ('save', gtk.STOCK_SAVE, None, None, None, self.mh), + ('saveas', gtk.STOCK_SAVE_AS, None, None, None, self.mh), + ('loadmod', None, _("Load Module"), None, None, self.mh), + ('close', gtk.STOCK_CLOSE, None, None, None, self.mh), + ('quit', gtk.STOCK_QUIT, None, None, None, self.mh), + ('edit', None, _("_Edit"), None, None, self.mh), + ('cut', None, _("_Cut"), "%sx" % CTRL_KEY, None, self.mh), + ('copy', None, _("_Copy"), "%sc" % CTRL_KEY, None, self.mh), + ('paste', None, _("_Paste"), + "%sv" % CTRL_KEY, None, self.mh), + ('delete', None, _("_Delete"), "Delete", None, self.mh), + ('all', None, _("Select _All"), None, None, self.mh), + ('move_up', None, _("Move _Up"), + "%sUp" % CTRL_KEY, None, self.mh), + ('move_dn', None, _("Move Dow_n"), + "%sDown" % CTRL_KEY, None, self.mh), + ('exchange', None, _("E_xchange"), + "%sx" % CTRL_KEY, None, self.mh), + ('properties', None, _("P_roperties"), None, None, self.mh), + ('view', None, _("_View"), None, None, self.mh), + ('columns', None, _("Columns"), None, None, self.mh), + ('viewdeveloper', None, _("Developer"), None, None, self.mh), + ('devshowraw', None, _('Show raw memory'), + "%sr" % CTRL_KEY, None, self.mh), + ('devdiffraw', None, _("Diff raw memories"), + "%sd" % CTRL_KEY, None, self.mh), + ('devdifftab', None, _("Diff tabs"), + "%st" % CTRL_KEY, None, self.mh), + ('language', None, _("Change language"), None, None, self.mh), + ('radio', None, _("_Radio"), None, None, self.mh), + ('download', None, _("Download From Radio"), + "%sd" % ALT_KEY, None, self.mh), + ('upload', None, _("Upload To Radio"), + "%su" % ALT_KEY, None, self.mh), + ('import', None, _("Import"), "%si" % ALT_KEY, None, self.mh), + ('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), + ('stock', None, _("Import from stock config"), + None, None, self.mh), + ('channel_defaults', None, _("Channel defaults"), + None, None, self.mh), + ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh), + ('help', None, _('Help'), None, None, self.mh), + ('about', gtk.STOCK_ABOUT, None, None, None, self.mh), + ('gethelp', None, _("Get Help Online..."), None, None, self.mh), + ] + + conf = config.get() + re = not conf.get_bool("no_report") + hu = conf.get_bool("hide_unused", "memedit", default=True) + dv = conf.get_bool("developer", "state") + cf = not conf.get_bool("clone_information", "noconfirm") + ci = not conf.get_bool("clone_instructions", "noconfirm") + st = not conf.get_bool("no_smart_tmode", "memedit") + + toggles = [('report', None, _("Report Statistics"), + None, None, self.mh, re), + ('hide_unused', None, _("Hide Unused Fields"), + None, None, self.mh, hu), + ('no_smart_tmode', None, _("Smart Tone Modes"), + None, None, self.mh, st), + ('clone_information', None, _("Show Information"), + None, None, self.mh, cf), + ('clone_instructions', None, _("Show Instructions"), + None, None, self.mh, ci), + ('developer', None, _("Enable Developer Functions"), + None, None, self.mh, dv), + ] + + self.menu_uim = gtk.UIManager() + self.menu_ag = gtk.ActionGroup("MenuBar") + self.menu_ag.add_actions(actions) + self.menu_ag.add_toggle_actions(toggles) + + self.menu_uim.insert_action_group(self.menu_ag, 0) + self.menu_uim.add_ui_from_string(menu_xml) + + self.add_accel_group(self.menu_uim.get_accel_group()) + + self.infomenu = self.menu_uim.get_widget( + "/MenuBar/help/clone_information") + + self.clonemenu = self.menu_uim.get_widget( + "/MenuBar/help/clone_instructions") + + # Initialize + self.do_toggle_developer(self.menu_ag.get_action("developer")) + + return self.menu_uim.get_widget("/MenuBar") + + def make_tabs(self): + self.tabs = gtk.Notebook() + self.tabs.set_scrollable(True) + + return self.tabs + + def close_out(self): + num = self.tabs.get_n_pages() + while num > 0: + num -= 1 + LOG.debug("Closing %i" % num) + try: + self.do_close(self.tabs.get_nth_page(num)) + except ModifiedError: + return False + + gtk.main_quit() + + return True + + def make_status_bar(self): + box = gtk.HBox(False, 2) + + self.sb_general = gtk.Statusbar() + self.sb_general.set_has_resize_grip(False) + self.sb_general.show() + box.pack_start(self.sb_general, 1, 1, 1) + + self.sb_radio = gtk.Statusbar() + self.sb_radio.set_has_resize_grip(True) + self.sb_radio.show() + box.pack_start(self.sb_radio, 1, 1, 1) + + box.show() + return box + + def ev_delete(self, window, event): + if not self.close_out(): + return True # Don't exit + + def ev_destroy(self, window): + if not self.close_out(): + return True # Don't exit + + def setup_extra_hotkeys(self): + accelg = self.menu_uim.get_accel_group() + + def memedit(a): + self.get_current_editorset().editors["memedit"].hotkey(a) + + actions = [ + # ("action_name", "key", function) + ] + + for name, key, fn in actions: + a = gtk.Action(name, name, name, "") + a.connect("activate", fn) + self.menu_ag.add_action_with_accel(a, key) + a.set_accel_group(accelg) + a.connect_accelerator() + + def _set_icon(self): + this_platform = platform.get_platform() + path = (this_platform.find_resource("chirp.png") or + this_platform.find_resource(os.path.join("pixmaps", + "chirp.png"))) + if os.path.exists(path): + self.set_icon_from_file(path) + else: + LOG.warn("Icon %s not found" % path) + + def _updates(self, version): + if not version: + return + + if version == CHIRP_VERSION: + return + + LOG.info("Server reports version %s is available" % version) + + # Report new updates every three days + intv = 3600 * 24 * 3 + + if CONF.is_defined("last_update_check", "state") and \ + (time.time() - CONF.get_int("last_update_check", "state")) < intv: + return + + CONF.set_int("last_update_check", int(time.time()), "state") + d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, parent=self, + type=gtk.MESSAGE_INFO) + d.label.set_markup( + _('A new version of CHIRP is available: ' + + '{ver}. '.format(ver=version) + + 'It is recommended that you upgrade as soon as possible. ' + 'Please go to: \r\n\r\n' + + 'http://chirp.danplanet.com')) + response = d.run() + d.destroy() + if response == gtk.RESPONSE_OK: + webbrowser.open('http://chirp.danplanet.com/' + 'projects/chirp/wiki/Download') + + def _init_macos(self, menu_bar): + macapp = None + + # for KK7DS runtime <= R10 + try: + import gtk_osxapplication + macapp = gtk_osxapplication.OSXApplication() + except ImportError: + pass + + # for gtk-mac-integration >= 2.0.7 + try: + import gtkosx_application + macapp = gtkosx_application.Application() + except ImportError: + pass + + if macapp is None: + LOG.error("No MacOS support: %s" % e) + return + + this_platform = platform.get_platform() + icon = (this_platform.find_resource("chirp.png") or + this_platform.find_resource(os.path.join("pixmaps", + "chirp.png"))) + if os.path.exists(icon): + icon_pixmap = gtk.gdk.pixbuf_new_from_file(icon) + macapp.set_dock_icon_pixbuf(icon_pixmap) + + menu_bar.hide() + macapp.set_menu_bar(menu_bar) + + quititem = self.menu_uim.get_widget("/MenuBar/file/quit") + quititem.hide() + + aboutitem = self.menu_uim.get_widget("/MenuBar/help/about") + macapp.insert_app_menu_item(aboutitem, 0) + + documentationitem = self.menu_uim.get_widget("/MenuBar/help/gethelp") + macapp.insert_app_menu_item(documentationitem, 0) + + macapp.set_use_quartz_accelerators(False) + macapp.ready() + + LOG.debug("Initialized MacOS support") + + def __init__(self, *args, **kwargs): + gtk.Window.__init__(self, *args, **kwargs) + + def expose(window, event): + allocation = window.get_allocation() + CONF.set_int("window_w", allocation.width, "state") + CONF.set_int("window_h", allocation.height, "state") + self.connect("expose_event", expose) + + def state_change(window, event): + CONF.set_bool( + "window_maximized", + event.new_window_state == gtk.gdk.WINDOW_STATE_MAXIMIZED, + "state") + self.connect("window-state-event", state_change) + + d = CONF.get("last_dir", "state") + if d and os.path.isdir(d): + platform.get_platform().set_last_dir(d) + + vbox = gtk.VBox(False, 2) + + self._recent = [] + + self.menu_ag = None + mbar = self.make_menubar() + + if os.name != "nt": + self._set_icon() # Windows gets the icon from the exe + if os.uname()[0] == "Darwin": + self._init_macos(mbar) + + vbox.pack_start(mbar, 0, 0, 0) + + self.tabs = None + tabs = self.make_tabs() + tabs.connect("switch-page", lambda n, _, p: self.ev_tab_switched(p)) + tabs.connect("page-removed", lambda *a: self.ev_tab_switched()) + tabs.show() + self.ev_tab_switched() + vbox.pack_start(tabs, 1, 1, 1) + + vbox.pack_start(self.make_status_bar(), 0, 0, 0) + + vbox.show() + + self.add(vbox) + + try: + width = CONF.get_int("window_w", "state") + height = CONF.get_int("window_h", "state") + except Exception: + width = 800 + height = 600 + + self.set_default_size(width, height) + if CONF.get_bool("window_maximized", "state"): + self.maximize() + self.set_title("CHIRP") + + self.connect("delete_event", self.ev_delete) + self.connect("destroy", self.ev_destroy) + + if not CONF.get_bool("warned_about_reporting") and \ + not CONF.get_bool("no_report"): + d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=self) + d.set_markup("" + + _("Error reporting is enabled") + + "") + d.format_secondary_markup( + _("If you wish to disable this feature you may do so in " + "the Help menu")) + d.run() + d.destroy() + CONF.set_bool("warned_about_reporting", True) + + self.update_recent_files() + try: + self.update_stock_configs() + except UnicodeDecodeError: + LOG.exception('We hit bug #272 while working with unicode paths. ' + 'Not copying stock configs so we can continue ' + 'startup.') + self.setup_extra_hotkeys() + + def updates_callback(ver): + gobject.idle_add(self._updates, ver) + + if not CONF.get_bool("skip_update_check", "state"): + reporting.check_for_updates(updates_callback)