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