[chirp_devel] [PATCH] Add import support for Kenwood *.hmk files
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1333491695 21600 # Node ID 1bb3df3d624fa4ee7dfc959b1dc6bd42a5e019f0 # Parent 91be43cc7ac4063d921b0d8eb0bdd73fdb9aa9fa Add import support for Kenwood *.hmk files.
diff -r 91be43cc7ac4 -r 1bb3df3d624f chirp/directory.py --- a/chirp/directory.py Tue Apr 03 11:19:10 2012 -0700 +++ b/chirp/directory.py Tue Apr 03 16:21:35 2012 -0600 @@ -91,6 +91,9 @@ if image_file.lower().endswith(".csv"): return get_radio("Generic_CSV")(image_file)
+ if image_file.lower().endswith(".hmk"): + return get_radio("Kenwood_HMK")(image_file) + if icf.is_9x_icf(image_file): return get_radio("Icom_IC91_92AD_ICF")(image_file)
diff -r 91be43cc7ac4 -r 1bb3df3d624f chirp/kenwood_hmk.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/kenwood_hmk.py Tue Apr 03 16:21:35 2012 -0600 @@ -0,0 +1,227 @@ +# Copyright 2008 Dan Smith dsmith@danplanet.com +# Copyright 2012 Tom Haywward 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 os +import csv + +from chirp import chirp_common, errors, directory + +class OmittedHeaderError(Exception): + pass + +@directory.register +class HMKRadio(chirp_common.CloneModeRadio): + VENDOR = "Kenwood" + MODEL = "HMK" + FILE_EXTENSION = "hmk" + + ATTR_MAP = { + "!!Ch" : (int, "number"), + "M.Name" : (str, "name"), + "Rx Freq." : (chirp_common.parse_freq, "freq"), + "Shift/Split" : (str, "duplex"), + "Offset" : (chirp_common.parse_freq, "offset"), + "T/CT/DCS" : (str, "tmode"), + "TO Freq." : (float, "rtone"), + "CT Freq." : (float, "ctone"), + "DCS Code" : (int, "dtcs"), + "Mode" : (str, "mode"), + "Tx Freq." : (chirp_common.parse_freq, "txfreq"), + "Rx Step" : (float, "tuning_step"), + "L.Out" : (str, "skip"), + } + + TMODE_MAP = { + "Off": "", + "T": "Tone", + "CT": "TSQL", + "DCS": "DTCS", + "": "Cross", + } + + SKIP_MAP = { + "Off": "", + "On": "S", + } + + DUPLEX_MAP = { + " ": "", + "S": "split", + "+": "+", + "-": "-", + } + + def _blank(self): + self.errors = [] + self.memories = [] + for i in range(0, 1000): + m = chirp_common.Memory() + m.number = i + m.empty = True + self.memories.append(m) + + def __init__(self, pipe): + chirp_common.CloneModeRadio.__init__(self, None) + + self._filename = pipe + if self._filename and os.path.exists(self._filename): + self.load() + else: + self._blank() + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_dtcs_polarity = False + rf.memory_bounds = (0, len(self.memories)) + rf.has_infinite_number = True + + rf.valid_modes = list(chirp_common.MODES) + rf.valid_tmodes = list(chirp_common.TONE_MODES) + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS) + rf.valid_bands = [(1, 10000000000)] + rf.valid_skips = ["", "S"] + rf.valid_characters = chirp_common.CHARSET_ASCII + rf.valid_name_length = 999 + + return rf + + def _parse_quoted_line(self, line): + line = line.replace("\n", "") + line = line.replace("\r", "") + line = line.replace('"', "") + + return line.split(",") + + def _get_datum_by_header(self, headers, data, header): + if header not in headers: + raise OmittedHeaderError("Header %s not provided" % header) + + try: + return data[headers.index(header)] + except IndexError: + raise OmittedHeaderError("Header %s not provided on this line" %\ + header) + + def _parse_csv_data_line(self, headers, line): + mem = chirp_common.Memory() + odd_split = False + + for header, (typ, attr) in self.ATTR_MAP.items(): + try: + val = self._get_datum_by_header(headers, line, header) + if not val and typ == int: + val = None + elif attr == "duplex": + val = typ(self.DUPLEX_MAP[val]) + if val == "split": + odd_split = True + elif attr == "skip": + val = typ(self.SKIP_MAP[val]) + elif attr == "tmode": + val = typ(self.TMODE_MAP[val]) + elif attr == 'txfreq': + tx_freq = typ(val) + else: + val = typ(val) + if hasattr(mem, attr): + setattr(mem, attr, val) + except OmittedHeaderError, e: + pass + except Exception, e: + raise Exception("[%s] %s" % (attr, e)) + + if odd_split: + mem.offset = tx_freq + + 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 i in range(0, 10): + f.readline().strip() + + #f.seek(0, 0) + 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(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 + + 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") + + def load_mmap(self, filename): + return self.load(filename) + + def get_memories(self, lo=0, hi=999): + return [x for x in self.memories if x.number >= lo and x.number <= hi] + + def get_memory(self, number): + try: + return self.memories[number] + except: + raise errors.InvalidMemoryLocation("No such memory %s" % number) + + def __grow(self, target): + delta = target - len(self.memories) + if delta < 0: + return + + delta += 1 + + for i in range(len(self.memories), len(self.memories) + delta + 1): + m = chirp_common.Memory() + m.empty = True + m.number = i + self.memories.append(m) + + def get_raw_memory(self, number): + return ",".join(self.memories[number].to_csv()) diff -r 91be43cc7ac4 -r 1bb3df3d624f chirpui/mainapp.py --- a/chirpui/mainapp.py Tue Apr 03 11:19:10 2012 -0700 +++ b/chirpui/mainapp.py Tue Apr 03 16:21:35 2012 -0600 @@ -667,6 +667,7 @@ (_("CSV Files") + " (*.csv)", "*.csv"), (_("EVE Files (VX5)") + " (*.eve)", "*.eve"), (_("ICF Files") + " (*.icf)", "*.icf"), + (_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"), (_("VX5 Commander Files") + " (*.vx5)", "*.vx5"), (_("VX7 Commander Files") + " (*.vx7)", "*.vx7")] filen = platform.get_platform().gui_open_file(types=types)
diff -r 91be43cc7ac4 -r 1bb3df3d624f chirp/directory.py --- a/chirp/directory.py Tue Apr 03 11:19:10 2012 -0700 +++ b/chirp/directory.py Tue Apr 03 16:21:35 2012 -0600 @@ -91,6 +91,9 @@ if image_file.lower().endswith(".csv"): return get_radio("Generic_CSV")(image_file)
- if image_file.lower().endswith(".hmk"):
return get_radio("Kenwood_HMK")(image_file)
- if icf.is_9x_icf(image_file): return get_radio("Icom_IC91_92AD_ICF")(image_file)
This follows the existing pattern, but I think the existing pattern is ugly. I can say that, since it's mine.
I suggested on IRC that you use match_model(), and then realized when I saw the above that match_model() isn't used for CSV (or chirp) files, and can't be for extension-based matching. What was I thinking.
I'll send a patch to the list in a minute that will enable doing so, and convert the three cases there to use match_model() instead. Let me know what you think about doing it that way instead.
+class HMKRadio(chirp_common.CloneModeRadio):
I was thinking you would inherit from CSVRadio and hopefully eliminate the need to duplicate all that work. Was there some reason this didn't work out?
- def _blank(self):
self.errors = []
self.memories = []
for i in range(0, 1000):
m = chirp_common.Memory()
m.number = i
m.empty = True
self.memories.append(m)
I don't think you need _blank() if you're not going to support editing, right? If you inherit it from CSVRadio, it's one thing, but I think it'll confuse a future developer if it's here for no reason.
- def __init__(self, pipe):
chirp_common.CloneModeRadio.__init__(self, None)
self._filename = pipe
if self._filename and os.path.exists(self._filename):
self.load()
else:
self._blank()
It's an error if we're instantiated without a filename.
- def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_bank = False
rf.has_dtcs_polarity = False
rf.memory_bounds = (0, len(self.memories))
rf.has_infinite_number = True
rf.valid_modes = list(chirp_common.MODES)
rf.valid_tmodes = list(chirp_common.TONE_MODES)
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_tuning_steps = list(chirp_common.TUNING_STEPS)
rf.valid_bands = [(1, 10000000000)]
rf.valid_skips = ["", "S"]
rf.valid_characters = chirp_common.CHARSET_ASCII
rf.valid_name_length = 999
return rf
If you inherit from CSVRadio, then these are fine, but I think they are probably unnecessary if you're import-only, right?
- def _parse_quoted_line(self, line):
line = line.replace("\n", "")
line = line.replace("\r", "")
line = line.replace('"', "")
return line.split(",")
This is dead code from the CSV driver. Lets remove it from there and not replicate it here :)
- def _parse_csv_data_line(self, headers, line):
mem = chirp_common.Memory()
odd_split = False
for header, (typ, attr) in self.ATTR_MAP.items():
try:
val = self._get_datum_by_header(headers, line, header)
if not val and typ == int:
val = None
elif attr == "duplex":
val = typ(self.DUPLEX_MAP[val])
if val == "split":
odd_split = True
Hmm, what do the Tx Freq. and Offset columns look like in the two cases? Seems like we should be able to do better than this.
elif attr == "skip":
val = typ(self.SKIP_MAP[val])
elif attr == "tmode":
val = typ(self.TMODE_MAP[val])
Instead of special-casing these, why not use a function like the frequency parsing case? Something like this:
ATTR_MAP = { ... "T/CT/DCS" : (lambda v: self.TMODE_MAP[v], "tmode"), "L.Out" : (lambda v: self.SKIP_MAP[v], "skip"), ... }
That way you can just cover all the cases with this:
else:
val = typ(val)
...like the CSV driver does. Plus, if we can figure out something sneaky for the Offset and Tx Freq. cases, then you could still inherit from CSVRadio with a different ATTR_MAP :)
- 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()
I think you can remove this.
f = file(self._filename, "r")
for i in range(0, 10):
f.readline().strip()
Is it always ten lines? Maybe it would be better to probe smartly to chew up everything until the header row?
#f.seek(0, 0)
Did you intend to leave this in or take it out?
On Tue, Apr 3, 2012 at 18:53, Dan Smith dsmith@danplanet.com wrote:
I'll send a patch to the list in a minute that will enable doing so, and convert the three cases there to use match_model() instead. Let me know what you think about doing it that way instead.
Looks good. I'll get this patch updated to use it.
+class HMKRadio(chirp_common.CloneModeRadio):
I was thinking you would inherit from CSVRadio and hopefully eliminate the need to duplicate all that work. Was there some reason this didn't work out?
I was hoping to inherit CSVRadio, but CSVRadio inherits IcomDstarSupport and I don't want that. If I inherit from CSVRadio, is there a way to disable the Dstar stuff?
At first when I supported File > Open, I was getting a Dstar tab I didn't want. Now that it's import-only, this might not be an issue. I just don't want to see Dstar columns/features.
I don't think you need _blank() if you're not going to support editing, right? If you inherit it from CSVRadio, it's one thing, but I think it'll confuse a future developer if it's here for no reason.
Indeed. That is leftover copypasta that I meant to remove.
- def __init__(self, pipe):
- chirp_common.CloneModeRadio.__init__(self, None)
- self._filename = pipe
- if self._filename and os.path.exists(self._filename):
- self.load()
- else:
- self._blank()
It's an error if we're instantiated without a filename.
Added to my list of fixes.
- def get_features(self):
If you inherit from CSVRadio, then these are fine, but I think they are probably unnecessary if you're import-only, right?
Ok, are you saying get_features() is not used during import? I can remove it if it's not used.
- def _parse_quoted_line(self, line):
- line = line.replace("\n", "")
- line = line.replace("\r", "")
- line = line.replace('"', "")
- return line.split(",")
This is dead code from the CSV driver. Lets remove it from there and not replicate it here :)
Ok
- def _parse_csv_data_line(self, headers, line):
- mem = chirp_common.Memory()
- odd_split = False
- for header, (typ, attr) in self.ATTR_MAP.items():
- try:
- val = self._get_datum_by_header(headers, line, header)
- if not val and typ == int:
- val = None
- elif attr == "duplex":
- val = typ(self.DUPLEX_MAP[val])
- if val == "split":
- odd_split = True
Hmm, what do the Tx Freq. and Offset columns look like in the two cases? Seems like we should be able to do better than this.
In this loop we only have access to one column at a time, so I am saving the vars odd_split and tx_freq to a broader scope and making the final assignment outside the loop. What exactly are you suggesting?
- elif attr == "skip":
- val = typ(self.SKIP_MAP[val])
- elif attr == "tmode":
- val = typ(self.TMODE_MAP[val])
Instead of special-casing these, why not use a function like the frequency parsing case? Something like this:
ATTR_MAP = { ... "T/CT/DCS" : (lambda v: self.TMODE_MAP[v], "tmode"), "L.Out" : (lambda v: self.SKIP_MAP[v], "skip"), ... }
That way you can just cover all the cases with this:
You are strong in the ways of Python.
- else:
- val = typ(val)
...like the CSV driver does. Plus, if we can figure out something sneaky for the Offset and Tx Freq. cases, then you could still inherit from CSVRadio with a different ATTR_MAP :)
- 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()
I think you can remove this.
Ok
- f = file(self._filename, "r")
- for i in range(0, 10):
- f.readline().strip()
Is it always ten lines? Maybe it would be better to probe smartly to chew up everything until the header row?
I have three example hmk files from three different Kenwood programming software and they are all 10 lines. But yes, I think I can do better. The header line always starts with !!Ch, so I can look for that, back up one line, and send that to the csv reader.
- #f.seek(0, 0)
Did you intend to leave this in or take it out?
This needs to be removed. Seeking back to 0 would obviously make my 10-line skip worthless :)
-- Dan Smith www.danplanet.com KK7DS
Tom KD7LXL
I was hoping to inherit CSVRadio, but CSVRadio inherits IcomDstarSupport and I don't want that. If I inherit from CSVRadio, is there a way to disable the Dstar stuff?
Ah, right. Dang. I wonder what would be involved in making a base CSV driver that doesn't support D-STAR and then subclassing it for the actual one that does? I'll take a look.
Ok, are you saying get_features() is not used during import? I can remove it if it's not used.
No, it is, but all the valid_* features are unnecessary. Not a big deal to leave them in I guess, it just seems confusing to put stuff there that is not needed.
In this loop we only have access to one column at a time, so I am saving the vars odd_split and tx_freq to a broader scope and making the final assignment outside the loop. What exactly are you suggesting?
Looking back at your hmk example, it looks like the Tx Freq. column always contains the transmit frequency, regardless of whether it's an odd or normal split, and even regardless of the duplex, right? I was just hoping to avoid the special casing there to allow for more re-use from the CSV driver, but maybe that isn't as easy.
I have three example hmk files from three different Kenwood programming software and they are all 10 lines. But yes, I think I can do better. The header line always starts with !!Ch, so I can look for that, back up one line, and send that to the csv reader.
Or maybe just read and parse and keep throwing stuff away until the first column is !!Ch, to avoid the "going back a line" behavior?
participants (2)
-
Dan Smith
-
Tom Hayward