# 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>