# HG changeset patch # User Tom Hayward tom@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@radiantmachines.com +# Copyright 2016 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 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 +@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 + + +@directory.register +class MD380VHFRDTFileRadio(MD380RDTFileRadio): + MODEL = "MD-380 VHF .rdt file" + _range = (136000000, 174000000) + _rangelbcd = "\x60\x13\x40\x17" + + +@directory.register +class MD380UHFRDTFileRadio(MD380RDTFileRadio): + MODEL = "MD-380 UHF .rdt file" + _range = (400000000, 480000000) + _rangelbcd = "\x00\x40\x00\x48" + + +# FIXME: channel decode errors +@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"),