# HG changeset patch
# User Jim Unroe
# Date 1605557938 18000
# Mon Nov 16 15:18:58 2020 -0500
# Node ID 3fd7ed9a0de01f7897286c9ac274237a45cc7831
# Parent d5e496f563fdfc9ea89dea5f119357235b82db6f
[TH-UV88] New Model: TYT TH-UV88
This patch adds support for the TYT TH-UV88
Initial radio protocol decode, channels and memory layout
by James Berry , Summer 2020
Related to #7817
diff -r d5e496f563fd -r 3fd7ed9a0de0 chirp/drivers/th_uv88.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/th_uv88.py Mon Nov 16 15:18:58 2020 -0500
@@ -0,0 +1,917 @@
+# Version 1.0 for TYT-UV88
+# Initial radio protocol decode, channels and memory layout
+# by James Berry , Summer 2020
+# Additional configuration and help, Jim Unroe
+#
+# 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 {
+ u8 name1[16]; // Intro Screen Line 1 (16 alpha text characters)
+ u8 name2[16]; // Intro Screen Line 2 (16 alpha text characters)
+} 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(16): # 0 - 16
+ name += chr(self._memobj.openradioname.name1[i])
+ name = name.rstrip() # remove trailing spaces
+
+ rx = RadioSettingValueString(0, 16, name)
+ rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
+ advanced.append(rset)
+
+ # software only
+ name = ""
+ for i in range(16): # 0 - 16
+ name += chr(self._memobj.openradioname.name2[i])
+ name = name.rstrip() # remove trailing spaces
+
+ rx = RadioSettingValueString(0, 16, 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):
+ return
+ _settings = self._memobj.settings
+ _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 3fd7ed9a0de0 tools/cpep8.manifest
--- a/tools/cpep8.manifest Fri Nov 13 08:07:04 2020 -0500
+++ b/tools/cpep8.manifest Mon Nov 16 15:18:58 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