[chirp_devel] [PATCH 0 of 4] RFC MD-380 WIP
I have been working on completing Travis Goodspeed's MD-380 driver. It's still unfinished, but I haven't touched this code in a week or two and wanted to share it rather than let it rot on my system.
There are a few things about the MD-380 that add complexity to the Chirp driver:
- It is a DMR radio, which means in addtion to channels and banks, a contact/talkgroup editor is required. - It uses USB DFU for upload/download, which has never been done before in Chirp.
Here is the current state:
- Memory map is complete. - get_memory() works - I think I finished set_memory(), but don't take my word for it. - Bank/zone editing works based on Travis' earlier implementation. - There's no contact editor. - Can read .rdt files from MD-380 OEM software - Can read .img files from md380tools - No attempt has been made to support upload/download, yet, although Travis' implementation in md380tools works. Porting this is on my to-do list. - Copy/paste of DMR channels doesn't work, because the "extra" settings aren't copied. With DMR radios there is essential stuff here. This looks rather involved to fix, because Chirp doesn't pass the full Memory object during copy/paste. - It's difficult to read the channel list because color code, timeslot, and talkgroup/contact are all hidden. I'm not sure the best way to add this to Chirp.
There's some other interesting stuff in this patchbomb:
- Import repeater list from DMR-MARC. Unfortunately this doesn't include talkgroup data, so it's not really useful for programming the radio. - Support for reading and writing files that contain img data between a header and footer. I've noticed this is pretty common with other radio programming software, so I created a generic way to support it. - A fix for a bug that has broken a Chirp feature for about 3 years. I guess I'm the only one who uses multiple memory maps!
Please comment.
Tom KD7LXL
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1474068111 25200 # Fri Sep 16 16:21:51 2016 -0700 # Node ID c55eeb0b9a3c898ab1a853272404ab247cccfa0f # Parent a1b8b53606f6025fc1ad727331837cfc7759f178 Add query/import from DMR-MARC repeater database.
diff -r a1b8b53606f6 -r c55eeb0b9a3c chirp/chirp_common.py --- a/chirp/chirp_common.py Sat Sep 10 11:34:01 2016 -0400 +++ b/chirp/chirp_common.py Fri Sep 16 16:21:51 2016 -0700 @@ -70,7 +70,7 @@
MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY", "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto", "RTTYR", - "FSK", "FSKR"] + "FSK", "FSKR", "DMR"]
TONE_MODES = [ "", diff -r a1b8b53606f6 -r c55eeb0b9a3c chirp/dmrmarc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/dmrmarc.py Fri Sep 16 16:21:51 2016 -0700 @@ -0,0 +1,136 @@ +# 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 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 json +import logging +import tempfile +import urllib +from chirp import chirp_common, errors +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueList + +LOG = logging.getLogger(__name__) + + +def list_filter(haystack, attr, needles): + if not needles or not needles[0]: + return haystack + return [x for x in haystack if x[attr] in needles] + + +class DMRMARCRadio(chirp_common.NetworkSourceRadio): + """DMR-MARC data source""" + VENDOR = "DMR-MARC" + MODEL = "Repeater database" + + URL = "http://www.dmr-marc.net/cgi-bin/trbo-database/datadump.cgi?" \ + "table=repeaters&format=json" + + def __init__(self, *args, **kwargs): + chirp_common.NetworkSourceRadio.__init__(self, *args, **kwargs) + self._repeaters = None + + def set_params(self, city, state, country): + """Set the parameters to be used for a query""" + self._city = city and [x.strip() for x in city.split(",")] or [''] + self._state = state and [x.strip() for x in state.split(",")] or [''] + self._country = country and [x.strip() for x in country.split(",")] \ + or [''] + + def do_fetch(self): + fn = tempfile.mktemp(".json") + filename, headers = urllib.urlretrieve(self.URL, fn) + with open(fn, 'r') as f: + try: + self._repeaters = json.load(f)['repeaters'] + except AttributeError: + raise errors.RadioError( + "Unexpected response from %s" % self.URL) + + self._repeaters = list_filter(self._repeaters, "city", self._city) + self._repeaters = list_filter(self._repeaters, "state", self._state) + self._repeaters = list_filter(self._repeaters, "country", + self._country) + + def get_features(self): + if not self._repeaters: + self.do_fetch() + + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (0, len(self._repeaters)-1) + rf.has_bank = False + rf.has_comment = True + rf.has_ctone = False + rf.valid_tmodes = [""] + return rf + + def get_raw_memory(self, number): + return repr(self._repeaters[number]) + + def get_memory(self, number): + if not self._repeaters: + self.do_fetch() + + repeater = self._repeaters[number] + + mem = chirp_common.Memory() + mem.number = number + + mem.name = repeater.get('city') + mem.freq = chirp_common.parse_freq(repeater.get('frequency')) + offset = chirp_common.parse_freq(repeater.get('offset', '0')) + if offset > 0: + mem.duplex = "+" + elif offset < 0: + mem.duplex = "-" + else: + mem.duplex = "" + mem.offset = abs(offset) + mem.mode = 'DMR' + mem.comment = repeater.get('map_info') + + mem.extra = RadioSettingGroup("Extra", "extra") + + rs = RadioSetting( + "color_code", "Color Code", RadioSettingValueList( + range(16), int(repeater.get('color_code', 0)))) + mem.extra.append(rs) + + return mem + + +def main(): + import argparse + from pprint import PrettyPrinter + + parser = argparse.ArgumentParser(description="Fetch DMR-MARC repeater " + "database and filter by city, state, and/or country. Multiple items " + "combined with a , will be filtered with logical OR.") + parser.add_argument("-c", "--city", + help="Comma-separated list of cities to include in output.") + parser.add_argument("-s", "--state", + help="Comma-separated list of states to include in output.") + parser.add_argument("--country", + help="Comma-separated list of countries to include in output.") + args = parser.parse_args() + + dmrmarc = DMRMARCRadio(None) + dmrmarc.set_params(**vars(args)) + dmrmarc.do_fetch() + pp = PrettyPrinter(indent=2) + pp.pprint(dmrmarc._repeaters) + +if __name__ == "__main__": + main() diff -r a1b8b53606f6 -r c55eeb0b9a3c chirp/ui/mainapp.py --- a/chirp/ui/mainapp.py Sat Sep 10 11:34:01 2016 -0400 +++ b/chirp/ui/mainapp.py Fri Sep 16 16:21:51 2016 -0700 @@ -806,6 +806,65 @@ count = eset.do_import(filen) reporting.report_model_usage(eset.rthread.radio, "import", count > 0)
+ def do_dmrmarc_prompt(self): + fields = {"1City": (gtk.Entry(), lambda x: x), + "2State": (gtk.Entry(), lambda x: x), + "3Country": (gtk.Entry(), lambda x: x), + } + + d = inputdialog.FieldDialog(title=_("DMR-MARC Repeater Database Dump"), + parent=self) + for k in sorted(fields.keys()): + d.add_field(k[1:], fields[k][0]) + fields[k][0].set_text(CONF.get(k[1:], "dmrmarc") or "") + + while d.run() == gtk.RESPONSE_OK: + for k in sorted(fields.keys()): + widget, validator = fields[k] + try: + if validator(widget.get_text()): + CONF.set(k[1:], widget.get_text(), "dmrmarc") + continue + except Exception: + pass + + d.destroy() + return True + + d.destroy() + return False + + def do_dmrmarc(self, do_import): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + if not self.do_dmrmarc_prompt(): + self.window.set_cursor(None) + return + + city = CONF.get("city", "dmrmarc") + state = CONF.get("state", "dmrmarc") + country = CONF.get("country", "dmrmarc") + + # Do this in case the import process is going to take a while + # to make sure we process events leading up to this + gtk.gdk.window_process_all_updates() + while gtk.events_pending(): + gtk.main_iteration(False) + + if do_import: + eset = self.get_current_editorset() + dmrmarcstr = "dmrmarc://%s/%s/%s" % (city, state, country) + eset.do_import(dmrmarcstr) + else: + try: + from chirp import dmrmarc + radio = dmrmarc.DMRMARCRadio(None) + radio.set_params(city, state, country) + self.do_open_live(radio, read_only=True) + except errors.RadioError, e: + common.show_error(e) + + self.window.set_cursor(None) + def do_repeaterbook_prompt(self): if not CONF.get_bool("has_seen_credit", "repeaterbook"): d = gtk.MessageDialog(parent=self, buttons=gtk.BUTTONS_OK) @@ -1434,6 +1493,8 @@ self.do_close() elif action == "import": self.do_import() + elif action in ["qdmrmarc", "idmrmarc"]: + self.do_dmrmarc(action[0] == "i") elif action in ["qrfinder", "irfinder"]: self.do_rfinder(action[0] == "i") elif action in ["qradioreference", "iradioreference"]: @@ -1529,12 +1590,14 @@ <menuitem action="download"/> <menuitem action="upload"/> <menu action="importsrc" name="importsrc"> + <menuitem action="idmrmarc"/> <menuitem action="iradioreference"/> <menuitem action="irbook"/> <menuitem action="ipr"/> <menuitem action="irfinder"/> </menu> <menu action="querysrc" name="querysrc"> + <menuitem action="qdmrmarc"/> <menuitem action="qradioreference"/> <menuitem action="qrbook"/> <menuitem action="qpr"/> @@ -1607,12 +1670,14 @@ ('export', None, _("Export"), "%se" % ALT_KEY, None, self.mh), ('importsrc', None, _("Import from data source"), None, None, self.mh), + ('idmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh), ('iradioreference', None, _("RadioReference.com"), None, None, self.mh), ('irfinder', None, _("RFinder"), None, None, self.mh), ('irbook', None, _("RepeaterBook"), None, None, self.mh), ('ipr', None, _("przemienniki.net"), None, None, self.mh), ('querysrc', None, _("Query data source"), None, None, self.mh), + ('qdmrmarc', None, _("DMR-MARC Repeaters"), None, None, self.mh), ('qradioreference', None, _("RadioReference.com"), None, None, self.mh), ('qrfinder', None, _("RFinder"), None, None, self.mh),
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1474068137 25200 # Fri Sep 16 16:22:17 2016 -0700 # Node ID 45cfb92ddc164dbe01354f43cb68cd970c3eb6f3 # Parent c55eeb0b9a3c898ab1a853272404ab247cccfa0f Add FileWrapperMixin for pulling img data out of 3rd party formats. #3755
diff -r c55eeb0b9a3c -r 45cfb92ddc16 chirp/chirp_common.py --- a/chirp/chirp_common.py Fri Sep 16 16:21:51 2016 -0700 +++ b/chirp/chirp_common.py Fri Sep 16 16:22:17 2016 -0700 @@ -1139,6 +1139,33 @@ return self._mmap
+class FileWrapperMixin: + def load_mmap(self, filename): + """Load the radio's memory map from @filename""" + mapfile = file(filename, "rb") + self._head = mapfile.read(self._headsize) + self._mmap = memmap.MemoryMap(mapfile.read(self._mmapsize)) + self._tail = mapfile.read() + if len(self._tail) != self._tailsize: + raise errors.RadioError("File wrapper is wrong length.") + mapfile.close() + self.process_mmap() + + def save_mmap(self, filename): + """ + try to open a file and write to it + If IOError raise a File Access Error Exception + """ + try: + mapfile = file(filename, "wb") + mapfile.write(self._head) + mapfile.write(self._mmap.get_packed()) + mapfile.write(self._tail) + mapfile.close() + except IOError: + raise Exception("File Access Error") + + class CloneModeRadio(FileBackedRadio): """A clone-mode radio does a full memory dump in and out and we store an image of the radio into an image file"""
# HG changeset patch # User Tom Hayward tom@tomh.us # Date 1474068138 25200 # Fri Sep 16 16:22:18 2016 -0700 # Node ID be978830736869866b91da00826548ad563fa9eb # Parent 45cfb92ddc164dbe01354f43cb68cd970c3eb6f3 Fix MemoryMapping bug that caused only the last-defined bank editor to load. #741
diff -r 45cfb92ddc16 -r be9788307368 chirp/ui/editorset.py --- a/chirp/ui/editorset.py Fri Sep 16 16:22:17 2016 -0700 +++ b/chirp/ui/editorset.py Fri Sep 16 16:22:18 2016 -0700 @@ -70,6 +70,7 @@ members.connect("changed", lambda x: names.mappings_changed()) names.connect("changed", lambda x: members.mappings_changed()) names.connect("changed", self.editor_changed) + sub_index += 1
def _make_device_editors(self, device, devrthread, index): if isinstance(device, chirp_common.IcomDstarSupport):
# 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"),
participants (1)
-
Tom Hayward