[chirp_devel] [PATCH 4 of 4] Add MD-380. #3755
Tom Hayward
Fri Sep 16 16:23:40 PDT 2016
# HG changeset patch
# User Tom Hayward <tom at tomh.us>
# Date 1474068139 25200
# Fri Sep 16 16:22:19 2016 -0700
# Node ID f53339aa5068ae5e91ada48651737549fed64e4a
# Parent be978830736869866b91da00826548ad563fa9eb
Add MD-380. #3755
diff -r be9788307368 -r f53339aa5068 chirp/drivers/md380.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/md380.py Fri Sep 16 16:22:19 2016 -0700
@@ -0,0 +1,657 @@
+# Copyright 2015 Travis Goodspeed <travis at radiantmachines.com>
+# Copyright 2016 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 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/>.
+
+
+
+# This is an incomplete and bug-ridden attempt at a chirp driver for
+# the TYT MD-380 by Travis Goodspeed, KK4VCZ. To use this plugin,
+# copy or symlink it into the drivers/ directory of Chirp.
+#
+# You probably want to read your radio's image with 'md380-dfu read
+# radio.img' and then open it as a file with chirpw.
+
+
+from chirp import chirp_common, directory, memmap
+from chirp import bitwise, errors
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+ RadioSettingValueInteger, RadioSettingValueList, \
+ RadioSettingValueBoolean, RadioSettingValueString, \
+ RadioSettingValueMap, InvalidValueError, RadioSettings
+
+import logging
+LOG = logging.getLogger(__name__)
+
+# Someday I'll figure out Chinese encoding, but for now we'll stick to ASCII.
+CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
+ [chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
+ [" ", ] + \
+ [chr(x) for x in range(ord("a"), ord("z") + 1)] + \
+ list(".,:;*#_-/&()@!?^ +") + list("\x00" * 100)
+DUPLEX = ["", "-", "+", "split"];
+#TODO 'DMR' should be added as a valid mode.
+MODES = ["FM", "NFM", "DIG"];
+
+# Here is where we define the memory map for the radio. Since
+# We often just know small bits of it, we can use #seekto to skip
+# around as needed.
+#
+# Large parts of this have yet to be reverse engineered, but I'm
+# getting there slowly.
+MEM_FORMAT = """
+
+#seekto 0x2180;
+struct {
+ char messages[288]; // 144 half-length characters, like always.
+} messages[50];
+
+
+#seekto 0x0005F80;
+struct {
+ ul24 callid; //DMR Call ID
+ u8 flags; //c2 for private with no tone
+ //e1 for a group call with an rx tone.
+ char name[32]; //U16L chars, of course.
+} contacts[1000];
+
+#seekto 0x0000ec20;
+struct {
+ char name[32];
+ ul16 members[32];
+} grouplists[250];
+
+#seekto 0x00018860;
+struct {
+ char name[32]; //UTF16L, like always.
+ u8 flags[10]; //last channel, priority, hold timing, sample time.
+ ul16 members[31]; //Just a list; unused entries are zeroed.
+} scanlists[20];
+
+#seekto 0x0001EE00;
+struct {
+ u8 loneworker:1,
+ unknown1a:1,
+ squelch:1, // 0: Tight, 1: Normal
+ autoscan:1,
+ fmbw:2, // 0: 12.5 KHz, 1: 20 KHz, 2: 25 KHz
+ mode:2; // 1: analog, 2: digital
+ u8 color_code:4, // 0-15
+ slot:2, // slot 1: 1, slot 2: 2, never zero
+ rxonly:1, // 1 for tx inhibit
+ allow_talkaround:1;
+ u8 data_call_confirmed:1,
+ priv_call_confirmed:1,
+ priv_mode:2, // 0 for cleartex, 1 for Basic Privacy, 2 for Enhanced Privacy.
+ priv_key:4; // key index. (E is slot 15, 0 is slot 1.)
+ u8 displaypttid:1,// 0: Display PTT ID, 1: default
+ compressed_udp_header:1, // 0: compressed_udp_header, 1: default
+ was2:2,
+ emergency_alarm_ack:1,
+ was0:1,
+ rxreffreq:2; // [Low, Med, High]
+ u8 admit:2, // [Always, Channel Free, Correct CTCSS, Color Code]
+ high_power:1,
+ vox:1,
+ qtreverse:1, // 0: 180, 1: 120
+ reverse_burst:1,
+ txreffreq:2; // [Low, Med, High]
+ u8 wasc3; //Unknown, normally C3
+ ul16 contact; // index in contacts array
+ u8 tot; // n * 15 seconds, 0 is infinite, 4 (60s) is default
+ u8 totrekey; // TOT Rekey Delay (s)
+ u8 emergency_system; // 0: None, 1-32: emergency system number
+ u8 scanlist; //Not yet supported.
+ u8 grouplist; //DMR Group list index. TODO
+ u8 was01; // 0: analog, 1: digital, function unknown
+ u8 decode8:1, // RX Signaling System Decode n
+ decode7:1,
+ decode6:1,
+ decode5:1,
+ decode4:1,
+ decode3:1,
+ decode2:1,
+ decode1:1;
+ u8 wasFF;
+ lbcd rxfreq[4];
+ lbcd txfreq[4]; //Stored as frequency, not offset.
+ lbcd rxtone[2]; //Receiver tone. (0xFFFF when unused.)
+ lbcd txtone[2]; //Transmitter tone.
+ u8 rxsignaling; // [Off, DTMF-1, DTMF-2, DTMF-3, DTMF-4]
+ u8 txsignaling; // [Off, DTMF-1, DTMF-2, DTMF-3, DTMF-4]
+ u8 yourguess[2];
+ char name[32]; //UTF16-LE
+} memory[1000];
+
+
+#seekto 0x1200;
+struct {
+ char sn[8];
+ char model[8];
+ char code[16];
+ u8 empty[8];
+ lbcd prog_yr[2];
+ lbcd prog_mon;
+ lbcd prog_day;
+ u8 empty_10f2c[4];
+} info;
+
+
+#seekto 0x149e0;
+struct {
+ char name[32]; //UTF16-LE
+ ul16 members[16]; //16 members for 16 positions on the dial
+} bank[99];
+
+
+#seekto 0x2000;
+struct {
+ u8 unknownff;
+ bbcd prog_yr[2];
+ bbcd prog_mon;
+ bbcd prog_day;
+ bbcd prog_hour;
+ bbcd prog_min;
+ bbcd prog_sec;
+ u8 unknownver[4]; //Probably version numbers.
+ u8 unknownff2[52]; //Maybe unused? All FF.
+ char line1[20]; //Top line of text at startup.
+ char line2[20]; //Bottom line of text at startup.
+ u8 unknownff3[24]; //all FF
+ u8 flags1; //FE
+ u8 flags2; //6B for no beeps, 6F will all beeps.
+ u8 flags3; //EE
+ u8 flags4; //FF
+ ul32 dmrid; //0x2084
+ u8 flags5[13]; //Unknown settings, seem mostly used.
+ u8 screenlit; //00 for infinite delay, 01 for 5s, 02 for 10s, 03 for 15s.
+ u8 unknownff4[2];
+ u8 unknownzeroes[8];
+ u8 unknownff5[16];
+ u32 radioname[32]; //Like all other strings.
+} general;
+
+
+#seekto 0x2f003;
+u8 selectedzone;
+
+
+
+"""
+
+def blankbcd(num):
+ """Sets an LBCD value to 0xFFFF"""
+ num[0].set_bits(0xFF);
+ num[1].set_bits(0xFF);
+
+
+def decode_tone(raw):
+ val = int(raw)
+ if raw.get_raw() == "\xff\xff":
+ return None, None, None
+ elif val > 8000:
+ # example: 8023: DTCS 023N, 12023: DTCS 023R
+ return "DTCS", val % 4000, val > 12000 and "R" or "N"
+ else:
+ return "Tone", val / 10.0, None
+
+
+def encode_tone(_mem, attr, mode, val, pol):
+ if mode == "Tone":
+ if val not in chirp_common.TONES:
+ raise errors.RadioError("Invalid tone: %f" % val)
+ setattr(_mem, attr, int(val * 10))
+ elif mode == "DTCS":
+ setattr(_mem, attr, val + (pol == "R" and 12000 or 8000))
+ else:
+ getattr(_mem, attr)[0].set_raw("\xff")
+ getattr(_mem, attr)[1].set_raw("\xff")
+
+
+def do_download(radio):
+ """Dummy function that will someday download from the radio."""
+ # NOTE: Remove this in your real implementation!
+ #return memmap.MemoryMap("\x00" * 262144)
+
+ # Get the serial port connection
+ serial = radio.pipe
+
+ # Our fake radio is just a simple download of 262144 bytes
+ # from the serial port. Do that one byte at a time and
+ # store them in the memory map
+ data = ""
+ for _i in range(0, 262144):
+ data = serial.read(1)
+
+ return memmap.MemoryMap(data)
+
+
+def utftoasc(utfstring):
+ """Converts a UTF16 string to ASCII."""
+ return str(str(utfstring).decode('utf-16le').rstrip("\x00"))
+
+
+def asctoutf(ascstring, size):
+ """Converts an ASCII string to UTF16."""
+ return ascstring.encode('utf-16le')[:size].ljust(size, "\x00")
+
+
+class MD380Bank(chirp_common.NamedBank):
+ def get_name(self):
+ _bank = getattr(self._radio._memobj,
+ self._attr_name)[self.index];
+ name = utftoasc(str(_bank.name));
+ return name.rstrip();
+
+ def set_name(self, name):
+ name = name.upper()
+ _bank = getattr(self._radio._memobj,
+ self._attr_name)[self.index];
+ _bank.name = asctoutf(name,32);
+
+
+class MD380BankModel(chirp_common.MTOBankModel):
+ """An MD380 Bank model"""
+ def get_num_mappings(self):
+ return self._num_mappings
+ #return len(self.get_mappings());
+
+ def get_mappings(self):
+ banks = []
+ for i in range(0, self._num_mappings):
+ #bank = chirp_common.Bank(self, "%i" % (i+1), "MG%i" % (i+1))
+ bank = MD380Bank(self, "%i" % (i+1), "MG%i" % (i+1))
+ bank._attr_name = self._attr_name
+ bank._radio = self._radio
+ bank.index = i
+ #print "Bank #%i has name %s" % (i,bank.get_name());
+ #if len(bank.get_name())>0:
+ banks.append(bank);
+ return banks
+
+ def add_memory_to_mapping(self, memory, bank):
+ _members = getattr(self._radio._memobj,
+ self._attr_name)[bank.index].members
+ #_bank_used = self._radio._memobj.bank_used[bank.index]
+ for i in range(0, self._num_members):
+ if _members[i] == 0x0000:
+ _members[i] = memory.number
+ #_bank_used.in_use = 0x0000
+ break
+
+ def remove_memory_from_mapping(self, memory, bank):
+ _members = getattr(self._radio._memobj,
+ self._attr_name)[bank.index].members
+
+ found = False
+ remaining_members = 0
+ for i in range(0, len(_members)):
+ if _members[i] == (memory.number):
+ _members[i] = 0x0000
+ found = True
+ elif _members[i] != 0x0000:
+ remaining_members += 1
+
+ if not found:
+ raise Exception("Memory {num} not in " +
+ "bank {bank}".format(num=memory.number,
+ bank=bank))
+ #if not remaining_members:
+ # _bank_used.in_use = 0x0000
+
+ def get_mapping_memories(self, bank):
+ memories = []
+
+ _members = getattr(self._radio._memobj,
+ self._attr_name)[bank.index].members
+ #_bank_used = self._radio._memobj.bank_used[bank.index]
+
+ #if _bank_used.in_use == 0x0000:
+ # return memories
+
+ for number in _members:
+ #Zero items are not memories.
+ if number == 0x0000:
+ continue
+
+ mem=self._radio.get_memory(number);
+ memories.append(mem)
+ return memories
+
+ def get_memory_mappings(self, memory):
+ banks = []
+ for bank in self.get_mappings():
+ if memory.number in [x.number for x in
+ self.get_mapping_memories(bank)]:
+ banks.append(bank)
+ return banks
+
+
+class MD380ZoneModel(MD380BankModel):
+ _attr_name = "bank"
+ _num_mappings = 99
+ _num_members = 16
+
+
+class MD380ScanlistModel(MD380BankModel):
+ _attr_name = "scanlists"
+ _num_mappings = 20
+ _num_members = 31
+
+
+class MD380GrouplistModel(MD380BankModel):
+ _attr_name = "grouplists"
+ _num_mappings = 250
+ _num_members = 32
+
+ def get_mapping_memories(self, bank):
+ contacts = []
+
+ _members = getattr(self._radio._memobj,
+ self._attr_name)[bank.index].members
+ #_bank_used = self._radio._memobj.bank_used[bank.index]
+
+ #if _bank_used.in_use == 0x0000:
+ # return memories
+
+ for number in _members:
+ #Zero items are not memories.
+ if number == 0x0000:
+ continue
+
+ contact = self._radio._memobj.contacts[number]
+ # This doesn't work. It still references memories, not contacts.
+ mem = chirp_common.Memory()
+ mem.number = number
+ mem.freq = contact.callid
+ mem.name = utftoasc(contact.name)
+ contacts.append(mem)
+ return contacts
+
+
+# Uncomment this to actually register this radio in CHIRP
+ at directory.register
+class MD380Radio(chirp_common.CloneModeRadio):
+ """MD380 Binary File"""
+ VENDOR = "TYT"
+ MODEL = "MD-380"
+ FILE_EXTENSION = "img"
+
+ _memsize = 262144
+ _range = (400000000, 480000000)
+
+ @classmethod
+ def match_model(cls, filedata, filename):
+ return len(filedata) == cls._memsize
+
+ # Return information about this radio's features, including
+ # how many memories it has, what bands it supports, etc
+ def get_features(self):
+ rf = chirp_common.RadioFeatures()
+ rf.has_bank = True
+ rf.has_bank_index = True
+ rf.has_bank_names = True
+ rf.can_odd_split = True
+ rf.memory_bounds = (1, 999) # Maybe 1000?
+
+ rf.valid_bands = [self._range]
+ rf.valid_characters = "".join(CHARSET)
+ rf.has_settings = True
+ rf.has_tuning_step = False
+ rf.has_ctone = True
+ rf.has_dtcs = True
+ rf.has_cross = True
+ rf.valid_modes = list(MODES)
+ rf.valid_skips = [""]
+ rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+ rf.valid_duplexes = list(DUPLEX)
+ rf.valid_name_length = 16
+ return rf
+
+ # Processes the mmap from a file.
+ def process_mmap(self):
+ self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+ self.contacts = [c for c in self.list_contacts()]
+ self.scanlists = ["None"]
+ for scanlist in self._memobj.scanlists:
+ self.scanlists.append(utftoasc(scanlist.name))
+ if not self.scanlists[-1]:
+ break
+ print self.scanlists
+ self.grouplists = [None]
+ for grouplist in self._memobj.grouplists:
+ self.grouplists.append(utftoasc(grouplist.name))
+ if not self.grouplists[-1]:
+ break
+ print self.grouplists
+
+ # Do a download of the radio from the serial port
+ def sync_in(self):
+ raise errors.RadioError("Not implemented.")
+
+ # Do an upload of the radio to the serial port
+ def sync_out(self):
+ raise errors.RadioError("Not implemented.")
+
+ def get_contact(self, number):
+ _contact = self._memobj.contacts[number-1]
+ return int(_contact.callid), utftoasc(_contact.name)
+
+ def list_contacts(self):
+ yield "None"
+ for i in xrange(1000):
+ yield utftoasc(self._memobj.contacts[i].name)
+
+ # Return a raw representation of the memory object, which
+ # is very helpful for development
+ def get_raw_memory(self, number):
+ return repr(self._memobj.memory[number-1])
+
+ # Extract a high-level memory object from the low-level memory map
+ # This is called to populate a memory in the UI
+ def get_memory(self, number):
+ _mem = self._memobj.memory[number-1]
+ mem = chirp_common.Memory()
+ mem.number = number;
+
+ if _mem.rxfreq.get_raw() == "\xFF" * 4:
+ mem.empty = True
+ return mem
+
+ mem.name = utftoasc(_mem.name)
+ mem.freq = int(_mem.rxfreq)*10;
+
+ chirp_common.split_tone_decode(
+ mem, decode_tone(_mem.txtone), decode_tone(_mem.rxtone))
+
+ # In split mode, offset is the TX freq.
+ mem.offset = int(_mem.txfreq) * 10
+ if _mem.rxonly:
+ mem.duplex = "off"
+ elif mem.offset == mem.freq:
+ mem.duplex = ""
+ mem.offset = 0
+ elif mem.offset > mem.freq:
+ mem.duplex = "+"
+ mem.offset = abs(mem.freq - mem.offset)
+ elif mem.offset < mem.freq:
+ mem.duplex = "-"
+ mem.offset = abs(mem.freq - mem.offset)
+
+ mem.mode = _mem.mode == 2 and "DIG" or _mem.fmbw and "FM" or "NFM"
+
+ mem.extra = RadioSettingGroup("Extra", "extra")
+
+ mem.extra.append(RadioSetting(
+ "squelch", "Tight Squelch",
+ RadioSettingValueBoolean(not bool(_mem.squelch))))
+ rs = RadioSetting("contact", "Contact", RadioSettingValueList(
+ self.contacts, self.contacts[_mem.contact]))
+ mem.extra.append(rs)
+ rs = RadioSetting("grouplist", "Group List", RadioSettingValueList(
+ self.grouplists, self.grouplists[_mem.grouplist]))
+ mem.extra.append(rs)
+ rs = RadioSetting("color_code", "Color Code", RadioSettingValueList(
+ range(16), int(_mem.color_code)))
+ mem.extra.append(rs)
+ rs = RadioSetting("slot", "Time Slot", RadioSettingValueMap(
+ ((1, 1), (2, 2)), _mem.slot))
+ mem.extra.append(rs)
+ rs = RadioSetting("scanlist", "Scan List", RadioSettingValueList(
+ self.scanlists, self.scanlists[_mem.scanlist]))
+ mem.extra.append(rs)
+
+ # print "\t".join([str(number), mem.name, str(_mem.squelch)])
+
+ return mem
+
+ # Store details about a high-level memory to the memory map
+ # This is called when a user edits a memory in the UI
+ def set_memory(self, mem):
+ # Get a low-level memory object mapped to the image
+ _mem = self._memobj.memory[mem.number-1]
+
+ if mem.empty:
+ _mem.set_raw("\xFF" * 32 + "\x00" * 32)
+ return
+
+ if _mem.rxfreq.get_raw() == "\xFF" * 4:
+ # new channel, set defaults
+ _mem.set_raw("\x62\x14\x00\xe0\x24\xc3\x00\x00" +
+ "\x04\x00\x00\x00\x00\x00\x00\xff" +
+ "\x00\x00\x00\x40\x00\x00\x00\x40" +
+ "\xff\xff\xff\xff\x00\x00\xff\xff" +
+ "\x00" * 32)
+
+ # Convert to low-level frequency representation
+ _mem.rxfreq = mem.freq / 10;
+
+ if mem.duplex == "split":
+ _mem.txfreq = mem.offset / 10
+ elif mem.duplex=="+":
+ _mem.txfreq = mem.freq / 10 + mem.offset / 10
+ elif mem.duplex=="-":
+ _mem.txfreq = mem.freq / 10 - mem.offset / 10
+ else:
+ _mem.txfreq = _mem.rxfreq
+ _mem.rxonly = int(mem.duplex == "off")
+ _mem.name = asctoutf(mem.name,32);
+
+ tx, rx = chirp_common.split_tone_encode(mem)
+ encode_tone(_mem, 'txtone', *tx)
+ encode_tone(_mem, 'rxtone', *rx)
+
+ _mem.mode = mem.mode == "DIG" and 2 or 1
+ if _mem.mode == 1:
+ _mem.fmbw = mem.mode == "NFM" and 0 or 2
+
+ print "md380 mem:", mem.__dict__
+ for setting in mem.extra:
+ print setting.get_name(), setting.value
+ if setting.get_name() == "xcontact":
+ print repr(setting.value), str(setting.value)
+ if setting.value:
+ _mem.contact = self.contacts.index(setting.value)
+ else:
+ setattr(_mem, setting.get_name(), setting.value)
+
+ def get_settings(self):
+ _general = self._memobj.general
+ _info = self._memobj.info
+
+ basic = RadioSettingGroup("basic", "Basic")
+ info = RadioSettingGroup("info", "Model Info")
+ general = RadioSettingGroup("general", "General Settings");
+
+
+ #top = RadioSettings(identity, basic)
+ top = RadioSettings(general)
+ general.append(RadioSetting(
+ "dmrid", "DMR Radio ID",
+ RadioSettingValueInteger(0, 100000000, _general.dmrid)));
+ general.append(RadioSetting(
+ "line1", "Startup Line 1",
+ RadioSettingValueString(0, 10, utftoasc(str(_general.line1)))));
+ general.append(RadioSetting(
+ "line2", "Startup Line 2",
+ RadioSettingValueString(0, 10, utftoasc(str(_general.line2)))));
+ return top
+
+ def set_settings(self, settings):
+ _general = self._memobj.general
+ _info = self._memobj.info
+ for element in settings:
+ if not isinstance(element, RadioSetting):
+ self.set_settings(element)
+ continue
+ if not element.changed():
+ continue
+ try:
+ setting = element.get_name()
+ #oldval = getattr(_settings, setting)
+ newval = element.value
+
+ #LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval))
+ if setting=="line1":
+ _general.line1=asctoutf(str(newval),20);
+ elif setting=="line2":
+ _general.line2=asctoutf(str(newval),20);
+ else:
+ print("Setting %s <= %s" % (setting, newval))
+ setattr(_general, setting, newval)
+ except Exception, e:
+ LOG.debug(element.get_name())
+ raise
+
+ def get_mapping_models(self):
+ return [MD380ZoneModel(self, "Zones"),
+ MD380ScanlistModel(self, "Scan Lists"),
+ MD380GrouplistModel(self, "Group Lists")]
+
+
+class MD380RDTFileRadio(chirp_common.FileWrapperMixin, MD380Radio):
+ """MD380 RDT File"""
+ FILE_EXTENSION = "rdt"
+ _headsize = 549
+ _mmapsize = MD380Radio._memsize
+ _tailsize = 16
+ _memsize = _headsize + _mmapsize + _tailsize
+
+ @classmethod
+ def match_model(cls, filedata, filename):
+ return len(filedata) == cls._memsize and \
+ filedata[0x139:0x13d] == cls._rangelbcd
+
+
+ at directory.register
+class MD380VHFRDTFileRadio(MD380RDTFileRadio):
+ MODEL = "MD-380 VHF .rdt file"
+ _range = (136000000, 174000000)
+ _rangelbcd = "\x60\x13\x40\x17"
+
+
+ at directory.register
+class MD380UHFRDTFileRadio(MD380RDTFileRadio):
+ MODEL = "MD-380 UHF .rdt file"
+ _range = (400000000, 480000000)
+ _rangelbcd = "\x00\x40\x00\x48"
+
+
+# FIXME: channel decode errors
+ at directory.register
+class CS700UHFRDTFileRadio(MD380RDTFileRadio):
+ VENDOR = "Connect Systems"
+ MODEL = "CS-700 UHF .rdt file"
+ _range = (400000000, 470000000)
+ _rangelbcd = "\x00\x40\x00\x47"
diff -r be9788307368 -r f53339aa5068 chirp/ui/mainapp.py
--- a/chirp/ui/mainapp.py Fri Sep 16 16:22:18 2016 -0700
+++ b/chirp/ui/mainapp.py Fri Sep 16 16:22:19 2016 -0700
@@ -321,6 +321,7 @@
(_("DAT Files") + " (*.dat)", "*.dat"),
(_("EVE Files (VX5)") + " (*.eve)", "*.eve"),
(_("ICF Files") + " (*.icf)", "*.icf"),
+ (_("DMR Radio Files") + " (*.rdt)", "*.rdt"),
(_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
(_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
(_("VX7 Commander Files") + " (*.vx7)", "*.vx7"),
@@ -794,6 +795,7 @@
(_("ICF Files") + " (*.icf)", "*.icf"),
(_("Kenwood HMK Files") + " (*.hmk)", "*.hmk"),
(_("Kenwood ITM Files") + " (*.itm)", "*.itm"),
+ (_("DMR Radio Files") + " (*.rdt)", "*.rdt"),
(_("Travel Plus Files") + " (*.tpe)", "*.tpe"),
(_("VX5 Commander Files") + " (*.vx5)", "*.vx5"),
(_("VX6 Commander Files") + " (*.vx6)", "*.vx6"),
More information about the chirp_devel
mailing list