[chirp_devel] [PATCH 1 of 3] [csv] Refactor line parsing to allow dialects. #533
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1360439120 28800 # Node ID f501ac229f9b1be1c24dab889b75ac40f27efd16 # Parent e917718c875d0b7931991811eb65ec9c627334ca [csv] Refactor line parsing to allow dialects. #533
diff -r e917718c875d -r f501ac229f9b chirp/generic_csv.py --- a/chirp/generic_csv.py Tue Feb 05 21:32:18 2013 -0800 +++ b/chirp/generic_csv.py Sat Feb 09 11:45:20 2013 -0800 @@ -106,6 +106,19 @@
return rf
+ def _clean(self, headers, line, mem): + """Runs post-processing functions on new mem objects. + + This is useful for parsing other CSV dialects when multiple columns + convert to a single Chirp column.""" + + for attr in dir(mem): + fname = "_clean_%s" % attr + if hasattr(self, fname): + mem = getattr(self, fname)(headers, line, mem) + + return mem + def _parse_csv_data_line(self, headers, line): mem = chirp_common.Memory() try: @@ -114,7 +127,11 @@ except OmittedHeaderError: pass
- for header, (typ, attr) in self.ATTR_MAP.items(): + for header in headers: + try: + typ, attr = self.ATTR_MAP[header] + except KeyError: + continue try: val = get_datum_by_header(headers, line, header) if not val and typ == int: @@ -128,7 +145,7 @@ except Exception, e: raise Exception("[%s] %s" % (attr, e))
- return mem + return self._clean(headers, line, mem)
def load(self, filename=None): if filename is None and self._filename is None:
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1360440250 28800 # Node ID 26774d27b348c139a013114d3588173617d3ad1d # Parent f501ac229f9b1be1c24dab889b75ac40f27efd16 [csv] Add support for reading CSV generated by RT Systems software. #477
diff -r f501ac229f9b -r 26774d27b348 chirp/chirp_common.py --- a/chirp/chirp_common.py Sat Feb 09 11:45:20 2013 -0800 +++ b/chirp/chirp_common.py Sat Feb 09 12:04:10 2013 -0800 @@ -185,6 +185,13 @@
def parse_freq(freqstr): """Parse a frequency string and return the value in integral Hz""" + 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(".") else: diff -r f501ac229f9b -r 26774d27b348 chirp/generic_csv.py --- a/chirp/generic_csv.py Sat Feb 09 11:45:20 2013 -0800 +++ b/chirp/generic_csv.py Sat Feb 09 12:04:10 2013 -0800 @@ -255,6 +255,93 @@ ",".join(self.memories[number].to_csv())
@classmethod - def match_model(cls, _filedata, filename): + def match_model(cls, filedata, filename): """Match files ending in .CSV""" - return filename.lower().endswith("." + cls.FILE_EXTENSION) + return filename.lower().endswith("." + cls.FILE_EXTENSION) and \ + (filedata.startswith("Location,") or filedata == "") + + +@directory.register +class RTCSVRadio(CSVRadio): + """A driver for reading CSV files generated by RT Systems software""" + VENDOR = "RT Systems" + MODEL = "CSV" + FILE_EXTENSION = "csv" + + DUPLEX_MAP = { + "Minus": "-", + "Plus": "+", + "Simplex": "", + "Split": "split", + } + + SKIP_MAP = { + "Off": "", + "On": "S", + "P Scan": "P", + "Skip": "S", + } + + TMODE_MAP = { + "None": "", + "T Sql": "TSQL", + } + + BOOL_MAP = { + "Off": False, + "On": True, + } + + ATTR_MAP = { + "Channel Number": (int, "number"), + "Receive Frequency":(chirp_common.parse_freq, "freq"), + "Offset Frequency": (chirp_common.parse_freq, "offset"), + "Offset Direction": (lambda v: RTCSVRadio.DUPLEX_MAP.get(v, v), "duplex"), + "Operating Mode": (str, "mode"), + "Name": (str, "name"), + "Tone Mode": (lambda v: RTCSVRadio.TMODE_MAP.get(v, v), "tmode"), + "CTCSS": (lambda v: float(v.split(" ")[0]), "rtone"), + "DCS": (int, "dtcs"), + "Skip": (lambda v: RTCSVRadio.SKIP_MAP.get(v, v), "skip"), + "Step": (lambda v: float(v.split(" ")[0]), "tuning_step"), + "Mask": (lambda v: RTCSVRadio.BOOL_MAP.get(v, v), "empty",), + "Comment": (str, "comment"), + } + + def _clean_duplex(self, headers, line, mem): + if mem.duplex == "split": + try: + val = get_datum_by_header(headers, line, "Transmit Frequency") + val = chirp_common.parse_freq(val) + mem.offset = val + except OmittedHeaderError: + pass + + return mem + + def _clean_mode(self, headers, line, mem): + if mem.mode == "FM": + try: + val = get_datum_by_header(headers, line, "Half Dev") + if self.BOOL_MAP[val]: + mem.mode = "FMN" + except OmittedHeaderError: + pass + + return mem + + def _clean_ctone(self, headers, line, mem): + # RT Systems only stores a single tone value + mem.ctone = mem.rtone + return mem + + @classmethod + def match_model(cls, filedata, filename): + """Match files ending in .csv and using RT Systems column names.""" + # RT Systems provides a different set of columns for each radio. + # We attempt to match only the first few columns, hoping they are + # consistent across radio models. + return filename.lower().endswith("." + cls.FILE_EXTENSION) and \ + filedata.startswith("Channel Number,Receive Frequency," + "Transmit Frequency,Offset Frequency,Offset Direction," + "Operating Mode,Name,Tone Mode,CTCSS,DCS")
- def match_model(cls, _filedata, filename):
- def match_model(cls, filedata, filename): """Match files ending in .CSV"""
return filename.lower().endswith("." + cls.FILE_EXTENSION)
return filename.lower().endswith("." + cls.FILE_EXTENSION)
and \
(filedata.startswith("Location,") or filedata == "")
Hmm, the CSV driver is willing to open empty files?
On Sat, Feb 9, 2013 at 1:59 PM, Dan Smith dsmith@danplanet.com wrote:
- def match_model(cls, _filedata, filename):
- def match_model(cls, filedata, filename): """Match files ending in .CSV"""
return filename.lower().endswith("." + cls.FILE_EXTENSION)
return filename.lower().endswith("." + cls.FILE_EXTENSION)
and \
(filedata.startswith("Location,") or filedata == "")
Hmm, the CSV driver is willing to open empty files?
Yes, I had to add that bit so that File > New would start working again :-)
Tom
Yes, I had to add that bit so that File > New would start working again :-)
Hmm, I think it's probably better to help File->New along than to make CSV claim to open empty files, would you agree? :)
On Sat, Feb 9, 2013 at 2:05 PM, Dan Smith dsmith@danplanet.com wrote:
Yes, I had to add that bit so that File > New would start working again :-)
Hmm, I think it's probably better to help File->New along than to make CSV claim to open empty files, would you agree? :)
It only claims to open empty files with filenames ending in .csv. This has been the behavior for years (since CSV was written).
If you want to change the File > New behavior, feel free, but that would be independent of these changes.
Tom
It only claims to open empty files with filenames ending in .csv. This has been the behavior for years (since CSV was written).
Okay, fair point.
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1360442666 28800 # Node ID 19cf8d11442dd5799a3d6759c8a544e6996e954f # Parent 26774d27b348c139a013114d3588173617d3ad1d Add support for Kenwood .itm files. #535
diff -r 26774d27b348 -r 19cf8d11442d chirp/kenwood_itm.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/kenwood_itm.py Sat Feb 09 12:44:26 2013 -0800 @@ -0,0 +1,130 @@ +# Copyright 2008 Dan Smith dsmith@danplanet.com +# Copyright 2012 Tom Hayward tom@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/. + +import csv + +from chirp import chirp_common, errors, directory, generic_csv + +class OmittedHeaderError(Exception): + """An internal exception to indicate that a header was omitted""" + pass + +@directory.register +class ITMRadio(generic_csv.CSVRadio): + """Kenwood ITM format""" + VENDOR = "Kenwood" + MODEL = "ITM" + FILE_EXTENSION = "itm" + + ATTR_MAP = { + "CH" : (int, "number"), + "RXF" : (chirp_common.parse_freq, "freq"), + "NAME" : (str, "name"), + } + + def _clean_duplex(self, headers, line, mem): + try: + txfreq = chirp_common.parse_freq( + generic_csv.get_datum_by_header(headers, line, "TXF")) + except ValueError: + mem.duplex = "off" + return mem + + if mem.freq == txfreq: + mem.duplex = "" + elif txfreq: + mem.duplex = "split" + mem.offset = txfreq + + return mem + + def _clean_number(self, headers, line, mem): + zone = int(generic_csv.get_datum_by_header(headers, line, "ZN")) + mem.number = zone * 100 + mem.number + return mem + + def _clean_tmode(self, headers, line, mem): + rtone = eval(generic_csv.get_datum_by_header(headers, line, "TXSIG")) + ctone = eval(generic_csv.get_datum_by_header(headers, line, "RXSIG")) + + if rtone: + mem.tmode = "Tone" + if ctone: + mem.tmode = "TSQL" + + mem.rtone = rtone or 88.5 + mem.ctone = ctone or mem.rtone + + return mem + + def load(self, filename=None): + if filename is None and self._filename is None: + raise errors.RadioError("Need a location to load from") + + if filename: + self._filename = filename + + self._blank() + + f = file(self._filename, "r") + for line in f: + if line.strip() == "// Conventional Data": + break + + reader = csv.reader(f, delimiter=chirp_common.SEPCHAR, quotechar='"') + + good = 0 + lineno = 0 + for line in reader: + lineno += 1 + if lineno == 1: + header = line + continue + + if len(line) == 0: + # End of channel data + break + + if len(header) > len(line): + print "Line %i has %i columns, expected %i" % (lineno, + len(line), + len(header)) + self.errors.append("Column number mismatch on line %i" % lineno) + continue + + # fix EU decimal + line = [i.replace(',','.') for i in line] + + try: + mem = self._parse_csv_data_line(header, line) + if mem.number is None: + raise Exception("Invalid Location field" % lineno) + except Exception, e: + print "Line %i: %s" % (lineno, e) + self.errors.append("Line %i: %s" % (lineno, e)) + continue + + self._grow(mem.number) + self.memories[mem.number] = mem + good += 1 + + if not good: + print self.errors + raise errors.InvalidDataError("No channels found") + + @classmethod + def match_model(cls, filedata, filename): + return filename.lower().endswith("." + cls.FILE_EXTENSION) diff -r 26774d27b348 -r 19cf8d11442d chirpui/mainapp.py --- a/chirpui/mainapp.py Sat Feb 09 12:04:10 2013 -0800 +++ b/chirpui/mainapp.py Sat Feb 09 12:44:26 2013 -0800 @@ -718,6 +718,7 @@ (_("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"),
participants (2)
-
Dan Smith
-
Tom Hayward