[chirp_devel] [PATCH] [TH-UV88] New Model: TYT TH-UV88 (replacement for previous patch)
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1605729943 18000 # Wed Nov 18 15:05:43 2020 -0500 # Node ID 9379757e3946f6da034d96093ad188c9b93dd622 # Parent d5e496f563fdfc9ea89dea5f119357235b82db6f [TH-UV88] New Model: TYT TH-UV88 (replacement for previous patch)
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout by James Berry james@coppermoth.com, Summer 2020
Related to #7817
diff -r d5e496f563fd -r 9379757e3946 chirp/drivers/th_uv88.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/th_uv88.py Wed Nov 18 15:05:43 2020 -0500 @@ -0,0 +1,918 @@ +# Version 1.0 for TYT-UV88 +# Initial radio protocol decode, channels and memory layout +# by James Berry james@coppermoth.com, Summer 2020 +# Additional configuration and help, Jim Unroe rock.unroe@gmail.com +# +# 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/. + +import time +import struct +import logging +import re +import math +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettingValueFloat, RadioSettings, InvalidValueError +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MEM_FORMAT = """ +struct chns { + ul32 rxfreq; + ul32 txfreq; + ul16 scramble:4 + rxtone:12; //decode:12 + ul16 decodeDSCI:1 + encodeDSCI:1 + unk1:1 + unk2:1 + txtone:12; //encode:12 + u8 power:2 + wide:2 + b_lock:2 + unk3:2; + u8 unk4:3 + signal:2 + displayName:1 + unk5:2; + u8 unk6:2 + pttid:2 + step:4; // not required + u8 name[6]; +}; + +struct vfo { + ul32 rxfreq; + ul32 txfreq; // displayed as an offset + ul16 scramble:4 + rxtone:12; //decode:12 + ul16 decodeDSCI:1 + encodeDSCI:1 + unk1:1 + unk2:1 + txtone:12; //encode:12 + u8 power:2 + wide:2 + b_lock:2 + unk3:2; + u8 unk4:3 + signal:2 + displayName:1 + unk5:2; + u8 unk6:2 + pttid:2 + step:4; + u8 name[6]; +}; + +struct chname { + u8 extra_name[10]; +}; + +#seekto 0x0000; +struct chns chan_mem[199]; + +#seekto 0x1960; +struct chname chan_name[199]; + +#seekto 0x1180; +struct { + u8 bitmap[26]; // one bit for each channel marked in use +} chan_avail; + +#seekto 0x11A0; +struct { + u8 bitmap[26]; // one bit for each channel skipped +} chan_skip; + +#seekto 0x1140; +struct { + u8 autoKeylock:1, // 0x1140 [18] *OFF, On + unk_bit6_5:2, // + vfomrmode:1, // *VFO, MR + unk_bit3_0:4; // + u8 unk_1141; // 0x1141 + u8 unk_1142; // 0x1142 + u8 unk_bit7_3:5, // + ab:1, // * A, B + unk_bit1_0:2; // +} workmodesettings; + +#seekto 0x1160; +struct { + u8 introScreen1[12]; // 0x1160 *Intro Screen Line 1(truncated to 12 alpha + // text characters) + u8 offFreqVoltage : 3, // 0x116C unknown referred to in code but not on + // screen + unk_bit4 : 1, // + sqlLevel : 4; // [05] *OFF, 1-9 + u8 beep : 1 // 0x116D [09] *OFF, On + callKind : 2, // code says 1750,2100,1000,1450 as options + // not on screen + introScreen: 2, // [20] *OFF, Voltage, Char String + unkstr2: 2, // + txChSelect : 1; // [02] *Last CH, Main CH + u8 autoPowOff : 3, // 0x116E not on screen? OFF, 30Min, 1HR, 2HR + unk : 1, // + tot : 4; // [11] *OFF, 30 Second, 60 Second, 90 Second, + // ... , 270 Second + u8 unk_bit7:1, // 0x116F + roger:1, // [14] *OFF, On + dailDef:1, // Unknown - 'Volume, Frequency' + language:1, // ?Chinese, English + unk_bit3:1, // + endToneElim:1, // *OFF, Frequency + unkCheckBox1:1, // + unkCheckBox2:1; // + u8 scanResumeTime : 2, // 0x1170 2S, 5S, 10S, 15S (not on screen) + disMode : 2, // [33] *Frequency, Channel, Name + scanType: 2, // [17] *To, Co, Se + ledMode: 2; // [07] *Off, On, Auto + u8 unky; // 0x1171 + u8 str6; // 0x1172 Has flags to do with logging - factory + // enabled (bits 16,64,128) + u8 unk; // 0x1173 + u8 swAudio : 1, // 0x1174 [19] *OFF, On + radioMoni : 1, // [34]*OFF, On + keylock : 1, // *OFF, Auto + dualWait : 1, // [06] *OFF, On + unk_bit3 : 1, // + light : 3; // [08] *1, 2, 3, 4, 5, 6, 7 + u8 voxSw : 1, // 0x1175 [13] *OFF, On + voxDelay: 4, // *0.5S, 1.0S, 1.5S, 2.0S, 2.5S, 3.0S, 3.5S, + // 4.0S, 4.5S, 5.0S + voxLevel : 3; // [03] *1, 2, 3, 4, 5, 6, 7 + u8 str9 : 4, // 0x1176 + saveMode : 2, // [16] *OFF, 1:1, 1:2, 1:4 + keyMode : 2; // [32] *ALL, PTT, KEY, Key & Side Key + u8 unk2; // 0x1177 + u8 unk3; // 0x1178 + u8 unk4; // 0x1179 + u8 name2[6]; // 0x117A unused +} basicsettings; + +#seekto 0x1940; +struct { + char name1[15]; // Intro Screen Line 1 (16 alpha text characters) + u8 unk1; + char name2[15]; // Intro Screen Line 2 (16 alpha text characters) + u8 unk2; +} openradioname; + +""" + +MEM_SIZE = 0x22A0 +BLOCK_SIZE = 0x20 +STIMEOUT = 2 +BAUDRATE = 57600 + +# Channel power: 3 levels +POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00), + chirp_common.PowerLevel("Mid", watts=2.50), + chirp_common.PowerLevel("Low", watts=0.50)] + +SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"] +B_LOCK_LIST = ["OFF", "Sub", "Carrier"] +OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"] +PTTID_LIST = ["Off", "BOT", "EOT", "Both"] +STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0] +LIST_STEPS = [str(x) for x in STEPS] + + +def _clean_buffer(radio): + radio.pipe.timeout = 0.005 + junk = radio.pipe.read(256) + radio.pipe.timeout = STIMEOUT + if junk: + LOG.debug("Got %i bytes of junk before starting" % len(junk)) + + +def _rawrecv(radio, amount): + """Raw read from the radio device""" + data = "" + try: + data = radio.pipe.read(amount) + except Exception: + _exit_program_mode(radio) + msg = "Generic error reading data from radio; check your cable." + raise errors.RadioError(msg) + + if len(data) != amount: + _exit_program_mode(radio) + msg = "Error reading from radio: not the amount of data we want." + raise errors.RadioError(msg) + + return data + + +def _rawsend(radio, data): + """Raw send to the radio device""" + try: + radio.pipe.write(data) + except Exception: + raise errors.RadioError("Error sending data to radio") + + +def _make_read_frame(addr, length): + frame = "\xFE\xFE\xEE\xEF\xEB" + """Pack the info in the header format""" + frame += struct.pack(">ih", addr, length) + + frame += "\xFD" + # Return the data + return frame + + +def _make_write_frame(addr, length, data=""): + frame = "\xFE\xFE\xEE\xEF\xE4" + + """Pack the info in the header format""" + output = struct.pack(">ih", addr, length) + # Add the data if set + if len(data) != 0: + output += data + + frame += output + frame += _calculate_checksum(output) + + frame += "\xFD" + # Return the data + return frame + + +def _calculate_checksum(data): + num = 0 + for x in range(0, len(data)): + num = (num + ord(data[x])) % 256 + + if num == 0: + return chr(0) + + return chr(256 - num) + + +def _recv(radio, addr, length): + """Get data from the radio """ + + data = _rawrecv(radio, length) + + # DEBUG + LOG.info("Response:") + LOG.debug(util.hexprint(data)) + + return data + + +def _do_ident(radio): + """Put the radio in PROGRAM mode & identify it""" + radio.pipe.baudrate = BAUDRATE + radio.pipe.parity = "N" + radio.pipe.timeout = STIMEOUT + + # Flush input buffer + _clean_buffer(radio) + + # Ident radio + magic = "\xFE\xFE\xEE\xEF\xE0\x55\x56\x38\x38\xFD" + _rawsend(radio, magic) + ack = _rawrecv(radio, 36) + + if not ack.startswith("\xFE\xFE\xEF\xEE\xE1\x55\x56\x38\x38" + ) or not ack.endswith("\xFD"): + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond as expected (A)") + + return True + + +def _exit_program_mode(radio): + # This may be the last part of a read + magic = "\xFE\xFE\xEE\xEF\xE5\x55\x56\x38\x38\xFD" + _rawsend(radio, magic) + ack = _rawrecv(radio, 7) + if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond as expected (B)") + + +def _download(radio): + """Get the memory map""" + + # Put radio in program mode and identify it + _do_ident(radio) + + # Enter read mode + magic = "\xFE\xFE\xEE\xEF\xE2\x55\x56\x38\x38\xFD" + _rawsend(radio, magic) + ack = _rawrecv(radio, 7) + if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond to enter read mode") + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + data = "" + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + frame = _make_read_frame(addr, BLOCK_SIZE) + # DEBUG + LOG.debug("Frame=" + util.hexprint(frame)) + + # Sending the read request + _rawsend(radio, frame) + + # Now we read data + d = _recv(radio, addr, BLOCK_SIZE + 13) + + LOG.debug("Response Data= " + util.hexprint(d)) + + if not d.startswith("\xFE\xFE\xEF\xEE\xE4"): + LOG.warning("Incorrect start") + if not d.endswith("\xFD"): + LOG.warning("Incorrect end") + # could validate the block data + + # Aggregate the data + data += d[11:-2] + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning from radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + return data + + +def _upload(radio): + """Upload procedure""" + # Put radio in program mode and identify it + _do_ident(radio) + + magic = "\xFE\xFE\xEE\xEF\xE3\x55\x56\x38\x38\xFD" + _rawsend(radio, magic) + ack = _rawrecv(radio, 7) + if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD": + _exit_program_mode(radio) + if ack: + LOG.debug(repr(ack)) + raise errors.RadioError("Radio did not respond to enter write mode") + + # UI progress + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + # The fun starts here + for addr in range(0, MEM_SIZE, BLOCK_SIZE): + # Official programmer skips writing these memory locations + if addr >= 0x1680 and addr < 0x1940: + continue + + # Sending the data + data = radio.get_mmap()[addr:addr + BLOCK_SIZE] + + frame = _make_write_frame(addr, BLOCK_SIZE, data) + LOG.warning("Frame:%s:" % util.hexprint(frame)) + _rawsend(radio, frame) + + ack = _rawrecv(radio, 7) + LOG.debug("Response Data= " + util.hexprint(ack)) + + if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"): + LOG.warning("Unexpected response") + _exit_program_mode(radio) + msg = "Bad ack writing block 0x%04x" % addr + raise errors.RadioError(msg) + + # UI Update + status.cur = addr / BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _exit_program_mode(radio) + + +def _do_map(chn, sclr, mary): + """Set or Clear the chn (1-128) bit in mary[] word array map""" + # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state + # mary[] is u8 array, but the map is by nibbles + ndx = int(math.floor((chn - 1) / 8)) + bv = (chn - 1) % 8 + msk = 1 << bv + mapbit = sclr + if sclr == 1: # Set the bit + mary[ndx] = mary[ndx] | msk + elif sclr == 0: # clear + mary[ndx] = mary[ndx] & (~ msk) # ~ is complement + else: # return current bit state + mapbit = 0 + if (mary[ndx] & msk) > 0: + mapbit = 1 + return mapbit + + +@directory.register +class THUV88Radio(chirp_common.CloneModeRadio): + """TYT UV88 Radio""" + VENDOR = "TYT" + MODEL = "TH-UV88" + MODES = ['WFM', 'FM', 'NFM'] + TONES = chirp_common.TONES + DTCS_CODES = chirp_common.DTCS_CODES + NAME_LENGTH = 10 + DTMF_CHARS = list("0123456789ABCD*#") + # 136-174, 400-480 + VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)] + + # Valid chars on the LCD + VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \ + "`!"#$%&'()*+,-./:;<=>?@[]^_" + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.info = \ + ('TYT UV-88\n') + + rp.pre_download = _(dedent("""\ + This is an early stage beta driver + """)) + rp.pre_upload = _(dedent("""\ + This is an early stage beta driver - upload at your own risk + """)) + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_comment = False + rf.has_tuning_step = False # Not as chan feature + rf.valid_tuning_steps = STEPS + rf.can_odd_split = False + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.has_sub_devices = False + rf.valid_name_length = self.NAME_LENGTH + rf.valid_modes = self.MODES + rf.valid_characters = self.VALID_CHARS + rf.valid_duplexes = ["-", "+", "off", ""] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS", + "Tone->DTCS", "DTCS->Tone", "->Tone", + "DTCS->DTCS"] + rf.valid_skips = [] + rf.valid_power_levels = POWER_LEVELS + rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES # this is just to + # get it working, not sure this is right + rf.valid_bands = self.VALID_BANDS + rf.memory_bounds = (1, 199) + rf.valid_skips = ["", "S"] + return rf + + def sync_in(self): + """Download from radio""" + try: + data = _download(self) + except errors.RadioError: + # Pass through any real errors we raise + raise + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during download') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + self._mmap = memmap.MemoryMap(data) + self.process_mmap() + + def sync_out(self): + """Upload to radio""" + + try: + _upload(self) + except Exception: + # If anything unexpected happens, make sure we raise + # a RadioError and log the problem + LOG.exception('Unexpected error during upload') + raise errors.RadioError('Unexpected error communicating ' + 'with the radio') + + def process_mmap(self): + """Process the mem map into the mem object""" + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def set_memory(self, memory): + """A value in a UI column for chan 'number' has been modified.""" + # update all raw channel memory values (_mem) from UI (mem) + _mem = self._memobj.chan_mem[memory.number - 1] + _name = self._memobj.chan_name[memory.number - 1] + + if memory.empty: + _do_map(memory.number, 0, self._memobj.chan_avail.bitmap) + return + + _do_map(memory.number, 1, self._memobj.chan_avail.bitmap) + + if memory.skip == "": + _do_map(memory.number, 1, self._memobj.chan_skip.bitmap) + else: + _do_map(memory.number, 0, self._memobj.chan_skip.bitmap) + + return self._set_memory(memory, _mem, _name) + + def get_memory(self, number): + # radio first channel is 1, mem map is base 0 + _mem = self._memobj.chan_mem[number - 1] + _name = self._memobj.chan_name[number - 1] + mem = chirp_common.Memory() + mem.number = number + + # Determine if channel is empty + + if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0: + mem.empty = True + return mem + + if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0: + mem.skip = "" + else: + mem.skip = "S" + + return self._get_memory(mem, _mem, _name) + + def _get_memory(self, mem, _mem, _name): + """Convert raw channel memory data into UI columns""" + mem.extra = RadioSettingGroup("extra", "Extra") + + mem.empty = False + # This function process both 'normal' and Freq up/down' entries + mem.freq = int(_mem.rxfreq) * 10 + + if _mem.txfreq == 0xFFFFFFFF: + # TX freq not set + mem.duplex = "off" + mem.offset = 0 + elif int(_mem.rxfreq) == int(_mem.txfreq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \ + and "-" or "+" + mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10 + + mem.name = "" + for i in range(6): # 0 - 6 + mem.name += chr(_mem.name[i]) + for i in range(10): + mem.name += chr(_name.extra_name[i]) + + mem.name = mem.name.rstrip() # remove trailing spaces + + # ########## TONE ########## + + if _mem.txtone > 2600: + # All off + txmode = "" + elif _mem.txtone > 511: + txmode = "Tone" + mem.rtone = int(_mem.txtone) / 10.0 + else: + # DTSC + txmode = "DTCS" + mem.dtcs = int(format(int(_mem.txtone), 'o')) + + if _mem.rxtone > 2600: + rxmode = "" + elif _mem.rxtone > 511: + rxmode = "Tone" + mem.ctone = int(_mem.rxtone) / 10.0 + else: + rxmode = "DTCS" + mem.rx_dtcs = int(format(int(_mem.rxtone), 'o')) + + mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + ( + "N", "R")[_mem.decodeDSCI] + + mem.tmode = "" + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + # ########## TONE ########## + + mem.mode = self.MODES[_mem.wide] + mem.power = POWER_LEVELS[int(_mem.power)] + + b_lock = RadioSetting("b_lock", "B_Lock", + RadioSettingValueList(B_LOCK_LIST, + B_LOCK_LIST[_mem.b_lock])) + mem.extra.append(b_lock) + + b_lock = RadioSetting("step", "Step", + RadioSettingValueList(LIST_STEPS, + LIST_STEPS[_mem.step])) + mem.extra.append(b_lock) + + scramble_value = _mem.scramble + if scramble_value >= 8: # Looks like OFF is 0x0f ** CONFIRM + scramble_value = 0 + scramble = RadioSetting("scramble", "Scramble", + RadioSettingValueList(SCRAMBLE_LIST, + SCRAMBLE_LIST[ + scramble_value])) + mem.extra.append(scramble) + + optsig = RadioSetting("signal", "Optional signaling", + RadioSettingValueList( + OPTSIG_LIST, + OPTSIG_LIST[_mem.signal])) + mem.extra.append(optsig) + + rs = RadioSetting("pttid", "PTT ID", + RadioSettingValueList(PTTID_LIST, + PTTID_LIST[_mem.pttid])) + mem.extra.append(rs) + + return mem + + def _set_memory(self, mem, _mem, _name): + # """Convert UI column data (mem) into MEM_FORMAT memory (_mem).""" + + _mem.rxfreq = mem.freq / 10 + if mem.duplex == "off": + _mem.txfreq = 0xFFFFFFFF + elif mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) / 10 + else: + _mem.txfreq = _mem.rxfreq + + out_name = mem.name.ljust(16) + + for i in range(6): # 0 - 6 + _mem.name[i] = ord(out_name[i]) + for i in range(10): + _name.extra_name[i] = ord(out_name[i+6]) + + if mem.name != "": + _mem.displayName = 1 # Name only displayed if this is set on + else: + _mem.displayName = 0 + + rxmode = "" + txmode = "" + + if mem.tmode == "Tone": + txmode = "Tone" + elif mem.tmode == "TSQL": + rxmode = "Tone" + txmode = "TSQL" + elif mem.tmode == "DTCS": + rxmode = "DTCSSQL" + txmode = "DTCS" + elif mem.tmode == "Cross": + txmode, rxmode = mem.cross_mode.split("->", 1) + + if mem.dtcs_polarity[1] == "N": + _mem.decodeDSCI = 0 + else: + _mem.decodeDSCI = 1 + + if rxmode == "": + _mem.rxtone = 0xFFF + elif rxmode == "Tone": + _mem.rxtone = int(float(mem.ctone) * 10) + elif rxmode == "DTCSSQL": + _mem.rxtone = int(str(mem.dtcs), 8) + elif rxmode == "DTCS": + _mem.rxtone = int(str(mem.rx_dtcs), 8) + + if mem.dtcs_polarity[0] == "N": + _mem.encodeDSCI = 0 + else: + _mem.encodeDSCI = 1 + + if txmode == "": + _mem.txtone = 0xFFF + elif txmode == "Tone": + _mem.txtone = int(float(mem.rtone) * 10) + elif txmode == "TSQL": + _mem.txtone = int(float(mem.ctone) * 10) + elif txmode == "DTCS": + _mem.txtone = int(str(mem.dtcs), 8) + + _mem.wide = self.MODES.index(mem.mode) + _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power) + + for element in mem.extra: + setattr(_mem, element.get_name(), element.value) + + return + + def get_settings(self): + """Translate the MEM_FORMAT structs into setstuf in the UI""" + _settings = self._memobj.basicsettings + _workmode = self._memobj.workmodesettings + + basic = RadioSettingGroup("basic", "Basic Settings") + group = RadioSettings(basic) + + # Menu 02 - TX Channel Select + options = ["Last Channel", "Main Channel"] + rx = RadioSettingValueList(options, options[_settings.txChSelect]) + rset = RadioSetting("basicsettings.txChSelect", + "Priority Transmit", rx) + basic.append(rset) + + # Menu 03 - VOX Level + rx = RadioSettingValueInteger(1, 7, _settings.voxLevel - 1) + rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx) + basic.append(rset) + + # Menu 05 - Squelch Level + options = ["OFF"] + ["%s" % x for x in range(1, 10)] + rx = RadioSettingValueList(options, options[_settings.sqlLevel]) + rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx) + basic.append(rset) + + # Menu 06 - Dual Wait + rx = RadioSettingValueBoolean(_settings.dualWait) + rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx) + basic.append(rset) + + # Menu 07 - LED Mode + options = ["Off", "On", "Auto"] + rx = RadioSettingValueList(options, options[_settings.ledMode]) + rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx) + basic.append(rset) + + # Menu 08 - Light + options = ["%s" % x for x in range(1, 8)] + rx = RadioSettingValueList(options, options[_settings.light]) + rset = RadioSetting("basicsettings.light", + "Background Light Color", rx) + basic.append(rset) + + # Menu 09 - Beep + rx = RadioSettingValueBoolean(_settings.beep) + rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx) + basic.append(rset) + + # Menu 11 - TOT + options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)] + rx = RadioSettingValueList(options, options[_settings.tot]) + rset = RadioSetting("basicsettings.tot", + "Transmission Time-out Timer", rx) + basic.append(rset) + + # Menu 13 - VOX Switch + rx = RadioSettingValueBoolean(_settings.voxSw) + rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx) + basic.append(rset) + + # Menu 14 - Roger + rx = RadioSettingValueBoolean(_settings.roger) + rset = RadioSetting("basicsettings.roger", "Roger Beep", rx) + basic.append(rset) + + # Menu 16 - Save Mode + options = ["Off", "1:1", "1:2", "1:4"] + rx = RadioSettingValueList(options, options[_settings.saveMode]) + rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx) + basic.append(rset) + + # Menu 33 - Display Mode + options = ['Frequency', 'Channel', 'Name'] + rx = RadioSettingValueList(options, options[_settings.disMode]) + rset = RadioSetting("basicsettings.disMode", "LED Display Mode", rx) + basic.append(rset) + + advanced = RadioSettingGroup("advanced", "Advanced Settings") + group.append(advanced) + + # software only + options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S', + '4.0S', '4.5S', '5.0S'] + rx = RadioSettingValueList(options, options[_settings.voxDelay]) + rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx) + advanced.append(rset) + + # software only + name = "" + for i in range(15): # 0 - 15 + name += chr(self._memobj.openradioname.name1[i]) + name = name.rstrip() # remove trailing spaces + + rx = RadioSettingValueString(0, 15, name) + rset = RadioSetting("openradioname.name1", "Intro Line 1", rx) + advanced.append(rset) + + # software only + name = "" + for i in range(15): # 0 - 15 + name += chr(self._memobj.openradioname.name2[i]) + name = name.rstrip() # remove trailing spaces + + rx = RadioSettingValueString(0, 15, name) + rset = RadioSetting("openradioname.name2", "Intro Line 2", rx) + advanced.append(rset) + + workmode = RadioSettingGroup("workmode", "Work Mode Settings") + group.append(workmode) + + # Toggle with [#] key + options = ["Frequency", "Channel"] + rx = RadioSettingValueList(options, options[_workmode.vfomrmode]) + rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx) + workmode.append(rset) + + # Toggle with [A/B] key + options = ["A", "B"] + rx = RadioSettingValueList(options, options[_workmode.ab]) + rset = RadioSetting("workmodesettings.ab", "A/B Select", rx) + workmode.append(rset) + + return group # END get_settings() + + def set_settings(self, settings): + _settings = self._memobj.basicsettings + _mem = self._memobj + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + try: + name = element.get_name() + if "." in name: + bits = name.split(".") + obj = self._memobj + for bit in bits[:-1]: + if "/" in bit: + bit, index = bit.split("/", 1) + index = int(index) + obj = getattr(obj, bit)[index] + else: + obj = getattr(obj, bit) + setting = bits[-1] + else: + obj = _settings + setting = element.get_name() + + if element.has_apply_callback(): + LOG.debug("Using apply callback") + element.run_apply_callback() + elif setting == "voxLevel": + setattr(obj, setting, int(element.value) + 1) + elif element.value.get_mutable(): + LOG.debug("Setting %s = %s" % (setting, element.value)) + setattr(obj, setting, element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise diff -r d5e496f563fd -r 9379757e3946 tools/cpep8.manifest --- a/tools/cpep8.manifest Fri Nov 13 08:07:04 2020 -0500 +++ b/tools/cpep8.manifest Wed Nov 18 15:05:43 2020 -0500 @@ -77,6 +77,7 @@ ./chirp/drivers/th_uv3r.py ./chirp/drivers/th_uv3r25.py ./chirp/drivers/th_uv8000.py +./chirp/drivers/th_uv88.py ./chirp/drivers/th_uvf8d.py ./chirp/drivers/thd72.py ./chirp/drivers/thuv1f.py
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1605729943 18000 # Wed Nov 18 15:05:43 2020 -0500 # Node ID 9379757e3946f6da034d96093ad188c9b93dd622 # Parent d5e496f563fdfc9ea89dea5f119357235b82db6f [TH-UV88] New Model: TYT TH-UV88 (replacement for previous patch)
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout by James Berry james@coppermoth.com, Summer 2020
Related to #7817
Can we get an image for testing this?
+def _clean_buffer(radio):
- radio.pipe.timeout = 0.005
This is 5ms, too low to be meaningful on most systems I think, and it looks like this is just for the serial purge. I think something like 100ms is a lot better. I'm not sure how long it takes to read 256 bytes at 57600 but it wouldn't surprise me if this is too short to even clear most buffers.
+def _make_read_frame(addr, length):
- frame = "\xFE\xFE\xEE\xEF\xEB"
This isn't going to be py3 compatible, but I'm so behind on processing this stuff I guess maybe I shouldn't be too picky.
- """Pack the info in the header format"""
- frame += struct.pack(">ih", addr, length)
- frame += "\xFD"
- # Return the data
- return frame
Also, this does look a *lot* like the icon protocol. Have you tried seeing if the icom radio base will talk to this?
--Dan
Dan,
Jim sent an image… It does look a bit like the ICOM protocol (I started there) – but uses its own subcommand just for doing a memory dump and upload. So unfortunately no overlap. I hope that it will work with other TYT radios though because the programmer seems to be written with multiple radios in mind (I decompiled it to get some of the checksum info calculation and memory layout).
Jim did the topping and tailing (I don’t have a lot of time for testing now), the timeout as is was as it was in the driver that I cribbed from – I’m sure it could be almost anything.
Best wishes James
From: chirp_devel-bounces@intrepid.danplanet.com chirp_devel-bounces@intrepid.danplanet.com Date: Thursday, 19 November 2020 at 20:58 To: chirp_devel@intrepid.danplanet.com chirp_devel@intrepid.danplanet.com Subject: Re: [chirp_devel] [PATCH] [TH-UV88] New Model: TYT TH-UV88 (replacement for previous patch)
# HG changeset patch # User Jim Unroe rock.unroe@gmail.com # Date 1605729943 18000 # Wed Nov 18 15:05:43 2020 -0500 # Node ID 9379757e3946f6da034d96093ad188c9b93dd622 # Parent d5e496f563fdfc9ea89dea5f119357235b82db6f [TH-UV88] New Model: TYT TH-UV88 (replacement for previous patch)
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout by James Berry james@coppermoth.com, Summer 2020
Related to #7817
Can we get an image for testing this?
+def _clean_buffer(radio):
- radio.pipe.timeout = 0.005
This is 5ms, too low to be meaningful on most systems I think, and it looks like this is just for the serial purge. I think something like 100ms is a lot better. I'm not sure how long it takes to read 256 bytes at 57600 but it wouldn't surprise me if this is too short to even clear most buffers.
+def _make_read_frame(addr, length):
- frame = "\xFE\xFE\xEE\xEF\xEB"
This isn't going to be py3 compatible, but I'm so behind on processing this stuff I guess maybe I shouldn't be too picky.
- """Pack the info in the header format"""
- frame += struct.pack(">ih", addr, length)
- frame += "\xFD"
- # Return the data
- return frame
Also, this does look a *lot* like the icon protocol. Have you tried seeing if the icom radio base will talk to this?
--Dan _______________________________________________ chirp_devel mailing list chirp_devel@intrepid.danplanet.com http://intrepid.danplanet.com/mailman/listinfo/chirp_devel Developer docs: http://chirp.danplanet.com/projects/chirp/wiki/Developers
Jim sent an image…
In the next patch, sorry I hadn't seen it yet when I sent that.
It does look a bit like the ICOM protocol (I started there) – but uses its own subcommand just for doing a memory dump and upload. So unfortunately no overlap.
Okay.
Jim did the topping and tailing (I don’t have a lot of time for testing now), the timeout as is was as it was in the driver that I cribbed from – I’m sure it could be almost anything.
Yeah, that was my guess.
Jim, you also sent a couple other versions of the patch (in HTML form). Were those re-sends or are they different?
--Dan
participants (3)
-
Dan Smith
-
James Berry
-
Jim Unroe