[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