[chirp_devel] Information Prompt Patch
Rick DeWitt
Wed Jun 20 05:55:13 PDT 2018
Oops...
And Nope, not familiar with either one; all my s/w change tracking
experience has been with in-house methods.
Rick DeWitt
AA0RD
Sequim, Washington, USA
360-681-3494
On 6/19/2018 4:24 PM, Dan Smith via chirp_devel wrote:
>> OK, mercurial has been giving me fits trying to split these changesets
>> out. So here is a combined patch with the diffs for chirp_common,
>> mainapp and the lt725uv driver. All in support of issue #5889, adding
>> the information prompt at download.
> I think you forgot to attach it...
>
>> I'm still trying to get the ft450d driver change (adding the info prompt
>> verbage) to process, keep getting -
>> "abort: working directory revision is not qtip" from the "hg qnew -ef
>> ft450d_info" cmd.
> This looks like you maybe have moved things around since your last q-operation, like maybe if you did a commit, rollback or checkout. Does that make sense?
>
> I was asking you on the other thread and never got a response, but ... are you more familiar with git than mercurial? I got the impression that you weren't familiar with either, but was hoping to use you as a data point either way.
>
> Thanks!
>
> --Dan
> _______________________________________________
> chirp_devel mailing list
> chirp_devel at intrepid.danplanet.com
> http://intrepid.danplanet.com/mailman/listinfo/chirp_devel
> Developer docs: http://chirp.danplanet.com/projects/chirp/wiki/Developers
-------------- next part --------------
# HG changeset patch
# User Rick DeWitt <aa0rd at yahoo.com>
# 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 <dsmith at danplanet.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 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 <http://www.gnu.org/licenses/>.
-
-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 <dsmith at danplanet.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 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 <http://www.gnu.org/licenses/>.
+
+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, <rock.unroe at gmail.com>
-# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), <aa0rd at yahoo.com>#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-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
-
-
- at 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"
-
-
- at directory.register
-class Baojie218(LT725UV):
- """Baojie BJ-218"""
- VENDOR = "Baojie"
- MODEL = "BJ-218"
- ALIASES = [Zastone, Hesenate, ]
+# Copyright 2016:
+# * Jim Unroe KC9HI, <rock.unroe at gmail.com>
+# Modified for Baojie BJ-218: 2018 by Rick DeWitt (RJD), <aa0rd at yahoo.com>#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+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
+
+
+ at 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"
+
+
+ at 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 <dsmith at danplanet.com>
-# Copyright 2012 Tom Hayward <tom at tomh.us>
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-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("<b>-1</b> for either Mem # does a full-file hex " +
- "dump with diffs highlighted.\n" +
- "<b>-2</b> for first Mem # shows " +
- "<b>only</b> 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("""<b><big>Unable to detect model!</big></b>
-
-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("<big><b>" + _("Note:") + "</b></big>")
- msg = _("The {vendor} {model} operates in <b>live mode</b>. "
- "This means that any changes you make are immediately sent "
- "to the radio. Because of this, you cannot perform the "
- "<u>Save</u> or <u>Upload</u> operations. If you wish to "
- "edit the contents offline, please <u>Export</u> to a CSV "
- "file, using the <b>File menu</b>.")
- 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("<big><b>" + _("{name} Instructions").format(
- name=radio.get_name()) + "</b></big>")
- 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("<big><big><b>RepeaterBook</b></big>\r\n" +
- "<i>North American Repeater Directory</i></big>")
- 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 <dsmith at danplanet.com>",
- _("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 = "<b><big>" + _("Reporting is disabled") + "</big></b>"
- d.set_markup(markup)
- msg = _("The reporting feature of CHIRP is designed to help "
- "<u>improve quality</u> 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 <u>please consider leaving "
- "this feature enabled to help make CHIRP better!</u>\n\n"
- "<b>Are you sure you want to disable this feature?</b>")
- 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 = """
-<ui>
- <menubar name="MenuBar">
- <menu action="file">
- <menuitem action="new"/>
- <menuitem action="open"/>
- <menu action="openstock" name="openstock"/>
- <menu action="recent" name="recent"/>
- <menuitem action="save"/>
- <menuitem action="saveas"/>
- <menuitem action="loadmod"/>
- <separator/>
- <menuitem action="import"/>
- <menuitem action="export"/>
- <separator/>
- <menuitem action="close"/>
- <menuitem action="quit"/>
- </menu>
- <menu action="edit">
- <menuitem action="cut"/>
- <menuitem action="copy"/>
- <menuitem action="paste"/>
- <menuitem action="delete"/>
- <separator/>
- <menuitem action="all"/>
- <separator/>
- <menuitem action="move_up"/>
- <menuitem action="move_dn"/>
- <menuitem action="exchange"/>
- <separator/>
- <menuitem action="properties"/>
- </menu>
- <menu action="view">
- <menuitem action="columns"/>
- <menuitem action="hide_unused"/>
- <menuitem action="no_smart_tmode"/>
- <menu action="viewdeveloper">
- <menuitem action="devshowraw"/>
- <menuitem action="devdiffraw"/>
- <menuitem action="devdifftab"/>
- </menu>
- <menuitem action="language"/>
- </menu>
- <menu action="radio" name="radio">
- <menuitem action="download"/>
- <menuitem action="upload"/>
- <menu action="importsrc" name="importsrc">
- <menuitem action="idmrmarc"/>
- <menuitem action="iradioreference"/>
- <menu action="irbook" name="irbook">
- <menuitem action="irbookpolitical"/>
- <menuitem action="irbookproximity"/>
- </menu>
- <menuitem action="ipr"/>
- <menuitem action="irfinder"/>
- </menu>
- <menu action="querysrc" name="querysrc">
- <menuitem action="qdmrmarc"/>
- <menuitem action="qradioreference"/>
- <menu action="qrbook" name="qrbook">
- <menuitem action="qrbookpolitical"/>
- <menuitem action="qrbookproximity"/>
- </menu>
- <menuitem action="qpr"/>
- <menuitem action="qrfinder"/>
- </menu>
- <menu action="stock" name="stock"/>
- <separator/>
- <menuitem action="channel_defaults"/>
- <separator/>
- <menuitem action="cancelq"/>
- </menu>
- <menu action="help">
- <menuitem action="gethelp"/>
- <separator/>
- <menuitem action="report"/>
- <menuitem action="clone_instructions"/>
- <menuitem action="developer"/>
- <separator/>
- <menuitem action="about"/>
- </menu>
- </menubar>
-</ui>
-"""
- ALT_KEY = "<Alt>"
- CTRL_KEY = "<Ctrl>"
- if sys.platform == 'darwin':
- ALT_KEY = "<Meta>"
- CTRL_KEY = "<Meta>"
- 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"),
- "%s<Shift>x" % 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'),
- "%s<Shift>r" % CTRL_KEY, None, self.mh),
- ('devdiffraw', None, _("Diff raw memories"),
- "%s<Shift>d" % CTRL_KEY, None, self.mh),
- ('devdifftab', None, _("Diff tabs"),
- "%s<Shift>t" % 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<a href="http://chirp.danplanet.com">' +
- 'http://chirp.danplanet.com</a>'))
- 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("<b><big>" +
- _("Error reporting is enabled") +
- "</big></b>")
- d.format_secondary_markup(
- _("If you wish to disable this feature you may do so in "
- "the <u>Help</u> 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 <dsmith at danplanet.com>
+# Copyright 2012 Tom Hayward <tom at tomh.us>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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("<b>-1</b> for either Mem # does a full-file hex " +
+ "dump with diffs highlighted.\n" +
+ "<b>-2</b> for first Mem # shows " +
+ "<b>only</b> 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("""<b><big>Unable to detect model!</big></b>
+
+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("<big><b>" + _("Note:") + "</b></big>")
+ msg = _("The {vendor} {model} operates in <b>live mode</b>. "
+ "This means that any changes you make are immediately sent "
+ "to the radio. Because of this, you cannot perform the "
+ "<u>Save</u> or <u>Upload</u> operations. If you wish to "
+ "edit the contents offline, please <u>Export</u> to a CSV "
+ "file, using the <b>File menu</b>.")
+ 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("<big><b>" + _("{name} Information").format(
+ name=radio.get_name()) + "</b></big>")
+ 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("<big><b>" + _("{name} Instructions").format(
+ name=radio.get_name()) + "</b></big>")
+ 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("<big><big><b>RepeaterBook</b></big>\r\n" +
+ "<i>North American Repeater Directory</i></big>")
+ 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 <dsmith at danplanet.com>",
+ _("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 = "<b><big>" + _("Reporting is disabled") + "</big></b>"
+ d.set_markup(markup)
+ msg = _("The reporting feature of CHIRP is designed to help "
+ "<u>improve quality</u> 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 <u>please consider leaving "
+ "this feature enabled to help make CHIRP better!</u>\n\n"
+ "<b>Are you sure you want to disable this feature?</b>")
+ 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 = """
+<ui>
+ <menubar name="MenuBar">
+ <menu action="file">
+ <menuitem action="new"/>
+ <menuitem action="open"/>
+ <menu action="openstock" name="openstock"/>
+ <menu action="recent" name="recent"/>
+ <menuitem action="save"/>
+ <menuitem action="saveas"/>
+ <menuitem action="loadmod"/>
+ <separator/>
+ <menuitem action="import"/>
+ <menuitem action="export"/>
+ <separator/>
+ <menuitem action="close"/>
+ <menuitem action="quit"/>
+ </menu>
+ <menu action="edit">
+ <menuitem action="cut"/>
+ <menuitem action="copy"/>
+ <menuitem action="paste"/>
+ <menuitem action="delete"/>
+ <separator/>
+ <menuitem action="all"/>
+ <separator/>
+ <menuitem action="move_up"/>
+ <menuitem action="move_dn"/>
+ <menuitem action="exchange"/>
+ <separator/>
+ <menuitem action="properties"/>
+ </menu>
+ <menu action="view">
+ <menuitem action="columns"/>
+ <menuitem action="hide_unused"/>
+ <menuitem action="no_smart_tmode"/>
+ <menu action="viewdeveloper">
+ <menuitem action="devshowraw"/>
+ <menuitem action="devdiffraw"/>
+ <menuitem action="devdifftab"/>
+ </menu>
+ <menuitem action="language"/>
+ </menu>
+ <menu action="radio" name="radio">
+ <menuitem action="download"/>
+ <menuitem action="upload"/>
+ <menu action="importsrc" name="importsrc">
+ <menuitem action="idmrmarc"/>
+ <menuitem action="iradioreference"/>
+ <menu action="irbook" name="irbook">
+ <menuitem action="irbookpolitical"/>
+ <menuitem action="irbookproximity"/>
+ </menu>
+ <menuitem action="ipr"/>
+ <menuitem action="irfinder"/>
+ </menu>
+ <menu action="querysrc" name="querysrc">
+ <menuitem action="qdmrmarc"/>
+ <menuitem action="qradioreference"/>
+ <menu action="qrbook" name="qrbook">
+ <menuitem action="qrbookpolitical"/>
+ <menuitem action="qrbookproximity"/>
+ </menu>
+ <menuitem action="qpr"/>
+ <menuitem action="qrfinder"/>
+ </menu>
+ <menu action="stock" name="stock"/>
+ <separator/>
+ <menuitem action="channel_defaults"/>
+ <separator/>
+ <menuitem action="cancelq"/>
+ </menu>
+ <menu action="help">
+ <menuitem action="gethelp"/>
+ <separator/>
+ <menuitem action="report"/>
+ <menuitem action="clone_information"/>
+ <menuitem action="clone_instructions"/>
+ <menuitem action="developer"/>
+ <separator/>
+ <menuitem action="about"/>
+ </menu>
+ </menubar>
+</ui>
+"""
+ ALT_KEY = "<Alt>"
+ CTRL_KEY = "<Ctrl>"
+ if sys.platform == 'darwin':
+ ALT_KEY = "<Meta>"
+ CTRL_KEY = "<Meta>"
+ 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"),
+ "%s<Shift>x" % 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'),
+ "%s<Shift>r" % CTRL_KEY, None, self.mh),
+ ('devdiffraw', None, _("Diff raw memories"),
+ "%s<Shift>d" % CTRL_KEY, None, self.mh),
+ ('devdifftab', None, _("Diff tabs"),
+ "%s<Shift>t" % 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<a href="http://chirp.danplanet.com">' +
+ 'http://chirp.danplanet.com</a>'))
+ 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("<b><big>" +
+ _("Error reporting is enabled") +
+ "</big></b>")
+ d.format_secondary_markup(
+ _("If you wish to disable this feature you may do so in "
+ "the <u>Help</u> 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)
More information about the chirp_devel
mailing list