[chirp_devel] Fwd: [PATCH] [RT76P] Add Retevis RT76P

Jim Unroe
Sun May 30 19:49:43 PDT 2021


Factory CHIRP Radio Images (*.img) file attached: Retevis_RT76P.img

---------- Forwarded message ---------
From: Jim Unroe <kc9hi at comcast.net>
Date: Sun, May 30, 2021 at 10:44 PM
Subject: [PATCH] [RT76P] Add Retevis RT76P
To: <Rock.Unroe at gmail.com>


# HG changeset patch
# User Jim Unroe <rock.unroe at gmail.com>
# Date 1622428761 14400
#      Sun May 30 22:39:21 2021 -0400
# Node ID f8aa35624ba03d4c6d5480dcaec3aed10c1af897
# Parent  b62aebea801d5d8a4996046287d50e94a1483660
[RT76P] Add Retevis RT76P

This patch adds support for the Retevis RT76P GMRS handheld radio.

#8681

diff -r b62aebea801d -r f8aa35624ba0 chirp/drivers/retevis_rt76p.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/retevis_rt76p.py    Sun May 30 22:39:21 2021 -0400
@@ -0,0 +1,1043 @@
+# Copyright 2021 Jim Unroe <rock.unroe at 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 logging
+import os
+import struct
+import time
+
+from chirp import (
+    bitwise,
+    chirp_common,
+    directory,
+    errors,
+    memmap,
+    util,
+)
+from chirp.settings import (
+    RadioSetting,
+    RadioSettingGroup,
+    RadioSettings,
+    RadioSettingValueBoolean,
+    RadioSettingValueFloat,
+    RadioSettingValueInteger,
+    RadioSettingValueList,
+    RadioSettingValueString,
+)
+
+LOG = logging.getLogger(__name__)
+
+MEM_FORMAT = """
+#seekto 0x0000;
+struct {
+  lbcd rxfreq[4];     // 0-3
+  lbcd txfreq[4];     // 4-7
+  ul16 rxtone;        // 8-9
+  ul16 txtone;        // A-B
+  u8 unknown1:4,      // C
+     scode:4;         //     Signal
+  u8 unknown2:6,      // D
+     pttid:2;         //     PTT-ID
+  u8 unknown3:7,      // E
+     lowpower:1;      //     Power Level 0 = High, 1 = Low
+  u8 ani:1,           // F   ANI
+     narrow:1,        //     Bandwidth  0 = Wide, 1 = Narrow
+     unknown4:2,
+     bcl:1,           //     BCL
+     scan:1,          //     Scan  0 = Skip, 1 = Scan
+     unknown5:1,
+     compand:1;       //     Compand
+} memory[30];
+
+#seekto 0x0C00;
+struct {
+  char name[10];      // 10-character Alpha Tag
+  u8 unused[6];
+} names[30];
+
+#seekto 0x1A00;
+struct {
+  u8 unknown:4,       // 1A00
+     squelch:4;       //      Squelch Level
+  u8 unknown_1a01:7,  // 1A01
+     save:1;          //      Save Mode
+  u8 unknown_1a02:4,  // 1A04
+     vox:4;           // 1A02 VOX Level
+  u8 unknown_1a03:4,  // 1A03
+     abr:4;           //      Auto Backlight Time-out
+  u8 unknown_1a04:7,  // 1A04
+     tdr:1;           //      Dual Standby
+  u8 tot;             // 1A05 Time-out Timer
+  u8 unknown_1a06:7,  // 1A06
+     beep:1;          //      Beep
+  u8 unknown_1a07:7,  // 1A07
+     voice:1;         //      Voice Switch
+  u8 unknown_1a08:7,  // 1A08
+     language:1;      //      Language
+  u8 unknown_1a09:6,  // 1A09
+     dtmfst:2;        //      DTMF ST
+  u8 unknown_101a:6,  // 1A0A
+     scmode:2;        //      Scan Mode
+  u8 unknown_1a0a;    // 1A0B
+  u8 pttlt;           // 1A0C PTT Delay
+  u8 unknown_1a0d:6,  // 1A0D
+     mdfa:2;          //      Channle A Display
+  u8 unknown_1a0e:6,  // 1A0E
+     mdfb:2;          //      Channle B Display
+  u8 unknown_1a0f:7,  // 1A0F
+     bcl:1;           //      BCL
+  u8 unknown_1a10:7,  // 1A10
+     autolock:1;      //      AutoLock
+  u8 unknown_1a11:6,  // 1A11
+     almod:2;         //      Alarm Mode
+  u8 unknown_1a12:7,  // 1A12
+     alarm:1;         //      Alarm Sound
+  u8 unknown_1a13:6,  // 1A13
+     tdrab:2;         //      Tx Under TDR Start
+  u8 unknown_1a14:7,  // 1A14
+     ste:1;           //      Tail Noise Clear
+  u8 unknown_1a15:4,  // 1A15
+     rpste:4;         //      Pass Repet Noise
+  u8 unknown_1a16:4,  // 1A16
+     rptrl:4;         //      Pass Repet Noise
+  u8 unknown_1a17:7,  // 1A17
+     roger:1;         //      Roger
+  u8 unknown_1a18;    // 1A18
+  u8 unknown_1a19:7,  // 1A19
+     fmradio:1;       //      FM Radio (inverted)
+  u8 unknown_1a1a:7,  // 1A1A
+     workmode:1;      //      Work Mode
+  u8 unknown_1a1b:7,  // 1A1B
+     kblock:1;        //      KB_Lock
+  u8 unknown_1a1c:6,  // 1A1C
+     pwronmsg:2;      //      Pwr On Msg
+  u8 unknown_1a1d;    // 1A1D
+  u8 unknown_1a1e:6,  // 1A1E
+     tone:2;          //      Tone
+  u8 unknown_1a1f;    // 1A1F
+  u8 unknown_1a20[7]; // 1A20-1A26
+  u8 unknown_1a27:6,  // 1A27
+     wtled:2;         //      Wait Backlight Color
+  u8 unknown_1a28:6,  // 1A28
+     rxled:2;         //      Rx Backlight Color
+  u8 unknown_1a29:6,  // 1A29
+     txled:2;         //      Tx Backlight Color
+} settings;
+
+#seekto 0x1A80;
+struct {
+  u8 shortp;          // 1A80 Skey Short
+  u8 longp;           // 1A81 Skey Long
+} skey;
+
+#seekto 0x1B00;
+struct {
+  u8 code[6];         // 6-character PTT-ID Code
+  u8 unused[10];
+} pttid[15];
+
+#seekto 0x1BF0;
+struct {
+  u8 code[6];         // ANI Code
+  u8 unknown111;
+  u8 dtmfon;          // DTMF Speed (on time)
+  u8 dtmfoff;         // DTMR Speed (off time)
+  u8 unused222[7];
+  u8 killword[6];     // Kill Word
+  u8 unused333[2];
+  u8 revive[6];       // Revive
+  u8 unused444[2];
+} dtmf;
+
+#seekto 0x1FE0;
+struct {
+  char line1[16];     // Power-on Message Line 1
+  char line2[16];     // Power-on Message Line 2
+} poweron_msg;
+"""
+
+
+CMD_ACK = "\x06"
+
+RT76P_DTCS = sorted(chirp_common.DTCS_CODES + [645])
+
+DTMF_CHARS = "0123456789 *#ABCD"
+
+ALMOD_LIST = ["On Site", "Send Sound", "Send Code"]
+BACKLIGHT_LIST = ["Off", "Blue", "Orange", "Purple"]
+DTMFSPEED_LIST = ["%s ms" % x for x in range(50, 2010, 10)]
+DTMFST_LIST = ["Off", "KeyBboard Side Tone", "ANI Side Tone", "KB ST + ANI ST"]
+LANGUAGE_LIST = ["English", "China"]
+MDF_LIST = ["Name", "Frequency", "Number"]
+OFF1TO10_LIST = ["Off"] + ["%s" % x for x in range(1, 11)]
+PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
+PTTIDCODE_LIST = ["%s" % x for x in range(1, 16)]
+PWRONMSG_LIST = ["Picture", "Message", "Voltage"]
+RPSTE_LIST = ["Off"] + ["%s" % x for x in range(100, 1100, 100)]
+SCMODE_LIST = ["Time (TO)", "Carrier (CO)", "Search (SE)"]
+TDRAB_LIST = ["Off", "A Band", "B Band"]
+TIMEOUTTIMER_LIST = ["%s seconds" % x for x in range(15, 615, 15)]
+TONE_LIST = ["1000Hz", "1450Hz", "1750Hz", "2100Hz"]
+VOICE_LIST = ["Off", "On"]
+WORKMODE_LIST = ["VFO Mode", "Channel Mode"]
+
+SKEY_CHOICES = ["FM", "Tx Power", "Moni", "Scan", "Offline", "Weather"]
+SKEY_VALUES = [0x07, 0x0A, 0x05, 0x1C, 0x0B, 0x0C]
+
+
+SETTING_LISTS = {
+    "abr": OFF1TO10_LIST,
+    "almod": ALMOD_LIST,
+    "dtmfspeed": DTMFSPEED_LIST,
+    "language": LANGUAGE_LIST,
+    "mdfa": MDF_LIST,
+    "mdfb": MDF_LIST,
+    "pttid": PTTID_LIST,
+    "rpste": RPSTE_LIST,
+    "rptrl": RPSTE_LIST,
+    "rxled": BACKLIGHT_LIST,
+    "scode": PTTIDCODE_LIST,
+    "scmode": SCMODE_LIST,
+    "tdrab": TDRAB_LIST,
+    "tot": TIMEOUTTIMER_LIST,
+    "tone": TONE_LIST,
+    "txled": BACKLIGHT_LIST,
+    "voice": VOICE_LIST,
+    "vox": OFF1TO10_LIST,
+    "workmode": WORKMODE_LIST,
+    "wtled": BACKLIGHT_LIST,
+    }
+
+GMRS_FREQS1 = [462.5625, 462.5875, 462.6125, 462.6375, 462.6625,
+               462.6875, 462.7125]
+GMRS_FREQS2 = [467.5625, 467.5875, 467.6125, 467.6375, 467.6625,
+               467.6875, 467.7125]
+GMRS_FREQS3 = [462.5500, 462.5750, 462.6000, 462.6250, 462.6500,
+               462.6750, 462.7000, 462.7250]
+GMRS_FREQS = GMRS_FREQS1 + GMRS_FREQS2 + GMRS_FREQS3 * 2
+
+
+def _rt76p_enter_programming_mode(radio):
+    serial = radio.pipe
+
+    exito = False
+    for i in range(0, 5):
+        serial.write(radio._magic)
+        ack = serial.read(1)
+
+        try:
+            if ack == CMD_ACK:
+                exito = True
+                break
+        except:
+            LOG.debug("Attempt #%s, failed, trying again" % i)
+            pass
+
+    # check if we had EXITO
+    if exito is False:
+        msg = "The radio did not accept program mode after five tries.\n"
+        msg += "Check you interface cable and power cycle your radio."
+        raise errors.RadioError(msg)
+
+    try:
+        serial.write("F")
+        ident = serial.read(8)
+    except:
+        raise errors.RadioError("Error communicating with radio")
+
+    if not ident == radio._fingerprint:
+        LOG.debug(util.hexprint(ident))
+        raise errors.RadioError("Radio returned unknown identification string")
+
+
+def _rt76p_exit_programming_mode(radio):
+    serial = radio.pipe
+    try:
+        serial.write("E")
+    except:
+        raise errors.RadioError("Radio refused to exit programming mode")
+
+
+def _rt76p_read_block(radio, block_addr, block_size):
+    serial = radio.pipe
+
+    cmd = struct.pack(">cHb", 'R', block_addr, block_size)
+    expectedresponse = "R" + cmd[1:]
+    LOG.debug("Reading block %04x..." % (block_addr))
+
+    try:
+        serial.write(cmd)
+        response = serial.read(4 + block_size)
+        if response[:4] != expectedresponse:
+            raise Exception("Error reading block %04x." % (block_addr))
+
+        block_data = response[4:]
+    except:
+        raise errors.RadioError("Failed to read block at %04x" % block_addr)
+
+    return block_data
+
+
+def _rt76p_write_block(radio, block_addr, block_size):
+    serial = radio.pipe
+
+    cmd = struct.pack(">cHb", 'W', block_addr, block_size)
+    data = radio.get_mmap()[block_addr:block_addr + block_size]
+
+    LOG.debug("Writing Data:")
+    LOG.debug(util.hexprint(cmd + data))
+
+    try:
+        serial.write(cmd + data)
+        if serial.read(1) != CMD_ACK:
+            raise Exception("No ACK")
+    except:
+        raise errors.RadioError("Failed to send block "
+                                "to radio at %04x" % block_addr)
+
+
+def do_download(radio):
+    serial = radio.pipe
+    LOG.debug("download")
+    _rt76p_enter_programming_mode(radio)
+
+    data = ""
+
+    status = chirp_common.Status()
+    status.msg = "Cloning from radio"
+
+    status.cur = 0
+    status.max = radio._memsize
+
+    for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
+        status.cur = addr + radio.BLOCK_SIZE
+        radio.status_fn(status)
+
+        block = _rt76p_read_block(radio, addr, radio.BLOCK_SIZE)
+        data += block
+
+        LOG.debug("Address: %04x" % addr)
+        LOG.debug(util.hexprint(block))
+
+    _rt76p_exit_programming_mode(radio)
+
+    return memmap.MemoryMap(data)
+
+
+def do_upload(radio):
+    status = chirp_common.Status()
+    status.msg = "Uploading to radio"
+
+    _rt76p_enter_programming_mode(radio)
+
+    status.cur = 0
+    status.max = radio._memsize
+
+    for start_addr, end_addr in radio._ranges:
+        for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
+            status.cur = addr + radio.BLOCK_SIZE_UP
+            radio.status_fn(status)
+            _rt76p_write_block(radio, addr, radio.BLOCK_SIZE_UP)
+
+    _rt76p_exit_programming_mode(radio)
+
+
+ at directory.register
+class RT76PRadio(chirp_common.CloneModeRadio):
+    """RETEVIS RT76P"""
+    VENDOR = "Retevis"
+    MODEL = "RT76P"
+    BAUD_RATE = 9600
+    BLOCK_SIZE = 0x40
+    BLOCK_SIZE_UP = 0x20
+
+    RT76P_POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
+                          chirp_common.PowerLevel("Low", watts=0.50)]
+
+    _magic = "PROGROMCD2U"
+    _fingerprint = "\x01\x36\x01\x74\x04\x00\x05\x20"
+
+    _ranges = [
+               (0x0000, 0x0820),
+               (0x0C00, 0x1400),
+               (0x1A00, 0x1E00),
+               (0x1FE0, 0x2000),
+              ]
+    _memsize = 0x2000
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_bank = False
+        rf.has_ctone = True
+        rf.has_cross = True
+        rf.has_rx_dtcs = True
+        rf.has_tuning_step = False
+        rf.can_odd_split = True
+        rf.has_name = True
+        rf.valid_name_length = 10
+        rf.valid_skips = ["", "S"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
+                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
+        rf.valid_power_levels = self.RT76P_POWER_LEVELS
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.valid_modes = ["FM", "NFM"]  # 25 kHz, 12.5 KHz.
+        rf.valid_dtcs_codes = RT76P_DTCS
+        rf.memory_bounds = (1, 30)
+        rf.valid_tuning_steps = [2.5, 5., 6.25, 10., 12.5, 20., 25., 50.]
+        rf.valid_bands = [(400000000, 480000000)]
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def validate_memory(self, mem):
+        msgs = ""
+        msgs = chirp_common.CloneModeRadio.validate_memory(self, mem)
+
+        _msg_freq = 'Memory location cannot change frequency'
+        _msg_simplex = 'Memory location only supports Duplex:(None)'
+        _msg_duplex = 'Memory location only supports Duplex: +'
+        _msg_offset = 'Memory location only supports Offset: 5.000000'
+        _msg_nfm = 'Memory location only supports Mode: NFM'
+        _msg_txp = 'Memory location only supports Power: Low'
+
+        # GMRS models
+        # range of memories with values set by FCC rules
+        if mem.freq != int(GMRS_FREQS[mem.number - 1] * 1000000):
+            # warn user can't change frequency
+            msgs.append(chirp_common.ValidationError(_msg_freq))
+
+        # channels 1 - 22 are simplex only
+        if mem.number <= 22:
+            if str(mem.duplex) != "":
+                # warn user can't change duplex
+                msgs.append(chirp_common.ValidationError(_msg_simplex))
+
+        # channels 23 - 30 are +5 MHz duplex only
+        if mem.number >= 23:
+            if str(mem.duplex) != "+":
+                # warn user can't change duplex
+                msgs.append(chirp_common.ValidationError(_msg_duplex))
+
+            if str(mem.offset) != "5000000":
+                # warn user can't change offset
+                msgs.append(chirp_common.ValidationError(_msg_offset))
+
+        # channels 8 - 14 are low power NFM only
+        if mem.number >= 8 and mem.number <= 14:
+            if mem.mode != "NFM":
+                # warn user can't change mode
+                msgs.append(chirp_common.ValidationError(_msg_nfm))
+
+            if mem.power != "Low":
+                # warn user can't change power
+                msgs.append(chirp_common.ValidationError(_msg_txp))
+
+        return msgs
+
+    def sync_in(self):
+        """Download from radio"""
+        try:
+            data = do_download(self)
+        except errors.RadioError:
+            # Pass through any real errors we raise
+            raise
+        except:
+            # 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 = data
+        self.process_mmap()
+
+    def sync_out(self):
+        """Upload to radio"""
+        try:
+            do_upload(self)
+        except:
+            # 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 get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+        _nam = self._memobj.names[number - 1]
+
+        mem = chirp_common.Memory()
+
+        mem.number = number
+        mem.freq = int(_mem.rxfreq) * 10
+
+        # We'll consider any blank (i.e. 0MHz frequency) to be empty
+        if mem.freq == 0:
+            mem.empty = True
+            return mem
+
+        if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF":
+            mem.freq = 0
+            mem.empty = True
+            return mem
+
+        if _mem.get_raw() == ("\xFF" * 16):
+            LOG.debug("Initializing empty memory")
+            _mem.set_raw("\x00" * 13 + "\x30\x8F\xF8")
+
+        if 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
+
+        for char in _nam.name:
+            if str(char) == "\xFF":
+                char = " "  # may have 0xFF mid-name
+            mem.name += str(char)
+        mem.name = mem.name.rstrip()
+
+        dtcs_pol = ["N", "N"]
+
+        if _mem.txtone in [0, 0xFFFF]:
+            txmode = ""
+        elif _mem.txtone >= 0x0258:
+            txmode = "Tone"
+            mem.rtone = int(_mem.txtone) / 10.0
+        elif _mem.txtone <= 0x0258:
+            txmode = "DTCS"
+            if _mem.txtone > 0x69:
+                index = _mem.txtone - 0x6A
+                dtcs_pol[0] = "R"
+            else:
+                index = _mem.txtone - 1
+            mem.dtcs = RT76P_DTCS[index]
+        else:
+            LOG.warn("Bug: txtone is %04x" % _mem.txtone)
+
+        if _mem.rxtone in [0, 0xFFFF]:
+            rxmode = ""
+        elif _mem.rxtone >= 0x0258:
+            rxmode = "Tone"
+            mem.ctone = int(_mem.rxtone) / 10.0
+        elif _mem.rxtone <= 0x0258:
+            rxmode = "DTCS"
+            if _mem.rxtone >= 0x6A:
+                index = _mem.rxtone - 0x6A
+                dtcs_pol[1] = "R"
+            else:
+                index = _mem.rxtone - 1
+            mem.rx_dtcs = RT76P_DTCS[index]
+        else:
+            LOG.warn("Bug: rxtone is %04x" % _mem.rxtone)
+
+        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)
+
+        mem.dtcs_polarity = "".join(dtcs_pol)
+
+        if not _mem.scan:
+            mem.skip = "S"
+
+        mem.power = self.RT76P_POWER_LEVELS[_mem.lowpower]
+
+        mem.mode = _mem.narrow and "NFM" or "FM"
+
+        mem.extra = RadioSettingGroup("Extra", "extra")
+
+        # BCL (Busy Channel Lockout)
+        rs = RadioSettingValueBoolean(_mem.bcl)
+        rset = RadioSetting("bcl", "BCL", rs)
+        mem.extra.append(rset)
+
+        # PTT-ID
+        rs = RadioSettingValueList(PTTID_LIST, PTTID_LIST[_mem.pttid])
+        rset = RadioSetting("pttid", "PTT ID", rs)
+        mem.extra.append(rset)
+
+        # Signal (DTMF Encoder Group #)
+        rs = RadioSettingValueList(PTTIDCODE_LIST, PTTIDCODE_LIST[_mem.scode])
+        rset = RadioSetting("scode", "PTT ID Code", rs)
+        mem.extra.append(rset)
+
+        # Compand
+        rs = RadioSettingValueBoolean(_mem.compand)
+        rset = RadioSetting("compand", "Compand", rs)
+        mem.extra.append(rset)
+
+        # ANI
+        rs = RadioSettingValueBoolean(_mem.ani)
+        rset = RadioSetting("ani", "ANI", rs)
+        mem.extra.append(rset)
+
+        return mem
+
+    def set_memory(self, mem):
+        _mem = self._memobj.memory[mem.number - 1]
+        _nam = self._memobj.names[mem.number - 1]
+
+        if mem.empty:
+            _mem.set_raw("\xff" * 8 + "\x00" * 8)
+
+            GMRS_FREQ = int(GMRS_FREQS[mem.number - 1] * 100000)
+            _mem.narrow = True
+            _mem.scan = True
+            if mem.number > 22:
+                _mem.rxfreq = GMRS_FREQ
+                _mem.txfreq = int(_mem.rxfreq) + 500000
+            else:
+                _mem.rxfreq = _mem.txfreq = GMRS_FREQ
+            if mem.number >= 8 and mem.number <= 14:
+                _mem.lowpower = True
+
+            _nam.set_raw("\xff" * 16)
+            return
+
+        _mem.set_raw("\x00" * 16)
+
+        _mem.rxfreq = mem.freq / 10
+
+        if mem.duplex == "off":
+            for i in range(0, 4):
+                _mem.txfreq[i].set_raw("\xFF")
+        elif mem.duplex == "split":
+            _mem.txfreq = mem.offset / 10
+        elif mem.duplex == "+":
+            _mem.txfreq = (mem.freq + mem.offset) / 10
+        elif mem.duplex == "-":
+            _mem.txfreq = (mem.freq - mem.offset) / 10
+        else:
+            _mem.txfreq = mem.freq / 10
+
+        _namelength = self.get_features().valid_name_length
+        for i in range(_namelength):
+            try:
+                _nam.name[i] = mem.name[i]
+            except IndexError:
+                _nam.name[i] = "\xFF"
+
+        rxmode = txmode = ""
+        if mem.tmode == "Tone":
+            _mem.txtone = int(mem.rtone * 10)
+            _mem.rxtone = 0
+        elif mem.tmode == "TSQL":
+            _mem.txtone = int(mem.ctone * 10)
+            _mem.rxtone = int(mem.ctone * 10)
+        elif mem.tmode == "DTCS":
+            rxmode = txmode = "DTCS"
+            _mem.txtone = RT76P_DTCS.index(mem.dtcs) + 1
+            _mem.rxtone = RT76P_DTCS.index(mem.dtcs) + 1
+        elif mem.tmode == "Cross":
+            txmode, rxmode = mem.cross_mode.split("->", 1)
+            if txmode == "Tone":
+                _mem.txtone = int(mem.rtone * 10)
+            elif txmode == "DTCS":
+                _mem.txtone = RT76P_DTCS.index(mem.dtcs) + 1
+            else:
+                _mem.txtone = 0
+            if rxmode == "Tone":
+                _mem.rxtone = int(mem.ctone * 10)
+            elif rxmode == "DTCS":
+                _mem.rxtone = RT76P_DTCS.index(mem.rx_dtcs) + 1
+            else:
+                _mem.rxtone = 0
+        else:
+            _mem.rxtone = 0
+            _mem.txtone = 0
+
+        if txmode == "DTCS" and mem.dtcs_polarity[0] == "R":
+            _mem.txtone += 0x69
+        if rxmode == "DTCS" and mem.dtcs_polarity[1] == "R":
+            _mem.rxtone += 0x69
+
+        _mem.scan = mem.skip != "S"
+        _mem.narrow = mem.mode == "NFM"
+
+        _mem.lowpower = mem.power == self.RT76P_POWER_LEVELS[1]
+
+        for setting in mem.extra:
+            if setting.get_name() == "scramble_type":
+                setattr(_mem, setting.get_name(), int(setting.value) + 8)
+                setattr(_mem, "scramble_type2", int(setting.value) + 8)
+            else:
+                setattr(_mem, setting.get_name(), setting.value)
+
+    def get_settings(self):
+        _dtmf = self._memobj.dtmf
+        _settings = self._memobj.settings
+        _skey = self._memobj.skey
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        group = RadioSettings(basic)
+
+        # Menu 00: Squelch (Squelch Level)
+        rs = RadioSettingValueInteger(0, 9, _settings.squelch)
+        rset = RadioSetting("squelch", "Squelch Level", rs)
+        basic.append(rset)
+
+        # Menu 01: Step (VFO setting)
+        # Menu 02: Tx Power (VFO setting - not available)
+
+        # Menu 03: Power save (Save Mode)
+        rs = RadioSettingValueBoolean(_settings.save)
+        rset = RadioSetting("save", "Power Save", rs)
+        basic.append(rset)
+
+        # Menu 04: Vox Level (VOX)
+        rs = RadioSettingValueList(OFF1TO10_LIST, OFF1TO10_LIST[_settings.vox])
+        rset = RadioSetting("vox", "VOX Level", rs)
+        basic.append(rset)
+
+        # Menu 05: Bandwidth
+
+        # Menu 06: Backlight (Auto Backlight)
+        rs = RadioSettingValueList(OFF1TO10_LIST,
+                                   OFF1TO10_LIST[_settings.abr])
+        rset = RadioSetting("abr", "Backlight Time-out", rs)
+        basic.append(rset)
+
+        # Menu 07: Dual Standby (TDR)
+        rs = RadioSettingValueBoolean(_settings.tdr)
+        rset = RadioSetting("tdr", "Dual Standby", rs)
+        basic.append(rset)
+
+        # Menu 08: Beep Prompt
+        rs = RadioSettingValueBoolean(_settings.beep)
+        rset = RadioSetting("beep", "Beep Prompt", rs)
+        basic.append(rset)
+
+        # Menu 09: Voice (Voice Switch)
+        rs = RadioSettingValueBoolean(_settings.voice)
+        rset = RadioSetting("voice", "Voice Prompts", rs)
+        basic.append(rset)
+
+        # Menu 10: Tx over time (Time Out)
+        rs = RadioSettingValueList(TIMEOUTTIMER_LIST,
+                                   TIMEOUTTIMER_LIST[_settings.tot - 1])
+        rset = RadioSetting("tot", "Time-out Timer", rs)
+        basic.append(rset)
+
+        # Menu 11: Rx DCS
+        # Menu 12: Rx CTCSS
+        # Menu 13: Tx DCS
+        # Menu 14: Tx CTCSS
+        # Menu 15: Voice Compand
+
+        # Menu 16: DTMFST (DTMF ST)
+        rs = RadioSettingValueList(DTMFST_LIST, DTMFST_LIST[_settings.dtmfst])
+        rset = RadioSetting("dtmfst", "DTMF Side Tone", rs)
+        basic.append(rset)
+
+        # Mneu 17: R-TONE (Tone)
+        rs = RadioSettingValueList(TONE_LIST, TONE_LIST[_settings.tone])
+        rset = RadioSetting("tone", "Tone-burst Frequency", rs)
+        basic.append(rset)
+
+        # Menu 18: S-CODE
+
+        # Menu 19: Scan Mode
+        rs = RadioSettingValueList(SCMODE_LIST, SCMODE_LIST[_settings.scmode])
+        rset = RadioSetting("scmode", "Scan Resume Method", rs)
+        basic.append(rset)
+
+        # Menu 20: ANI Match
+        # Menu 21: PTT-ID
+
+        # Menu 22: MDF-A (Channle_A Display)
+        rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdfa])
+        rset = RadioSetting("mdfa", "Memory Display Format A", rs)
+        basic.append(rset)
+
+        # Menu 23: MDF-B (Channle_B Display)
+        rs = RadioSettingValueList(MDF_LIST, MDF_LIST[_settings.mdfb])
+        rset = RadioSetting("mdfb", "Memory Display Format B", rs)
+        basic.append(rset)
+
+        # Menu 24: Busy Lockout
+
+        # Menu 25: Key Auto Lock (AutoLock)
+        rs = RadioSettingValueBoolean(_settings.autolock)
+        rset = RadioSetting("autolock", "Keypad Auto Lock", rs)
+        basic.append(rset)
+
+        # Menu 26: WT-LED (Wait Backlight)
+        rs = RadioSettingValueList(BACKLIGHT_LIST,
+                                   BACKLIGHT_LIST[_settings.wtled])
+        rset = RadioSetting("wtled", "Wait Backlight Color", rs)
+        basic.append(rset)
+
+        # Menu 27: RX-LED (Rx Backlight)
+        rs = RadioSettingValueList(BACKLIGHT_LIST,
+                                   BACKLIGHT_LIST[_settings.rxled])
+        rset = RadioSetting("rxled", "RX Backlight Color", rs)
+        basic.append(rset)
+
+        # Menu 28: TX-LED (Tx Backlight)
+        rs = RadioSettingValueList(BACKLIGHT_LIST,
+                                   BACKLIGHT_LIST[_settings.txled])
+        rset = RadioSetting("txled", "TX Backlight Color", rs)
+        basic.append(rset)
+
+        # Menu 29: Alarm Mode
+        rs = RadioSettingValueList(ALMOD_LIST, ALMOD_LIST[_settings.almod])
+        rset = RadioSetting("almod", "Alarm Mode", rs)
+        basic.append(rset)
+
+        # Menu 30: TAIL (Tail Noise Clear)
+        rs = RadioSettingValueBoolean(_settings.ste)
+        rset = RadioSetting("ste", "Squelch Tail Eliminate", rs)
+        basic.append(rset)
+
+        # Menu 31: PROGRE (Roger)
+        rs = RadioSettingValueBoolean(_settings.roger)
+        rset = RadioSetting("roger", "Roger Beep", rs)
+        basic.append(rset)
+
+        # Menu 32: Language
+        rs = RadioSettingValueList(LANGUAGE_LIST,
+                                   LANGUAGE_LIST[_settings.language])
+        rset = RadioSetting("language", "Language", rs)
+        basic.append(rset)
+
+        # Menu 33: OPENMGS (Pwr On Msg)
+        rs = RadioSettingValueList(PWRONMSG_LIST,
+                                   PWRONMSG_LIST[_settings.pwronmsg])
+        rset = RadioSetting("pwronmsg", "Power On Message", rs)
+        basic.append(rset)
+
+        dtmfchars = "0123456789ABCD*#"
+
+        # Menu 34: ANI ID (display only)
+        _codeobj = self._memobj.dtmf.code
+        print "_codeobj"
+        print _codeobj
+        _code = "".join([dtmfchars[x] for x in _codeobj if int(x) < 0x1F])
+        val = RadioSettingValueString(0, 6, _code, False)
+        val.set_charset(dtmfchars)
+        val.set_mutable(False)
+        rs = RadioSetting("dtmf.code", "Query ANI ID", val)
+        basic.append(rs)
+
+        # Menu 35: Reset
+
+        advanced = RadioSettingGroup("advanced", "Advanced Settings")
+        group.append(advanced)
+
+        # Work Mode
+        rs = RadioSettingValueList(WORKMODE_LIST,
+                                   WORKMODE_LIST[_settings.workmode])
+        rset = RadioSetting("workmode", "Work Mode", rs)
+        advanced.append(rset)
+
+        # PTT Delay
+        rs = RadioSettingValueInteger(0, 30, _settings.pttlt)
+        rset = RadioSetting("pttlt", "PTT ID Delay", rs)
+        advanced.append(rset)
+
+        def apply_skey_listvalue(setting, obj):
+            LOG.debug("Setting value: " + str(setting.value) + " from list")
+            val = str(setting.value)
+            index = SKEY_CHOICES.index(val)
+            val = SKEY_VALUES[index]
+            obj.set_value(val)
+
+        # Skey Short
+        if _skey.shortp in SKEY_VALUES:
+            idx = SKEY_VALUES.index(_skey.shortp)
+        else:
+            idx = SKEY_VALUES.index(0x0C)
+        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
+        rset = RadioSetting("skey.shortp", "Side Key (Short Press)", rs)
+        rset.set_apply_callback(apply_skey_listvalue, _skey.shortp)
+        advanced.append(rset)
+
+        # Skey Long
+        if _skey.longp in SKEY_VALUES:
+            idx = SKEY_VALUES.index(_skey.longp)
+        else:
+            idx = SKEY_VALUES.index(0x0C)
+        rs = RadioSettingValueList(SKEY_CHOICES, SKEY_CHOICES[idx])
+        rset = RadioSetting("skey.longp", "Side Key (Long Press)", rs)
+        rset.set_apply_callback(apply_skey_listvalue, _skey.longp)
+        advanced.append(rset)
+
+        # Pass Repet Noise
+        rs = RadioSettingValueList(RPSTE_LIST, RPSTE_LIST[_settings.rpste])
+        rset = RadioSetting("rpste", "Squelch Tail Eliminate (repeater)", rs)
+        advanced.append(rset)
+
+        # Pass Repet Noise
+        rs = RadioSettingValueList(RPSTE_LIST, RPSTE_LIST[_settings.rptrl])
+        rset = RadioSetting("rptrl", "STE Repeater Delay", rs)
+        advanced.append(rset)
+
+        # KB_Lock
+        rs = RadioSettingValueBoolean(_settings.kblock)
+        rset = RadioSetting("kblock", "Keypad Lock", rs)
+        advanced.append(rset)
+
+        # FM Radio Enable
+        rs = RadioSettingValueBoolean(not _settings.fmradio)
+        rset = RadioSetting("fmradio", "Broadcast FM Radio", rs)
+        advanced.append(rset)
+
+        # Alarm Sound
+        rs = RadioSettingValueBoolean(_settings.alarm)
+        rset = RadioSetting("alarm", "Alarm Sound", rs)
+        advanced.append(rset)
+
+        # Tx Under TDR Start
+        rs = RadioSettingValueList(TDRAB_LIST, TDRAB_LIST[_settings.tdrab])
+        rset = RadioSetting("tdrab", "Dual Standby TX Priority", rs)
+        advanced.append(rset)
+
+        def _filter(name):
+            filtered = ""
+            for char in str(name):
+                if char in chirp_common.CHARSET_ASCII:
+                    filtered += char
+                else:
+                    filtered += " "
+            return filtered
+
+        _msg = self._memobj.poweron_msg
+        rs = RadioSetting("poweron_msg.line1", "Power-On Message 1",
+                          RadioSettingValueString(
+                              0, 16, _filter(_msg.line1)))
+        advanced.append(rs)
+        rs = RadioSetting("poweron_msg.line2", "Power-On Message 2",
+                          RadioSettingValueString(
+                              0, 16, _filter(_msg.line2)))
+        advanced.append(rs)
+
+        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
+        group.append(dtmf)
+
+        def apply_code(setting, obj, length):
+            code = []
+            for j in range(0, length):
+                try:
+                    code.append(DTMF_CHARS.index(str(setting.value)[j]))
+                except IndexError:
+                    code.append(0xFF)
+            obj.code = code
+
+        for i in range(0, 15):
+            _codeobj = self._memobj.pttid[i].code
+            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
+            val = RadioSettingValueString(0, 6, _code, False)
+            val.set_charset(DTMF_CHARS)
+            pttid = RadioSetting("pttid/%i.code" % i,
+                                 "Signal Code %i" % (i + 1), val)
+            pttid.set_apply_callback(apply_code, self._memobj.pttid[i], 6)
+            dtmf.append(pttid)
+
+        _codeobj = self._memobj.dtmf.killword
+        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
+        val = RadioSettingValueString(0, 6, _code, False)
+        val.set_charset(DTMF_CHARS)
+        rs = RadioSetting("dtmf.killword", "Kill Word", val)
+        rs.set_apply_callback(apply_code, self._memobj.dtmf, 6)
+        dtmf.append(rs)
+
+        _codeobj = self._memobj.dtmf.revive
+        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
+        val = RadioSettingValueString(0, 6, _code, False)
+        val.set_charset(DTMF_CHARS)
+        rs = RadioSetting("dtmf.revive", "Revive Word", val)
+        rs.set_apply_callback(apply_code, self._memobj.dtmf, 6)
+        dtmf.append(rs)
+
+        _codeobj = self._memobj.dtmf.code
+        _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
+        val = RadioSettingValueString(0, 6, _code, False)
+        val.set_charset(DTMF_CHARS)
+        rs = RadioSetting("dtmf.code", "ANI Code", val)
+        rs.set_apply_callback(apply_code, self._memobj.dtmf, 6)
+        dtmf.append(rs)
+
+        if _dtmf.dtmfon > 0xC3:
+            val = 0x00
+        else:
+            val = _dtmf.dtmfon
+        rs = RadioSetting("dtmf.dtmfon", "DTMF Speed (on)",
+                          RadioSettingValueList(DTMFSPEED_LIST,
+                                                DTMFSPEED_LIST[val]))
+        dtmf.append(rs)
+
+        if _dtmf.dtmfoff > 0xC3:
+            val = 0x00
+        else:
+            val = _dtmf.dtmfoff
+        rs = RadioSetting("dtmf.dtmfoff", "DTMF Speed (off)",
+                          RadioSettingValueList(DTMFSPEED_LIST,
+                                                DTMFSPEED_LIST[val]))
+        dtmf.append(rs)
+
+        return group
+
+    def set_settings(self, settings):
+        _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 == "fmradio":
+                        setattr(obj, setting, not int(element.value))
+                    elif setting == "tot":
+                        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
+
+    @classmethod
+    def match_model(cls, filedata, filename):
+        # This radio has always been post-metadata, so never do
+        # old-school detection
+        return False
diff -r b62aebea801d -r f8aa35624ba0 tools/cpep8.manifest
--- a/tools/cpep8.manifest      Fri May 28 14:25:48 2021 -0400
+++ b/tools/cpep8.manifest      Sun May 30 22:39:21 2021 -0400
@@ -82,6 +82,7 @@
 ./chirp/drivers/retevis_rt22.py
 ./chirp/drivers/retevis_rt23.py
 ./chirp/drivers/retevis_rt26.py
+./chirp/drivers/retevis_rt76p.py
 ./chirp/drivers/rfinder.py
 ./chirp/drivers/tdxone_tdq8a.py
 ./chirp/drivers/template.py
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Retevis_RT76P.img
Type: application/octet-stream
Size: 8349 bytes
Desc: not available
Url : http://intrepid.danplanet.com/pipermail/chirp_devel/attachments/20210530/e9714b27/attachment-0001.img 


More information about the chirp_devel mailing list