# HG changeset patch # User James Lee N1DDK # Date 1420145950 18000 # Thu Jan 01 15:59:10 2015 -0500 # Node ID 32c2254e37d31f94393eb0ee244bd6523e9c23d6 # Parent 0ccddc8155647a3fb78d2eda13b098e7c2b59f94 [th9800] Add basic support for TYT TH9800 part of #1353 diff -r 0ccddc815564 -r 32c2254e37d3 chirp/th9800.py --- a/chirp/th9800.py Thu Jan 01 19:02:09 2015 +0100 +++ b/chirp/th9800.py Thu Jan 01 15:59:10 2015 -0500 @@ -1,4 +1,6 @@ # Copyright 2014 Tom Hayward +# Copyright 2014 Jens Jensen +# Copyright 2014 James Lee N1DDK # # 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 @@ -13,7 +15,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from chirp import bitwise, chirp_common, directory +from chirp import bitwise, chirp_common, directory, errors, util, memmap +import struct +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettingValueFloat, InvalidValueError +from chirp_common import format_freq +import os +import time +from datetime import date + +if os.getenv("CHIRP_DEBUG"): + CHIRP_DEBUG = True +else: + CHIRP_DEBUG = False TH9800_MEM_FORMAT = """ struct mem { @@ -22,17 +38,27 @@ lbcd ctcss[2]; lbcd dtcs[2]; u8 power:2, - unknown0a:4, + BeatShift:1, + unknown0a:2, + FreqName:1, scan:2; - u8 fmdev:2, - unknown1a:2, - unknown1b:4; - u8 unknown2; + u8 fmdev:2, // wide=00, mid=01, narrow=10 + scramb:1, + compand:1, + emphasis:1 + unknown1a:1, + unknown1b:2; + u8 rptmod:2, // off, -, + + unknown2a:1, + talkaround:1, + step:4; u8 dtcs_pol:2, - unknown3:4, + bclo:2, + unknown3:2, tmode:2; - u8 unknown4[4]; - u8 unknown5a:3, + lbcd offset[4]; + u8 hsdtype:2, // off, 2-tone, 5-tone, dtmf + unknown5a:1, am:1, unknown5b:4; u8 unknown6[3]; @@ -40,8 +66,88 @@ u8 empty[2]; }; -#seekto 0x1100; +#seekto 0x%04X; struct mem memory[800]; + +#seekto 0x%04X; +struct { + struct mem lower; + struct mem upper; +} scanlimits[5]; + +#seekto 0x%04X; +struct { + u8 unk0xdc20:5, + left_sql:3; + u8 apo; + u8 unk0xdc22:5, + backlight:3; + u8 unk0xdc23; + u8 beep:1, + keylock:1, + pttlock:2, + unk0xdc24_32:2, + hyper_chan:1, + right_func_key:1; + u8 tbst_freq:2, + ani_display:1, + unk0xdc25_4:1 + mute_mode:2, + unk0xdc25_10:2; + u8 auto_xfer:1, + auto_contact:1, + unk0xdc26_54:2, + auto_am:1, + unk0xdc26_210:3; + u8 unk0xdc27_76543:5, + scan_mode:1, + unk0xdc27_1:1, + scan_resume:1; + u16 scramb_freq; + u16 scramb_freq1; + u8 exit_delay; + u8 unk0xdc2d; + u8 unk0xdc2e:5, + right_sql:3; + u8 unk0xdc2f:4, + beep_vol:4; + u8 tot; + u8 tot_alert; + u8 tot_rekey; + u8 tot_reset; + u8 unk0xdc34; + u8 unk0xdc35; + u8 unk0xdc36; + u8 unk0xdc37; + u8 p1; + u8 p2; + u8 p3; + u8 p4; +} settings; + +#seekto 0x%04X; +u8 chan_active_a[128]; +u8 chan_active_b[128]; + +#seekto 0x%04X; +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; + +struct { + lbcd lorx[4]; + lbcd hirx[4]; + lbcd lotx[4]; + lbcd hitx[4]; +} bandlimits[9]; + """ @@ -49,129 +155,556 @@ "\x00\x00\x76\x00\x00\x00" + "\xFF" * 10 DTCS_POLARITY = ["NN", "RN", "NR", "RR"] SCAN_MODES = ["", "S", "P"] -MODES = ["FM", "FM", "NFM"] +MODES = ["FM", "FM", "NFM"] ## fixme? TMODES = ["", "Tone", "TSQL", "DTCS"] +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5.00), + chirp_common.PowerLevel("Mid2", watts=10.00), + chirp_common.PowerLevel("Mid1", watts=20.00), + chirp_common.PowerLevel("High", watts=50.00)] +BUSY_LOCK = ["off", "Carrier", "2 tone"] +MICKEYFUNC = ["None","SCAN","SQL.OFF","TCALL","PPTR","PRI","LOW","TONE", + "MHz","REV","HOME","BAND","VFO/MR"] +SQLPRESET = ["Off", "2", "5", "9", "Full"] +BANDS = ["30MHz", "50MHz", "60MHz", "108MHz", "150MHz", "250MHz", "350MHz", + "450MHz", "850MHz"] +STEPS = [ 2.5, 5.0, 6.25, 7.5, 8.33, 10.0, 12.5, + 15.0, 20.0, 25.0, 30.0, 50.0, 100.0 ] + +class TYTTH9800Base(chirp_common.Radio): + """Base class for TYT TH-9800""" + VENDOR = "TYT" + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 800) + rf.has_bank = False + rf.has_tuning_step = False + rf.can_odd_split = True + rf.valid_duplexes = ["", "-", "+", "split"] + rf.valid_tmodes = TMODES + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "#*-+" + rf.valid_bands = [( 26000000, 33000000), + ( 47000000, 54000000), + (108000000, 180000000), + (350000000, 399000000), + (430000000, 512000000), + (750000000, 947000000)] + rf.valid_skips = SCAN_MODES + rf.valid_modes = MODES + ["AM"] + rf.valid_name_length = 6 + rf.has_settings = True + return rf + def process_mmap(self): + self._memobj = bitwise.parse( TH9800_MEM_FORMAT % + (self._mmap_offset, self._scanlimits_offset, self._settings_offset, + self._chan_active_offset, self._info_offset), self._mmap) + + def get_chan_active(self, num): + index = (num - 1) / 8 + bitpos = (num - 1) % 8 + mask = 2**bitpos + enabled = self._memobj.chan_active_a[index] & mask + if CHIRP_DEBUG: + print "gca:", num, index, bitpos, self._memobj.chan_active_a[index], + enabled + if enabled: + return True + else: + return False + + def set_chan_active(self, num, enable): + index = (num - 1) / 8 + bitpos = (num - 1) % 8 + mask = 2**bitpos + if CHIRP_DEBUG: + print "setting to:", enable, mask + print "before:", self._memobj.chan_active_a[index] + if enable: + self._memobj.chan_active_a[index] |= mask + self._memobj.chan_active_b[index] |= mask + else: + self._memobj.chan_active_a[index] &= ~mask + self._memobj.chan_active_b[index] &= ~mask + if CHIRP_DEBUG: + print "sca:", num, index, bitpos + print "after:", self._memobj.chan_active_a[index] + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + mem = chirp_common.Memory() + mem.number = number + + #if not _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"): + # mem.empty = True + # return mem + + mem.empty = not self.get_chan_active(number) + if mem.empty: + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + txfreq = int(_mem.tx_freq) * 10 + if txfreq == mem.freq: + mem.duplex = "" + elif abs(txfreq - mem.freq) > 70000000: + mem.duplex = "split" + mem.offset = txfreq + elif txfreq < mem.freq: + mem.duplex = "-" + mem.offset = mem.freq - txfreq + elif txfreq > mem.freq: + mem.duplex = "+" + mem.offset = txfreq - mem.freq + + mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol] + + mem.tmode = TMODES[int(_mem.tmode)] + mem.ctone = mem.rtone = int(_mem.ctcss) / 10.0 + mem.dtcs = int(_mem.dtcs) + + mem.name = str(_mem.name) + mem.name = mem.name.replace("\xFF", " ").rstrip() + + mem.skip = SCAN_MODES[int(_mem.scan)] + mem.mode = _mem.am and "AM" or MODES[int(_mem.fmdev)] + + mem.power = POWER_LEVELS[_mem.power] + + mem.extra = RadioSettingGroup("extra", "Extra") + FreqName = RadioSetting("FreqName", "Frequency or Name", + RadioSettingValueBoolean(bool(_mem.FreqName))) + FreqName.set_doc("Display Frequency or Name on=Name") + mem.extra.append(FreqName) + + bclo = RadioSetting("bclo", "Busy Lockout", + RadioSettingValueList(BUSY_LOCK, + BUSY_LOCK[_mem.bclo])) + bclo.set_doc("Busy Lockout") + mem.extra.append(bclo) + + emphasis = RadioSetting("emphasis", "Emphasis", + RadioSettingValueBoolean(bool(_mem.emphasis))) + emphasis.set_doc("Boosts 300Hz to 2500Hz mic response") + mem.extra.append(emphasis) + + compand = RadioSetting("compand", "Compand", + RadioSettingValueBoolean(bool(_mem.compand))) + compand.set_doc("Compress Audio") + mem.extra.append(compand) + + opts = ["Wide", "Midddle", "Narrow"] + dev = RadioSetting("fmdev", "FM Deviation", + RadioSettingValueList(opts, opts[_mem.fmdev])) + dev.set_doc("Wide ~6KHz, Middle ~4KHz, Narrow ~2.5KHz") + mem.extra.append(dev) + + BeatShift = RadioSetting("BeatShift", "BeatShift", + RadioSettingValueBoolean(bool(_mem.BeatShift))) + BeatShift.set_doc("Beat Shift") + mem.extra.append(BeatShift) + + TalkAround = RadioSetting("talkaround", "Talk Around", + RadioSettingValueBoolean(bool(_mem.talkaround))) + TalkAround.set_doc("Simplex mode when out of range of repeater") + mem.extra.append(TalkAround) + + scramb = RadioSetting("scramb", "Scramble", + RadioSettingValueBoolean(bool(_mem.scramb))) + scramb.set_doc("Frequency inversion Scramble") + mem.extra.append(scramb) + + return mem + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + self.set_chan_active(mem.number, not mem.empty) + if mem.empty: + _mem.set_raw(BLANK_MEMORY) + return + + #if _mem.get_raw() == BLANK_MEMORY: + # print "Initializing empty memory" + # _mem.set_raw(BLANK_MEMORY) + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "split": + _mem.tx_freq = mem.offset / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + _mem.tmode = TMODES.index(mem.tmode) + _mem.ctcss = mem.rtone * 10 + _mem.dtcs = mem.dtcs + _mem.dtcs_pol = DTCS_POLARITY.index(mem.dtcs_polarity) + + _mem.name = mem.name.ljust(6, "\xFF") + + _mem.scan = SCAN_MODES.index(mem.skip) + + if mem.mode == "AM": + _mem.am = True + _mem.fmdev = 0 + else: + _mem.am = False + # _mem.fmdev = MODES.index(mem.mode) + + _mem.power = POWER_LEVELS.index(mem.power) + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + _info = self._memobj.info + _bandlimits = self._memobj.bandlimits + basic = RadioSettingGroup("basic", "Basic") + info = RadioSettingGroup("info", "Model Info") + top = RadioSettingGroup("top", "All Settings", + basic, info) + basic.append( RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep))) + basic.append( RadioSetting("beep_vol", "Beep Volume", + RadioSettingValueInteger(0, 15,_settings.beep_vol))) + basic.append( RadioSetting("keylock", "Key Lock", + RadioSettingValueBoolean(_settings.keylock))) + basic.append( RadioSetting("ani_display", "ANI Display", + RadioSettingValueBoolean(_settings.ani_display))) + basic.append( RadioSetting("auto_xfer", "Auto Transfer", + RadioSettingValueBoolean(_settings.auto_xfer))) + basic.append( RadioSetting("auto_contact", + "Auto Contact Always Remind", + RadioSettingValueBoolean(_settings.auto_contact))) + basic.append( RadioSetting("auto_am", "Auto AM", + RadioSettingValueBoolean(_settings.auto_am))) + basic.append( RadioSetting("left_sql", "Left Squelch", + RadioSettingValueList(SQLPRESET, + SQLPRESET[_settings.left_sql]))) + basic.append( RadioSetting("right_sql", "Right Squelch", + RadioSettingValueList(SQLPRESET, + SQLPRESET[_settings.right_sql]))) +# basic.append( RadioSetting("apo", "Auto Power off (0.1h)", +# RadioSettingValueInteger(0, 20,_settings.apo))) + opts = ["Off"] + ["%0.1f" % (t / 10.0) for t in range(1,21,1) ] + basic.append( RadioSetting("apo", "Auto Power off (Hours)", + RadioSettingValueList(opts, opts[_settings.apo]))) + opts = ["Off", "1", "2", "3", "Full"] + basic.append( RadioSetting("backlight", "Display Backlight", + RadioSettingValueList(opts, opts[_settings.backlight]))) + opts = ["Off", "Right", "Left", "Both"] + basic.append( RadioSetting("pttlock", "PTT Lock", + RadioSettingValueList(opts, opts[_settings.pttlock]))) + opts = ["Manual", "Auto"] + basic.append( RadioSetting("hyper_chan", "Hyper Channel", + RadioSettingValueList(opts, opts[_settings.hyper_chan]))) + opts = ["Key 1", "Key 2"] + basic.append( RadioSetting("right_func_key", "Right Function Key", + RadioSettingValueList(opts, opts[_settings.right_func_key]))) + opts = ["1000Hz", "1450Hz", "1750Hz", "2100Hz"] + basic.append( RadioSetting("tbst_freq", "Tone Burst Frequency", + RadioSettingValueList(opts, opts[_settings.tbst_freq]))) + opts = ["Off", "TX", "RX", "TX RX"] + basic.append( RadioSetting("mute_mode", "Mute Mode", + RadioSettingValueList(opts, opts[_settings.mute_mode]))) + opts = ["MEM", "MSM"] + basic.append( RadioSetting("scan_mode", "Scan Mode", + RadioSettingValueList(opts, opts[_settings.scan_mode]))) + opts = ["TO", "CO"] + basic.append( RadioSetting("scan_resume", "Scan Resume", + RadioSettingValueList(opts, opts[_settings.scan_resume]))) + opts = ["%0.1f" % (t / 10.0) for t in range(0,51,1) ] + basic.append( RadioSetting("exit_delay", "Span Transit Exit Delay", + RadioSettingValueList(opts, opts[_settings.exit_delay]))) + basic.append( RadioSetting("tot", "Time Out Timer (minutes)", + RadioSettingValueInteger(0, 30,_settings.tot))) + basic.append( RadioSetting("tot_alert", + "Time Out Timer Pre Alert(seconds)", + RadioSettingValueInteger(0, 15,_settings.tot_alert))) + basic.append( RadioSetting("tot_rekey", "Time Out Rekey (seconds)", + RadioSettingValueInteger(0, 15,_settings.tot_rekey))) + basic.append( RadioSetting("tot_reset", "Time Out Reset(seconds)", + RadioSettingValueInteger(0, 15,_settings.tot_reset))) + basic.append( RadioSetting("p1", "P1 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p1]))) + basic.append( RadioSetting("p2", "P2 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p2]))) + basic.append( RadioSetting("p3", "P3 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p3]))) + basic.append( RadioSetting("p4", "P4 Function", + RadioSettingValueList(MICKEYFUNC, MICKEYFUNC[_settings.p4]))) +# opts = ["0", "1"] +# basic.append( RadioSetting("x", "Desc", +# RadioSettingValueList(opts, opts[_settings.x]))) + + def _filter(name): + filtered = "" + for char in str(name): + if char in chirp_common.CHARSET_ASCII: + filtered += char + else: + filtered += " " + return filtered + + rsvs = RadioSettingValueString(0, 8, _filter(_info.sn)) + rsvs.set_mutable(False) + rs = RadioSetting("sn", "Serial Number", rsvs) + info.append(rs) + + rsvs = RadioSettingValueString(0, 8, _filter(_info.model)) + rsvs.set_mutable(False) + rs = RadioSetting("model", "Model Name", rsvs) + info.append(rs) + + rsvs = RadioSettingValueString(0, 16, _filter(_info.code)) + rsvs.set_mutable(False) + rs = RadioSetting("code", "Model Code", rsvs) + info.append(rs) + + progdate = "%d/%d/%d" % (_info.prog_mon, _info.prog_day, + _info.prog_yr ) + rsvs = RadioSettingValueString(0, 10, progdate) + rsvs.set_mutable(False) + rs = RadioSetting("progdate", "Last Program Date", rsvs) + info.append(rs) + + # 9 band limits + for i in range(0,9): + objname = BANDS[i] + "lorx" + objnamepp = BANDS[i] + " Rx Start" + #rsv = RadioSettingValueInteger(0,100000000, + # int(_bandlimits[i].lorx)) + rsv = RadioSettingValueString(0,10, + format_freq(int(_bandlimits[i].lorx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + objname = BANDS[i] + "hirx" + objnamepp = BANDS[i] + " Rx end" + rsv = RadioSettingValueString(0,10, + format_freq(int(_bandlimits[i].hirx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + objname = BANDS[i] + "lotx" + objnamepp = BANDS[i] + " Tx Start" + rsv = RadioSettingValueString(0,10, + format_freq(int(_bandlimits[i].lotx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + objname = BANDS[i] + "hitx" + objnamepp = BANDS[i] + " Tx end" + rsv = RadioSettingValueString(0,10, + format_freq(int(_bandlimits[i].hitx)*10)) + rsv.set_mutable(False) + rs = RadioSetting(objname, objnamepp, rsv) + info.append(rs) + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + _info = self._memobj.info + _bandlimits = self._memobj.bandlimits + 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 + + if CHIRP_DEBUG: + print "Setting %s(%s) <= %s" % (setting, + oldval, newval) + setattr(_settings, setting, newval) + except Exception, e: + print element.get_name() + raise @directory.register -class TYTTH9800File(chirp_common.FileBackedRadio): - """TYT TH-9800 .dat file""" - VENDOR = "TYT" - MODEL = "TH-9800" - FILE_EXTENSION = "dat" +class TYTTH9800File(TYTTH9800Base, chirp_common.FileBackedRadio): + """TYT TH-9800 .dat file""" + MODEL = "TH-9800 File" - _memsize = 69632 + FILE_EXTENSION = "dat" - def __init__(self, pipe): - self.errors = [] - self._mmap = None + _memsize = 69632 + _mmap_offset = 0x1100 + _scanlimits_offset = 0xC800 + _mmap_offset + _settings_offset = 0xCB20 + _mmap_offset + _chan_active_offset = 0xCB80 + _mmap_offset + _info_offset = 0xfe00 + _mmap_offset + + def __init__(self, pipe): + self.errors = [] + self._mmap = None - if isinstance(pipe, str): - self.pipe = None - self.load_mmap(pipe) - else: - chirp_common.FileBackedRadio.__init__(self, pipe) + if isinstance(pipe, str): + self.pipe = None + self.load_mmap(pipe) + else: + chirp_common.FileBackedRadio.__init__(self, pipe) - def get_features(self): - rf = chirp_common.RadioFeatures() - rf.memory_bounds = (1, 800) - rf.has_bank = False - rf.has_tuning_step = False - rf.can_odd_split = True - rf.valid_duplexes = ["", "-", "+", "split"] - rf.valid_tmodes = TMODES - rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "#*-+" - rf.valid_bands = [( 26000000, 33000000), - ( 47000000, 54000000), - (108000000, 180000000), - (350000000, 399000000), - (430000000, 512000000), - (750000000, 947000000)] - rf.valid_skips = SCAN_MODES - rf.valid_modes = MODES + ["AM"] - rf.valid_name_length = 6 + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize and filename.endswith('.dat') - return rf - @classmethod - def match_model(cls, filedata, filename): - return len(filedata) == cls._memsize and filename.endswith('.dat') +def _identify(radio): + """Do identify handshake with TYT""" + try: + radio.pipe.write("\x02PROGRA") + ack = radio.pipe.read(1) + if ack != "A": + util.hexprint(ack) + raise errors.RadioError("Radio did not ACK first command: %x" + % ord(ack)) + except: + print util.hexprint(ack) + raise errors.RadioError("Unable to communicate with the radio") - def process_mmap(self): - self._memobj = bitwise.parse(TH9800_MEM_FORMAT, self._mmap) + radio.pipe.write("M\x02") + ident = radio.pipe.read(16) + radio.pipe.write("A") + r = radio.pipe.read(1) + if r != "A": + raise errors.RadioError("Ack failed") + return ident - def get_raw_memory(self, number): - return repr(self._memobj.memory[number - 1]) - def get_memory(self, number): - _mem = self._memobj.memory[number - 1] - mem = chirp_common.Memory() - mem.number = number - if _mem.get_raw().startswith("\xFF\xFF\xFF\xFF"): - mem.empty = True - return mem +def _download(radio, memsize = 0x10000, blocksize = 0x80): + """Download from TYT TH-9800""" + data = _identify(radio) + print "ident:", util.hexprint(data) + offset = 0x100 + for addr in range(offset, memsize, blocksize): + msg = struct.pack(">cHB", "R", addr, blocksize) + radio.pipe.write(msg) + block = radio.pipe.read(blocksize + 4) + if len(block) != (blocksize + 4): + print util.hexprint(block) + raise errors.RadioError("Radio sent a short block") + radio.pipe.write("A") + ack = radio.pipe.read(1) + if ack != "A": + print util.hexprint(ack) + raise errors.RadioError("Radio NAKed block") + data += block[4:] - mem.freq = int(_mem.rx_freq) * 10 + if radio.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = memsize + status.msg = "Cloning from radio" + radio.status_fn(status) - txfreq = int(_mem.tx_freq) * 10 - if txfreq == mem.freq: - mem.duplex = "" - elif abs(txfreq - mem.freq) > 70000000: - mem.duplex = "split" - mem.offset = txfreq - elif txfreq < mem.freq: - mem.duplex = "-" - mem.offset = mem.freq - txfreq - elif txfreq > mem.freq: - mem.duplex = "+" - mem.offset = txfreq - mem.freq + radio.pipe.write("ENDR") - mem.dtcs_polarity = DTCS_POLARITY[_mem.dtcs_pol] + return memmap.MemoryMap(data) - mem.tmode = TMODES[int(_mem.tmode)] - mem.ctone = mem.rtone = int(_mem.ctcss) / 10.0 - mem.dtcs = int(_mem.dtcs) - mem.name = str(_mem.name) - mem.name = mem.name.replace("\xFF", " ").rstrip() +def _upload(radio, memsize = 0xF400, blocksize = 0x80): + """Upload to TYT TH-9800""" + data = _identify(radio) - mem.skip = SCAN_MODES[int(_mem.scan)] - mem.mode = _mem.am and "AM" or MODES[int(_mem.fmdev)] + radio.pipe.setTimeout(1) - return mem + if data != radio._mmap[:radio._mmap_offset]: + raise errors.RadioError( + "Model mis-match: \n%s\n%s" % (util.hexprint(data), + util.hexprint(radio._mmap[:radio._mmap_offset]))) + # in the factory software they update the last program date when + # they upload, So let's do the same + today = date.today() + y = today.year + m = today.month + d = today.day + _info = radio._memobj.info + if CHIRP_DEBUG: + ly = _info.prog_yr + lm = _info.prog_mon + ld = _info.prog_day + print "Updating last program date:%d/%d/%d" % (lm,ld,ly, m,d,y) + print " to today:%d/%d/%d" % (m,d,y) - def set_memory(self, mem): - _mem = self._memobj.memory[mem.number - 1] - if mem.empty: - _mem.set_raw(BLANK_MEMORY) - return + _info.prog_yr = y + _info.prog_mon = m + _info.prog_day = d - if _mem.get_raw() == BLANK_MEMORY: - print "Initializing empty memory" - _mem.set_raw(BLANK_MEMORY) + offset = 0x0100 + for addr in range(offset, memsize, blocksize): + mapaddr = addr + radio._mmap_offset - offset + if CHIRP_DEBUG: + print "addr: 0x%04X, mmapaddr: 0x%04X" % (addr, mapaddr) + msg = struct.pack(">cHB", "W", addr, blocksize) + msg += radio._mmap[mapaddr:(mapaddr + blocksize)] + print util.hexprint(msg) + radio.pipe.write(msg) + ack = radio.pipe.read(1) + if ack != "A": + print util.hexprint(ack) + raise errors.RadioError("Radio did not ack block 0x%04X" % addr) - _mem.rx_freq = mem.freq / 10 - if mem.duplex == "split": - _mem.tx_freq = mem.offset / 10 - elif mem.duplex == "-": - _mem.tx_freq = (mem.freq - mem.offset) / 10 - elif mem.duplex == "+": - _mem.tx_freq = (mem.freq + mem.offset) / 10 - else: - _mem.tx_freq = mem.freq / 10 + if radio.status_fn: + status = chirp_common.Status() + status.cur = addr + status.max = memsize + status.msg = "Cloning to radio" + radio.status_fn(status) - _mem.tmode = TMODES.index(mem.tmode) - _mem.ctcss = mem.rtone * 10 - _mem.dtcs = mem.dtcs - _mem.dtcs_pol = DTCS_POLARITY.index(mem.dtcs_polarity) + # End of clone + radio.pipe.write("ENDW") - _mem.name = mem.name.ljust(6, "\xFF") + # Checksum? + final_data = radio.pipe.read(3) + if CHIRP_DEBUG: + print "final:", util.hexprint(final_data) - _mem.scan = SCAN_MODES.index(mem.skip) +@directory.register +class TYTTH9800Radio(TYTTH9800Base, chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + VENDOR = "TYT" + MODEL = "TH-9800" + BAUD_RATE = 38400 - if mem.mode == "AM": - _mem.am = True - _mem.fmdev = 0 - else: - _mem.am = False - _mem.fmdev = MODES.index(mem.mode) + _memsize = 65296 + _mmap_offset = 0x0010 + _scanlimits_offset = 0xC800 + _mmap_offset + _settings_offset = 0xCB20 + _mmap_offset + _chan_active_offset = 0xCB80 + _mmap_offset + _info_offset = 0xfe00 + _mmap_offset + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ( + 'This is experimental support for TH-9800 ' + 'which is still under development.\n' + 'Please ensure you have a good backup with OEM software.\n' + 'Also please send in bug and enhancement requests!\n' + 'You have been warned. Proceed at your own risk!') + return rp + + def sync_in(self): + self._mmap = _download(self) + self.process_mmap() + + def sync_out(self): + _upload(self) +