CHIRP Radio Images (*.img) file for testing attached.

Jim KC9HI

---------- Forwarded message ---------
From: Jim Unroe <kc9hi@comcast.net>
Date: Sat, Jul 3, 2021 at 2:25 PM
Subject: [PATCH] [RT98] Add support for Retevis RT98 Single Band Mini Mobile Radios
To: <Rock.Unroe@gmail.com>


# HG changeset patch
# User Jim Unroe <rock.unroe@gmail.com>
# Date 1625336015 14400
#      Sat Jul 03 14:13:35 2021 -0400
# Node ID 0eab8146b294ef686ca4a49c17ea38abde54c7ab
# Parent  f586574bc8786fd6bef1e5d54d08d381c81edd47
[RT98] Add support for Retevis RT98 Single Band Mini Mobile Radios

This patch adds support for the various Retevis RT98 mobile radio models/modes.
VHF FreeNet
VHF COM
VHF COMII
UHF PMR
UHF COM
UHF COMII

#9181

diff -r f586574bc878 -r 0eab8146b294 chirp/drivers/retevis_rt98.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/retevis_rt98.py     Sat Jul 03 14:13:35 2021 -0400
@@ -0,0 +1,1364 @@
+# Copyright 2021 Jim Unroe <rock.unroe@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import struct
+import time
+import logging
+
+from chirp import bitwise
+from chirp import chirp_common
+from chirp import directory
+from chirp import errors
+from chirp import memmap
+from chirp import util
+from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \
+    RadioSettingValueList, RadioSettingValueString, RadioSettingValueBoolean, \
+    RadioSettingValueInteger, RadioSettingValueString, \
+    RadioSettingValueFloat, InvalidValueError
+
+LOG = logging.getLogger(__name__)
+
+#
+#  Chirp Driver for Retevis RT98 models: RT98V (136-174 Mhz)
+#                                        RT98U (400-490 Mhz)
+#
+#
+#
+# Global Parameters
+#
+TONES = [62.5] + list(chirp_common.TONES)
+TMODES = ['', 'Tone', 'DTCS']
+DUPLEXES = ['', '+', '-']
+
+TXPOWER_LOW = 0x00
+TXPOWER_MED = 0x01
+TXPOWER_HIGH = 0x02
+
+DUPLEX_NOSPLIT = 0x00
+DUPLEX_POSSPLIT = 0x01
+DUPLEX_NEGSPLIT = 0x02
+
+CHANNEL_WIDTH_12d5kHz = 0x00
+CHANNEL_WIDTH_20kHz = 0x01
+CHANNEL_WIDTH_25kHz = 0x02
+
+TUNING_STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 30.0, 50.0]
+
+POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
+                chirp_common.PowerLevel("Mid", watts=10),
+                chirp_common.PowerLevel("High", watts=15)]
+
+PMR_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.5), ]
+
+FREENET_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), ]
+
+PMR_FREQS = [446.00625, 446.01875, 446.03125, 446.04375,
+             446.05625, 446.06875, 446.08125, 446.09375,
+             446.10625, 446.11875, 446.13125, 446.14375,
+             446.15625, 446.16875, 446.18125, 446.19375]
+
+FREENET_FREQS = [149.02500, 149.03750, 149.05000,
+                 149.08750, 149.10000, 149.11250]
+
+CROSS_MODES = ["Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS", "DTCS->Tone",
+               "->Tone", "DTCS->DTCS"]
+
+LIST_STEP = [str(x) for x in TUNING_STEPS]
+LIST_TIMEOUT = ["Off"] + ["%s min" % x for x in range(1, 31)]
+LIST_APO = ["Off", "30 min", "1 hr", "2 hrs"]
+LIST_SQUELCH = ["Off"] + ["Level %s" % x for x in range(1, 10)]
+LIST_DISPLAY_MODE = ["Channel", "Frequency", "Name"]
+LIST_AOP = ["Manual", "Auto"]
+LIST_STE_TYPE = ["Off", "Silent", "120 Degree", "180 Degree", "240 Degree"]
+LIST_STE_FREQ = ["Off", "55.2 Hz", "259.2 Hz"]
+
+LIST_PRIORITY_CH = ["Off", "Priority Channel 1", "Priority Channel 2",
+                    "Priority Channel 1 + Priority Channel 2"]
+
+LIST_REVERT_CH = ["Selected", "Selected + TalkBack", "Priority Channel 1",
+                  "Priority Channel 2", "Last Called", "Last Used",
+                  "Priority Channel 1 + TalkBack",
+                  "Priority Channel 2 + TalkBack"]
+
+LIST_TIME50 = ["0.1", "0.2", "0.3", "0.4", "0.5",
+               "0.6", "0.7", "0.8", "0.9", "1.0",
+               "1.1", "1.2", "1.3", "1.4", "1.5",
+               "1.6", "1.7", "1.8", "1.9", "2.0",
+               "2.1", "3.2", "2.3", "2.4", "2.5",
+               "2.6", "2.7", "2.8", "2.9", "3.0",
+               "3.1", "3.2", "3.3", "3.4", "3.5",
+               "3.6", "3.7", "3.8", "3.9", "4.0",
+               "4.1", "4.2", "4.3", "4.4", "4.5",
+               "4.6", "4.7", "4.8", "4.9", "5.0"]
+LIST_TIME46 = LIST_TIME50[4:]
+
+LIST_RT98V_MODES = ["FreeNet", "COM", "COMII"]
+LIST_RT98U_MODES = ["PMR", "COM", "COMII"]
+
+LIST_RT98V_FREQS = ["Rx(149 - 149.2 MHz) Tx(149 - 149.2 MHz)",
+                    "Rx(136 - 174 MHz) Tx(136 - 174 MHz)",
+                    "Rx(147 - 174 MHz) Tx(147 - 174 MHz)"]
+
+LIST_RT98U_FREQS = ["Rx(446 - 446.2 MHz) Tx(446 - 446.2 MHz)",
+                    "Rx(400 - 470 MHz) Tx(400 - 470 MHz)",
+                    "Rx(450 - 470 MHz) Tx(450 - 470 MHz)"]
+
+SETTING_LISTS = {
+        "tuning_step": LIST_STEP,
+        "timeout_timer": LIST_TIMEOUT,
+        "auto_power_off": LIST_APO,
+        "squelch": LIST_SQUELCH,
+        "display_mode": LIST_DISPLAY_MODE,
+        "auto_power_on": LIST_AOP,
+        "ste_type": LIST_STE_TYPE,
+        "ste_frequency": LIST_STE_FREQ,
+        "priority_ch": LIST_PRIORITY_CH,
+        "revert_ch": LIST_REVERT_CH,
+        "settings2.dropout_delay_time": LIST_TIME50,
+        "settings2.dwell_time": LIST_TIME50,
+        "settings2.look_back_time_a": LIST_TIME46,
+        "settings2.look_back_time_b": LIST_TIME46
+}
+
+#  RT98  memory map
+#  section: 1  Channel Bank
+#         description of channel bank (199 channels , range 1-199)
+#         Each 32 Byte (0x20 hex)  record:
+#  bytes:bit  type                 description
+#  ---------------------------------------------------------------------------
+#  4          bbcd freq[4]         receive frequency in packed binary coded
+#                                  decimal
+#  4          bbcd offset[4]       transceive offset in packed binary coded
+#                                  decimal (note: +/- direction set by
+#                                  'duplex' field)
+#  1          u8 unknown0
+#  1          u8
+#   :1        reverse:1            reverse flag, 0=off, 1=on (reverses
+#                                  transmit and receive freqencies)
+#   :1        txoff:1              transmitt off flag, 0=transmit, 1=do not
+#                                  transmit
+#   :2        power:2              transmit power setting, value range 0-2,
+#                                  0=low, 1=middle, 2=high
+#   :2        duplex:2             duplex settings, 0=simplex, 1=plus (+)
+#                                  offset, 2=minus(-) offset (see offset field)
+#   :2        channel_width:2      channel spacing, 0=12.5kHz, 1=20kHz, 2=25kHz
+#  1          u8
+#   :2        unknown1:2
+#   :1        talkaround:1         talkaround flag, 0=off, 1=on
+#                                  (bypasses repeater)
+#   :1        squelch_mode:1       squelch mode flag, 0=carrier, 1=ctcss/dcs
+#   :1        rxdcsextra:1         use with rxcode for index of rx DCS to use
+#   :1        rxinv:1              inverse DCS rx polarity flag, 0=N, 1=I
+#   :1        txdcsextra:1         use with txcode for index of tx DCS to use
+#   :1        txinv:1              inverse DCS tx polarity flag, 0=N, 1=I
+#  1          u8
+#   :4        unknown2:4
+#   :2        rxtmode:2            rx tone mode, value range 0-2, 0=none,
+#                                  1=CTCSS, 2=DCS  (ctcss tone in field rxtone)
+#   :2        txtmode:2            tx tone mode, value range 0-2, 0=none,
+#                                  1=CTCSS, 3=DCS  (ctcss tone in field txtone)
+#  1          u8
+#   :2        unknown3:2
+#   :6        txtone:6             tx ctcss tone, menu index
+#  1          u8
+#   :2        unknown4:2
+#   :6        rxtone:6             rx ctcss tone, menu index
+#  1          u8 txcode            ?, not used for ctcss
+#  1          u8 rxcode            ?, not used for ctcss
+#  1          u8
+#   :6        unknown5:6
+#   :1        busychannellockout:1 busy channel lockout flag, 0=off, 1=enabled
+#   :1        unknown6:1
+#  6          char name[6]         6 byte char string for channel name
+#  9          u8 unknown7[9]
+#
+MEM_FORMAT = """
+#seekto 0x0000;
+struct {
+  bbcd freq[4];
+  bbcd offset[4];
+  u8 unknown0;
+  u8 reverse:1,
+     tx_off:1,
+     txpower:2,
+     duplex:2,
+     channel_width:2;
+  u8 unknown1:2,
+     talkaround:1,
+     squelch_mode:1,
+     rxdcsextra:1,
+     rxinv:1,
+     txdcsextra:1,
+     txinv:1;
+  u8 unknown2:4,
+     rxtmode:2,
+     txtmode:2;
+  u8 unknown3:2,
+     txtone:6;
+  u8 unknown4:2,
+     rxtone:6;
+  u8 txcode;
+  u8 rxcode;
+  u8 unknown5:6,
+     busychannellockout:1,
+     unknown6:1;
+  char name[6];
+  u8 unknown7[9];
+} memory[199];
+"""
+
+#  RT98  memory map
+#  section: 2 and 3  Channel Set/Skip Flags
+#
+#    Channel Set (starts 0x3240) : Channel Set  bit is value 0 if a memory
+#                                  location in the channel bank is active.
+#    Channel Skip (starts 0x3260): Channel Skip bit is value 0 if a memory
+#                                  location in the channel bank is active.
+#
+#    Both flag maps are a total 24 bytes in length, aligned on 32 byte records.
+#    bit = 0 channel not set/skip,  1 is channel set/no skip
+#
+#    to index a channel:
+#        cbyte = channel / 8 ;
+#        cbit  = channel % 8 ;
+#        setflag  = csetflag[cbyte].c[cbit] ;
+#        skipflag = cskipflag[cbyte].c[cbit] ;
+#
+#    channel range is 1-199, range is 32 bytes (last 7 unknown)
+#
+MEM_FORMAT = MEM_FORMAT + """
+#seekto 0x3240;
+struct {
+   bit c[8];
+} csetflag[32];
+
+#seekto 0x3260;
+struct {
+   bit c[8];
+} cskipflag[32];
+
+"""
+
+#  RT98  memory map
+#  section: 4  Startup Label
+#
+#  bytes:bit  type                 description
+#  ---------------------------------------------------------------------------
+#  6          char start_label[6]  label displayed at startup (usually
+#                                  your call sign)
+#
+MEM_FORMAT = MEM_FORMAT + """
+#seekto 0x3300;
+struct {
+    char startname[6];
+} slabel;
+"""
+
+#  RT98  memory map
+#  section: 5, 6 and 7  Radio Options
+#        used to set a number of radio options
+#
+# description of function setup options, starting at 0x3310 (settings3)
+#
+#  bytes:bit  type                 description
+#  ---------------------------------------------------------------------------
+#  1          u8
+#   :6        unknown:6
+#   :2        bandlimit_3310:2     frequency ranges, range 0-2,
+#                                  0=freenet(vhf) or pmr(uhf), 1=com, 2=comii
+#                   rt98v - 00 FreeNet Rx(149 - 149.2 MHz) Tx(149 - 149.2 MHz)
+#                           01 COM     Rx(136 - 174 MHz) Tx(136 - 174 MHz)
+#                           02 COMII   Rx(147 - 174 MHz) Tx(147 - 174 MHz)
+#                   rt98u - 00 PMR     Rx(446 - 446.2 MHz) Tx(446 - 446.2 MHz)
+#                           01 COM     Rx(400 - 470 MHz) Tx(400 - 470 MHz)
+#                           02 COMII   Rx(450 - 470 MHz) Tx(450 - 470 MHz)
+#  1          u8 ch_number;        channel number, range 1-199
+#
+# description of function setup options, starting at 0x3340 (settings)
+#
+#  bytes:bit  type                   description
+#  ---------------------------------------------------------------------------
+#  1          u8
+#   :4        unknown_3340:4
+#   :4        tuning_step:4          tuning step, menu index value from 0-8
+#                                    2.5, 5, 6.25, 10, 12.5, 20, 25, 30, 50
+#  1          u8
+#   :7        unknown_3341:7
+#   :1        beep:1                 beep mode, range 0-1, 0=off, 1=on
+#  1           u8
+#   :3        unknown_3342:3
+#   :5        timeout_timer:5        timeout timer, range off (no timeout),
+#                                    1-30 minutes
+#  1          u8
+#   :6        unknown_3343:6
+#   :2        auto_power_off:2       auto power off, range 0-3, off, 30min,
+#                                    1hr, 2hr
+#  1          u8
+#   :4        unknown_3344:4
+#   :4        squelch:4              squelch level, range off, 1-9
+#  1          u8
+#   :3        unknown_3345:3
+#   :5        volume:5               volume level, range 1-30 (no zero)
+#  1          u8 unknown_3346
+#  1          u8 unknown_3347
+#  1          u8   0x3348 [12]
+#   :6        unknown_3348:6
+#   :2        display_mode           display mode, range 0-2, 0=channel,
+#                                    1=frequency, 2=name
+#  1           u8
+#   :7        unknown_3349:7
+#   :1        auto_power_on:1        auto power on, range 0-1, 0=manual,
+#                                    1=auto
+#  1          u8
+#   :3        unknown_334A:3
+#   :5        mic_gain:5             mic gain, range 1-30 (no zero)
+#  1          u8
+#   :5        unknown_334C:5
+#   :3        ste_type:3             ste type, range 0-4, 0=off, 1=silent,
+#                                    2=120degree, 3=180degree, 4=240degree
+#  1          u8
+#   :7        unknown_334D:7
+#   :1        ste_frequency:1        ste frequency, range 0-2, 0=off,
+#                                    1=55.2Hz, 2=259.2Hz
+#  1          u8
+#   :2        unknown_0x334E:2
+#   :1        forbid_setting:1       forbid setting(optional function),
+#                                    range 0-1, 0=disabled, 1=enabled
+#   :1        forbid_initialize:1    forbid initialize operate, range 0-1,
+#                                    0=enabled, 1=disabled (inverted)
+#   :1        save_chan_param:1      save channel parameters, range 0-1,
+#                                    0=disabled, 1=enabled
+#   :1        forbid_chan_menu:1     forbid channel menu, range 0-1,
+#                                    0=disabled, 1=enabled
+#   :1        sql_key_function:1     sql key function, range 0-1,
+#                                    0=squelch off momentary, 1=squelch off
+#   :1        unknown:1
+#
+# description of function setup options, starting at 0x3380 (settings2)
+#
+#  bytes:bit  type                   description
+#  ---------------------------------------------------------------------------
+#  1          u8
+#   :7        unknown_3380:7
+#   :1        scan_mode:1            scan mode, range 0-1, 0=off, 1=on
+#  1          u8
+#   :6        unknown_3381:6
+#   :2        priority_ch:2          priority channel, range 0-3, 0=off,
+#                                    1=priority channel 1,
+#                                    2=priority channel 2,
+#                                    3=priority channel 1 + priority channel 2
+#  1          u8 priority_ch1        priority channel 1 number, range 1-199
+#  1          u8 priority_ch2        priority channel 2 number, range 1-199
+#  1          u8
+#   :4        unknown_3384:4
+#   :4        revert_ch:4            revert channel, range 0-3, 0=selected,
+#                                    1=selected + talkback, 2=last called,
+#                                    3=last used
+#  1          u8 look_back_time_a    look back time a, range 0-45
+#  1          u8 look_back_time_b    look back time b, range 0-45
+#  1          u8 dropout_delay_time  dropout delay time, range 0-49
+#  1          u8 dwell_time          dwell time, range 0-49
+#
+MEM_FORMAT = MEM_FORMAT + """
+#seekto 0x3310;
+struct {
+    u8 bandlimit;
+    u8 ch_number;
+} settings3;
+"""
+
+MEM_FORMAT = MEM_FORMAT + """
+#seekto 0x3340;
+struct {
+  u8 unknown_3340:4,
+     tuning_step:4;
+  u8 unknown_3341:7,
+     beep:1;
+  u8 unknown_3342:3,
+     timeout_timer:5;
+  u8 unknown_3343:6,
+     auto_power_off:2;
+  u8 unknown_3344:4,
+     squelch:4;
+  u8 unknown_3345:3,
+     volume:5;
+  u8 unknown_3346;
+  u8 unknown_3347;
+  u8 unknown_3348:6,
+     display_mode:2;
+  u8 unknown_3349:7,
+     auto_power_on:1;
+  u8 unknown_334A:3,
+     mic_gain:5;
+  u8 unknown_334B;
+  u8 unknown_334C:5,
+     ste_type:3;
+  u8 unknown_334D:6,
+     ste_frequency:2;
+  u8 unknown_334E:1,
+     forbid_setting:1,
+     unknown1:1,
+     forbid_initialize:1,
+     save_chan_param:1,
+     forbid_chan_menu:1,
+     sql_key_function:1,
+     unknown2:1;
+} settings;
+"""
+
+MEM_FORMAT = MEM_FORMAT + """
+#seekto 0x3380;
+struct {
+  u8 unknown_3380:7,
+     scan_mode:1;
+  u8 unknown_3381:6,
+     priority_ch:2;
+  u8 priority_ch1;
+  u8 priority_ch2;
+  u8 unknown_3384:4,
+     revert_ch:4;
+  u8 look_back_time_a;
+  u8 look_back_time_b;
+  u8 dropout_delay_time;
+  u8 dwell_time;
+} settings2;
+"""
+
+#  RT98  memory map
+#  section: 8  Embedded Messages
+#
+#  bytes:bit  type                 description
+#  ---------------------------------------------------------------------------
+#  6          char radio_type[5]   radio type, vhf=rt98v, uhf=rt98u
+#  2          u8 unknown1[2]
+#  4          char mcu_version[4]  mcu version, [x.xx]
+#  2          u8 unknown2[2]
+#  1          u8 mode              rt98u mode: 0=pmr, 1=com, 2=comii
+#                                  rt98v mode: 0=freenet, 1=com, 2=comii
+#  1          u8 unknown3
+#  10         u8 unused1[10]
+#  4          u8 unknown4[4]
+#  3          u8 unused2[3]
+#  16         u8 unknown5[16]
+#  10         char date_mfg[16]    date manufactured, [yyyy-mm-dd]
+#
+MEM_FORMAT = MEM_FORMAT + """
+#seekto 0x3D00;
+struct {
+char radio_type[5];
+u8 unknown1[2];
+char mcu_version[4];
+u8 unknown2[2];
+u8 mode;
+u8 unknown3;
+u8 unused1[10];
+u8 unknown4[4];
+u8 unused2[3];
+u8 unknown5[16];
+char date_mfg[10];
+} embedded_msg;
+"""
+
+
+# Format for the version messages returned by the radio
+VER_FORMAT = '''
+u8 hdr;
+char model[5];
+u8 unknown[2];
+u8 bandlimit;
+char version[6];
+u8 ack;
+'''
+
+
+# Radio supports upper case and symbols
+CHARSET_ASCII_PLUS = chirp_common.CHARSET_UPPER_NUMERIC + '- '
+
+# Band limits as defined by the band byte in ver_response, defined in Hz, for
+# VHF and UHF, used for RX and TX.
+RT98V_BAND_LIMITS = {0x00: [(149000000, 149200000)],
+                     0x01: [(136000000, 174000000)],
+                     0x02: [(147000000, 174000000)]}
+
+RT98U_BAND_LIMITS = {0x00: [(446000000, 446200000)],
+                     0x01: [(400000000, 470000000)],
+                     0x02: [(450000000, 470000000)]}
+
+
+# Get band limits from a band limit value
+def get_band_limits_Hz(radio_type, limit_value):
+    if radio_type == "RT98U":
+        if limit_value not in RT98U_BAND_LIMITS:
+            limit_value = 0x01
+            LOG.warning('Unknown band limit value 0x%02x, default to 0x01')
+        bandlimitfrequencies = RT98U_BAND_LIMITS[limit_value]
+    elif radio_type == "RT98V":
+        if limit_value not in RT98V_BAND_LIMITS:
+            limit_value = 0x01
+            LOG.warning('Unknown band limit value 0x%02x, default to 0x01')
+        bandlimitfrequencies = RT98V_BAND_LIMITS[limit_value]
+    return bandlimitfrequencies
+
+
+def _echo_write(radio, data):
+    try:
+        radio.pipe.write(data)
+        radio.pipe.read(len(data))
+    except Exception, e:
+        LOG.error("Error writing to radio: %s" % e)
+        raise errors.RadioError("Unable to write to radio")
+
+
+def _checksum(data):
+    cs = 0
+    for byte in data:
+        cs += ord(byte)
+    return cs % 256
+
+
+def _read(radio, length):
+    try:
+        data = radio.pipe.read(length)
+    except Exception, e:
+        _finish(radio)
+        LOG.error("Error reading from radio: %s" % e)
+        raise errors.RadioError("Unable to read from radio")
+
+    if len(data) != length:
+        _finish(radio)
+        LOG.error("Short read from radio (%i, expected %i)" %
+                  (len(data), length))
+        LOG.debug(util.hexprint(data))
+        raise errors.RadioError("Short read from radio")
+    return data
+
+
+# strip trailing 0x00 to convert a string returned by bitwise.parse into a
+# python string
+def cstring_to_py_string(cstring):
+    return "".join(c for c in cstring if c != '\x00')
+
+
+# Check the radio version reported to see if it's one we support,
+# returns bool version supported, and the band index
+def check_ver(ver_response, allowed_types):
+    ''' Check the returned radio version is one we approve of '''
+
+    LOG.debug('ver_response = ')
+    LOG.debug(util.hexprint(ver_response))
+
+    resp = bitwise.parse(VER_FORMAT, ver_response)
+    verok = False
+
+    if resp.hdr == 0x49 and resp.ack == 0x06:
+        model, version = [cstring_to_py_string(bitwise.get_string(s)).strip()
+                          for s in (resp.model, resp.version)]
+        LOG.debug('radio model: \'%s\' version: \'%s\'' %
+                  (model, version))
+        LOG.debug('allowed_types = %s' % allowed_types)
+
+        if model in allowed_types:
+            LOG.debug('model in allowed_types')
+
+            if version in allowed_types[model]:
+                LOG.debug('version in allowed_types[model]')
+                verok = True
+    else:
+        _finish(radio)
+        raise errors.RadioError('Failed to parse version response')
+
+    return verok, str(resp.model), int(resp.bandlimit)
+
+
+def _ident(radio):
+    radio.pipe.timeout = 1
+    _echo_write(radio, "PROGRAM")
+    response = radio.pipe.read(3)
+    if response != "QX\06":
+        _finish(radio)
+        LOG.debug("Response was :\n%s" % util.hexprint(response))
+        raise errors.RadioError("Radio did not respond. Check connection.")
+    _echo_write(radio, "\x02")
+    ver_response = radio.pipe.read(16)
+    LOG.debug(util.hexprint(ver_response))
+
+    verok, model, bandlimit = check_ver(ver_response,
+                                        radio.ALLOWED_RADIO_TYPES)
+    if not verok:
+        _finish(radio)
+        raise errors.RadioError(
+            'Radio version not in allowed list for %s-%s: %s' %
+            (radio.VENDOR, radio.MODEL, util.hexprint(ver_response)))
+
+    return model, bandlimit
+
+
+def _send(radio, cmd, addr, length, data=None):
+    frame = struct.pack(">cHb", cmd, addr, length)
+    if data:
+        frame += data
+        frame += chr(_checksum(frame[1:]))
+        frame += "\x06"
+    _echo_write(radio, frame)
+    LOG.debug("Sent:\n%s" % util.hexprint(frame))
+    if data:
+        result = radio.pipe.read(1)
+        if result != "\x06":
+            _finish(radio)
+            LOG.debug("Ack was: %s" % repr(result))
+            raise errors.RadioError("Radio did not accept block at %04x"
+                                    % addr)
+        return
+    result = _read(radio, length + 6)
+    LOG.debug("Got:\n%s" % util.hexprint(result))
+    header = result[0:4]
+    data = result[4:-2]
+    ack = result[-1]
+    if ack != "\x06":
+        _finish(radio)
+        LOG.debug("Ack was: %s" % repr(ack))
+        raise errors.RadioError("Radio NAK'd block at %04x" % addr)
+    _cmd, _addr, _length = struct.unpack(">cHb", header)
+    if _addr != addr or _length != _length:
+        _finish(radio)
+        LOG.debug("Expected/Received:")
+        LOG.debug(" Length: %02x/%02x" % (length, _length))
+        LOG.debug(" Addr: %04x/%04x" % (addr, _addr))
+        raise errors.RadioError("Radio send unexpected block")
+    cs = _checksum(result[1:-2])
+    if cs != ord(result[-2]):
+        _finish(radio)
+        LOG.debug("Calculated: %02x" % cs)
+        LOG.debug("Actual:     %02x" % ord(result[-2]))
+        raise errors.RadioError("Block at 0x%04x failed checksum" % addr)
+    return data
+
+
+def _finish(radio):
+    endframe = "\x45\x4E\x44"
+    _echo_write(radio, endframe)
+    result = radio.pipe.read(1)
+    if result != "\x06":
+        LOG.error("Got:\n%s" % util.hexprint(result))
+        raise errors.RadioError("Radio did not finish cleanly")
+
+
+def do_download(radio):
+
+    _ident(radio)
+
+    _memobj = None
+    data = ""
+
+    for addr in range(0, radio._memsize, 0x10):
+        block = _send(radio, 'R', addr, 0x10)
+        data += block
+        status = chirp_common.Status()
+        status.cur = len(data)
+        status.max = radio._memsize
+        status.msg = "Downloading from radio"
+        radio.status_fn(status)
+
+    _finish(radio)
+
+    return memmap.MemoryMap(data)
+
+
+def do_upload(radio):
+    model, bandlimit = _ident(radio)
+    _embedded = radio._memobj.embedded_msg
+
+    if model != str(_embedded.radio_type):
+        LOG.warning('radio and image model types differ')
+        LOG.warning('model type (radio): %s' % str(model))
+        LOG.warning('model type (image): %s' % str(_embedded.radio_type))
+
+        _finish(radio)
+
+        msg = ("The upload was stopped because the radio type "
+               "of the image (%s) does not match that "
+               "of the radio (%s).")
+        raise errors.RadioError(msg % (str(_embedded.radio_type), str(model)))
+
+    if bandlimit != int(_embedded.mode):
+        if str(_embedded.radio_type) == "RT98U":
+            image_band_limits = LIST_RT98U_FREQS[int(_embedded.mode)]
+        if str(_embedded.radio_type) == "RT98V":
+            image_band_limits = LIST_RT98V_FREQS[int(_embedded.mode)]
+        if model == "RT98U":
+            radio_band_limits = LIST_RT98U_FREQS[int(bandlimit)]
+        if model == "RT98V":
+            radio_band_limits = LIST_RT98V_FREQS[int(bandlimit)]
+
+        LOG.warning('radio and image band limits differ')
+        LOG.warning('image band limits: %s' % image_band_limits)
+        LOG.warning('radio band limits: %s' % radio_band_limits)
+
+        _finish(radio)
+
+        msg = ("The upload was stopped because the band limits "
+               "of the image (%s) does not match that "
+               "of the radio (%s).")
+        raise errors.RadioError(msg % (image_band_limits, radio_band_limits))
+
+    try:
+        for start, end in radio._ranges:
+            for addr in range(start, end, 0x10):
+                block = radio._mmap[addr:addr+0x10]
+                _send(radio, 'W', addr, len(block), block)
+                status = chirp_common.Status()
+                status.cur = addr
+                status.max = end
+                status.msg = "Uploading to Radio"
+                radio.status_fn(status)
+        _finish(radio)
+    except errors.RadioError:
+        raise
+    except Exception as e:
+        _finish(radio)
+        raise errors.RadioError('Failed to upload to radio: %s' % e)
+
+
+#
+# The base class, extended for use with other models
+#
+class Rt98BaseRadio(chirp_common.CloneModeRadio,
+                    chirp_common.ExperimentalRadio):
+    """Retevis RT98 Base"""
+    VENDOR = "Retevis"
+    MODEL = "RT98 Base"
+    BAUD_RATE = 9600
+
+    _memsize = 0x3E00
+    _ranges = [(0x0000, 0x3310),
+               (0x3320, 0x3390)]
+
+    @classmethod
+    def get_prompts(cls):
+        rp = chirp_common.RadioPrompts()
+        rp.experimental = ("The Retevis RT98 driver is an beta version."
+                           "Proceed with Caution and backup your data")
+        return rp
+
+    def get_features(self):
+        _embedded = self._memobj.embedded_msg
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.has_bank = False
+        rf.can_odd_split = True
+        rf.has_name = True
+        if _embedded.mode == 0:  # PMR or FreeNet
+            rf.has_offset = False
+        else:
+            rf.has_offset = True
+        rf.has_ctone = True
+        rf.has_cross = True
+        rf.has_tuning_step = False
+        rf.has_dtcs = True
+        rf.has_rx_dtcs = True
+        rf.has_dtcs_polarity = True
+        rf.valid_skips = ["", "S"]
+        rf.memory_bounds = (1, 199)
+        rf.valid_name_length = 6
+        if _embedded.mode == 0:  # PMR or FreeNet
+            rf.valid_duplexes = ['']
+        else:
+            rf.valid_duplexes = DUPLEXES + ['split', 'off']
+        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "- "
+        if _embedded.mode == 0:  # PMR or FreeNet
+            rf.valid_modes = ['NFM']
+        else:
+            rf.valid_modes = ['FM', 'NFM']
+        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
+        rf.valid_cross_modes = CROSS_MODES
+        if _embedded.mode == 0:  # PMR or FreeNet
+            if str(_embedded.radio_type) == "RT98U":
+                rf.valid_power_levels = PMR_POWER_LEVELS
+            if str(_embedded.radio_type) == "RT98V":
+                rf.valid_power_levels = FREENET_POWER_LEVELS
+        else:
+            rf.valid_power_levels = POWER_LEVELS
+        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
+
+        try:
+            rf.valid_bands = get_band_limits_Hz(
+                str(_embedded.radio_type),
+                int(_embedded.mode))
+        except TypeError as e:
+            # If we're asked without memory loaded, assume the most permissive
+            rf.valid_bands = get_band_limits_Hz(str(_embedded.radio_type), 1)
+        except Exception as e:
+            LOG.error('Failed to get band limits for RT98: %s' % e)
+            rf.valid_bands = get_band_limits_Hz(str(_embedded.radio_type), 1)
+
+        rf.valid_tuning_steps = TUNING_STEPS
+        return rf
+
+    def validate_memory(self, mem):
+        _embedded = self._memobj.embedded_msg
+        msgs = ""
+        msgs = chirp_common.CloneModeRadio.validate_memory(self, mem)
+
+        # FreeNet and PMR radio types
+        if _embedded.mode == 0:  # PMR or FreeNet
+            freq = float(mem.freq) / 1000000
+
+            # FreeNet
+            if str(_embedded.radio_type) == "RT98V":
+                if freq not in FREENET_FREQS:
+                    _msg_freq = 'Memory location not a valid FreeNet frequency'
+                    # warn user invalid frequency
+                    msgs.append(chirp_common.ValidationError(_msg_freq))
+
+            # PMR
+            if str(_embedded.radio_type) == "RT98U":
+                if freq not in PMR_FREQS:
+                    _msg_freq = 'Memory location not a valid PMR frequency'
+                    # warn user invalid frequency
+                    msgs.append(chirp_common.ValidationError(_msg_freq))
+
+        return msgs
+
+    # Do a download of the radio from the serial port
+    def sync_in(self):
+        self._mmap = do_download(self)
+        self.process_mmap()
+
+    # Do an upload of the radio to the serial port
+    def sync_out(self):
+        do_upload(self)
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    # Return a raw representation of the memory object, which
+    # is very helpful for development
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def _get_dcs_index(self, _mem, which):
+        base = getattr(_mem, '%scode' % which)
+        extra = getattr(_mem, '%sdcsextra' % which)
+        return (int(extra) << 8) | int(base)
+
+    def _set_dcs_index(self, _mem, which, index):
+        base = getattr(_mem, '%scode' % which)
+        extra = getattr(_mem, '%sdcsextra' % which)
+        base.set_value(index & 0xFF)
+        extra.set_value(index >> 8)
+
+    # Extract a high-level memory object from the low-level memory map
+    # This is called to populate a memory in the UI
+    def get_memory(self, number):
+        _embedded = self._memobj.embedded_msg
+        # Get a low-level memory object mapped to the image
+        _mem = self._memobj.memory[number - 1]
+
+        # get flag info
+        cbyte = (number - 1) / 8
+        cbit = 7 - ((number - 1) % 8)
+        setflag = self._memobj.csetflag[cbyte].c[cbit]
+        skipflag = self._memobj.cskipflag[cbyte].c[cbit]
+
+        mem = chirp_common.Memory()
+
+        mem.number = number  # Set the memory number
+
+        # We'll consider any blank (i.e. 0MHz frequency) to be empty
+        if _mem.freq == 0:
+            mem.empty = True
+            return mem
+
+        if setflag == 0:
+            mem.empty = True
+            return mem
+
+        if _mem.get_raw()[0] == "\xFF":
+            mem.empty = True
+            return mem
+
+        # set the name
+        mem.name = str(_mem.name).rstrip()  # Set the alpha tag
+
+        # Convert your low-level frequency and offset to Hertz
+        mem.freq = int(_mem.freq) * 10
+        mem.offset = int(_mem.offset) * 10
+
+        # Set the duplex flags
+        if _mem.duplex == DUPLEX_POSSPLIT:
+            mem.duplex = '+'
+        elif _mem.duplex == DUPLEX_NEGSPLIT:
+            mem.duplex = '-'
+        elif _mem.duplex == DUPLEX_NOSPLIT:
+            mem.duplex = ''
+        elif _mem.duplex == DUPLEX_ODDSPLIT:
+            mem.duplex = 'split'
+        else:
+            LOG.error('%s: get_mem: unhandled duplex: %02x' %
+                      (mem.name, _mem.duplex))
+
+        # handle tx off
+        if _mem.tx_off:
+            mem.duplex = 'off'
+
+        # Set the channel width
+        if _mem.channel_width == CHANNEL_WIDTH_12d5kHz:
+            mem.mode = 'NFM'
+        elif _embedded.mode == 0:  # PMR or FreeNet
+            LOG.info('PMR and FreeNet channels must be Channel Width 12.5kHz')
+            mem.mode = 'NFM'
+        elif _mem.channel_width == CHANNEL_WIDTH_25kHz:
+            mem.mode = 'FM'
+        elif _mem.channel_width == CHANNEL_WIDTH_20kHz:
+            LOG.info(
+                '%s: get_mem: promoting 20kHz channel width to 25kHz' %
+                mem.name)
+            mem.mode = 'FM'
+        else:
+            LOG.error('%s: get_mem: unhandled channel width: 0x%02x' %
+                      (mem.name, _mem.channel_width))
+
+        # set the power level
+        if _embedded.mode == 0:  # PMR or FreeNet
+            if str(_embedded.radio_type) == "RT98U":
+                LOG.info('using PMR power levels')
+                _levels = PMR_POWER_LEVELS
+            if str(_embedded.radio_type) == "RT98V":
+                LOG.info('using FreeNet power levels')
+                _levels = FREENET_POWER_LEVELS
+        else:  # COM or COMII
+            LOG.info('using general power levels')
+            _levels = POWER_LEVELS
+
+        if _mem.txpower == TXPOWER_LOW:
+            mem.power = _levels[0]
+        elif _embedded.mode == 0:  # PMR or FreeNet
+            LOG.info('FreeNet or PMR channel is not set to TX Power Low')
+            LOG.info('Setting channel to TX Power Low')
+            mem.power = _levels[0]
+        elif _mem.txpower == TXPOWER_MED:
+            mem.power = _levels[1]
+        elif _mem.txpower == TXPOWER_HIGH:
+            mem.power = _levels[2]
+        else:
+            LOG.error('%s: get_mem: unhandled power level: 0x%02x' %
+                      (mem.name, _mem.txpower))
+
+        # CTCSS Tones and DTCS Codes
+        rxtone = txtone = None
+
+        rxmode = TMODES[_mem.rxtmode]
+        txmode = TMODES[_mem.txtmode]
+
+        if rxmode == "Tone":
+            rxtone = TONES[_mem.rxtone]
+        elif rxmode == "DTCS":
+            rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(
+                                                 _mem, 'rx')]
+
+        if txmode == "Tone":
+            txtone = TONES[_mem.txtone]
+        elif txmode == "DTCS":
+            txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(
+                                                 _mem, 'tx')]
+
+        rxpol = _mem.rxinv and "R" or "N"
+        txpol = _mem.txinv and "R" or "N"
+
+        chirp_common.split_tone_decode(mem,
+                                       (txmode, txtone, txpol),
+                                       (rxmode, rxtone, rxpol))
+
+        # Check if this memory is in the scan enabled list
+        mem.skip = "S" if skipflag == 0 else ""
+
+        # Extra
+        mem.extra = RadioSettingGroup("extra", "Extra")
+
+        rs = RadioSettingValueBoolean(bool(_mem.busychannellockout))
+        rset = RadioSetting("busychannellockout", "Busy channel lockout", rs)
+        mem.extra.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(_mem.reverse))
+        rset = RadioSetting("reverse", "Reverse", rs)
+        mem.extra.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(_mem.talkaround))
+        rset = RadioSetting("talkaround", "Talk around", rs)
+        mem.extra.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(_mem.squelch_mode))
+        rset = RadioSetting("squelch_mode", "Squelch mode", rs)
+        mem.extra.append(rset)
+
+        return mem
+
+    # Store details about a high-level memory to the memory map
+    # This is called when a user edits a memory in the UI
+    def set_memory(self, mem):
+        _embedded = self._memobj.embedded_msg
+        # Get a low-level memory object mapped to the image
+
+        _mem = self._memobj.memory[mem.number - 1]
+
+        cbyte = (mem.number - 1) / 8
+        cbit = 7 - ((mem.number - 1) % 8)
+
+        if mem.empty:
+            self._memobj.csetflag[cbyte].c[cbit] = 0
+            self._memobj.cskipflag[cbyte].c[cbit] = 0
+            _mem.set_raw('\xff' * (_mem.size() / 8))
+            return
+
+        _mem.set_raw('\x00' * (_mem.size() / 8))
+
+        # set the occupied bitfield
+        self._memobj.csetflag[cbyte].c[cbit] = 1
+        # set the scan add bitfield
+        self._memobj.cskipflag[cbyte].c[cbit] = 0 if (mem.skip == "S") else 1
+
+        _mem.freq = mem.freq / 10             # Convert to low-level frequency
+        _mem.offset = mem.offset / 10         # Convert to low-level frequency
+
+        # Store the alpha tag
+        _mem.name = mem.name.ljust(6)[:6]  # Store the alpha tag
+
+        # Set duplex bitfields
+        if mem.duplex == '+':
+            _mem.duplex = DUPLEX_POSSPLIT
+        elif mem.duplex == '-':
+            _mem.duplex = DUPLEX_NEGSPLIT
+        elif mem.duplex == '':
+            _mem.duplex = DUPLEX_NOSPLIT
+        elif mem.duplex == 'split':
+            diff = mem.offset - mem.freq
+            _mem.duplex = DUPLEXES.index("-") \
+                if diff < 0 else DUPLEXES.index("+")
+            _mem.offset = abs(diff) / 10
+        else:
+            LOG.error('%s: set_mem: unhandled duplex: %s' %
+                      (mem.name, mem.duplex))
+
+        # handle tx off
+        _mem.tx_off = 0
+        if mem.duplex == 'off':
+            _mem.tx_off = 1
+
+        # Set the channel width - remember we promote 20kHz channels to FM
+        # on import, so don't handle them here
+        if mem.mode == 'FM':
+            _mem.channel_width = CHANNEL_WIDTH_25kHz
+        elif mem.mode == 'NFM':
+            _mem.channel_width = CHANNEL_WIDTH_12d5kHz
+        else:
+            LOG.error('%s: set_mem: unhandled mode: %s' % (
+                mem.name, mem.mode))
+
+        # CTCSS Tones and DTCS Codes
+        ((txmode, txtone, txpol),
+         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
+
+        _mem.txtmode = TMODES.index(txmode)
+
+        _mem.rxtmode = TMODES.index(rxmode)
+
+        if txmode == "Tone":
+            _mem.txtone = TONES.index(txtone)
+        elif txmode == "DTCS":
+            self._set_dcs_index(_mem, 'tx',
+                                chirp_common.ALL_DTCS_CODES.index(txtone))
+
+        _mem.squelch_mode = False
+        if rxmode == "Tone":
+            _mem.rxtone = TONES.index(rxtone)
+            _mem.squelch_mode = True
+        elif rxmode == "DTCS":
+            self._set_dcs_index(_mem, 'rx',
+                                chirp_common.ALL_DTCS_CODES.index(rxtone))
+            _mem.squelch_mode = True
+
+        _mem.txinv = txpol == "R"
+        _mem.rxinv = rxpol == "R"
+
+        # set the power level
+        if mem.power == POWER_LEVELS[0]:
+            _mem.txpower = TXPOWER_LOW
+        elif mem.power == POWER_LEVELS[1]:
+            _mem.txpower = TXPOWER_MED
+        elif mem.power == POWER_LEVELS[2]:
+            _mem.txpower = TXPOWER_HIGH
+        else:
+            LOG.error('%s: set_mem: unhandled power level: %s' %
+                      (mem.name, mem.power))
+
+        # extra settings
+        for setting in mem.extra:
+            setattr(_mem, setting.get_name(), setting.value)
+
+    def _get_settings(self):
+        _embedded = self._memobj.embedded_msg
+        _settings = self._memobj.settings
+        _settings2 = self._memobj.settings2
+        _settings3 = self._memobj.settings3
+        _slabel = self._memobj.slabel
+
+        function = RadioSettingGroup("function", "Function Setup")
+        group = RadioSettings(function)
+
+        # Function Setup
+        # MODE SET
+        rs = RadioSettingValueList(LIST_DISPLAY_MODE,
+                                   LIST_DISPLAY_MODE[_settings.display_mode])
+        rset = RadioSetting("display_mode", "Display Mode", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueInteger(1, 199, _settings3.ch_number + 1)
+        rset = RadioSetting("settings3.ch_number", "Channel Number", rs)
+        function.append(rset)
+
+        # DISPLAY SET
+        def _filter(name):
+            filtered = ""
+            for char in str(name):
+                if char in chirp_common.CHARSET_ASCII:
+                    filtered += char
+                else:
+                    filtered += " "
+            return filtered
+
+        val = RadioSettingValueString(0, 6, _filter(_slabel.startname))
+        rs = RadioSetting("slabel.startname", "Startup Label", val)
+        function.append(rs)
+
+        # VOL SET
+        rs = RadioSettingValueBoolean(bool(_settings.beep))
+        rset = RadioSetting("beep", "Beep Prompt", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueInteger(1, 30, _settings.volume)
+        rset = RadioSetting("volume", "Volume Level", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueInteger(1, 16, _settings.mic_gain)
+        rset = RadioSetting("mic_gain", "Mic Gain", rs)
+        function.append(rset)
+
+        # ON/OFF SET
+        rs = RadioSettingValueList(LIST_APO,
+                                   LIST_APO[_settings.auto_power_off])
+        rset = RadioSetting("auto_power_off", "Auto Power Off", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueList(LIST_AOP, LIST_AOP[_settings.auto_power_on])
+        rset = RadioSetting("auto_power_on", "Power On Method", rs)
+        function.append(rset)
+
+        # STE SET
+        rs = RadioSettingValueList(LIST_STE_FREQ,
+                                   LIST_STE_FREQ[_settings.ste_frequency])
+        rset = RadioSetting("ste_frequency", "STE Frequency", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueList(LIST_STE_TYPE,
+                                   LIST_STE_TYPE[_settings.ste_type])
+        rset = RadioSetting("ste_type", "STE Type", rs)
+        function.append(rset)
+
+        # FUNCTION SET
+        rs = RadioSettingValueList(LIST_STEP, LIST_STEP[_settings.tuning_step])
+        rset = RadioSetting("tuning_step", "Tuning Step", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueList(LIST_SQUELCH,
+                                   LIST_SQUELCH[_settings.squelch])
+        rset = RadioSetting("squelch", "Squelch Level", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(_settings.sql_key_function))
+        rset = RadioSetting("sql_key_function", "SQL Key Function", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueList(LIST_TIMEOUT,
+                                   LIST_TIMEOUT[_settings.timeout_timer])
+        rset = RadioSetting("timeout_timer", "Timeout Timer", rs)
+        function.append(rset)
+
+        # uncategorized
+        rs = RadioSettingValueBoolean(bool(_settings.save_chan_param))
+        rset = RadioSetting("save_chan_param", "Save Channel Parameters", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(_settings.forbid_chan_menu))
+        rset = RadioSetting("forbid_chan_menu", "Forbid Channel Menu", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(not _settings.forbid_initialize))
+        rset = RadioSetting("forbid_initialize", "Forbid Initialize", rs)
+        function.append(rset)
+
+        rs = RadioSettingValueBoolean(bool(_settings.forbid_setting))
+        rset = RadioSetting("forbid_setting", "Forbid Setting", rs)
+        function.append(rset)
+
+        # Information Of Scanning Channel
+        scanning = RadioSettingGroup("scanning", "Scanning Setup")
+        group.append(scanning)
+
+        rs = RadioSettingValueBoolean(bool(_settings2.scan_mode))
+        rset = RadioSetting("settings2.scan_mode", "Scan Mode", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueList(LIST_PRIORITY_CH,
+                                   LIST_PRIORITY_CH[_settings2.priority_ch])
+        rset = RadioSetting("settings2.priority_ch", "Priority Channel", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueInteger(1, 199, _settings2.priority_ch1 + 1)
+        rset = RadioSetting("settings2.priority_ch1", "Priority Channel 1", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueInteger(1, 199, _settings2.priority_ch2 + 1)
+        rset = RadioSetting("settings2.priority_ch2", "Priority Channel 2", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueList(LIST_REVERT_CH,
+                                   LIST_REVERT_CH[_settings2.revert_ch])
+        rset = RadioSetting("settings2.revert_ch", "Revert Channel", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueList(LIST_TIME46,
+                                   LIST_TIME46[_settings2.look_back_time_a])
+        rset = RadioSetting("settings2.look_back_time_a",
+                            "Look Back Time A", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueList(LIST_TIME46,
+                                   LIST_TIME46[_settings2.look_back_time_b])
+        rset = RadioSetting("settings2.look_back_time_b",
+                            "Look Back Time B", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueList(LIST_TIME50,
+                                   LIST_TIME50[_settings2.dropout_delay_time])
+        rset = RadioSetting("settings2.dropout_delay_time",
+                            "Dropout Delay Time", rs)
+        scanning.append(rset)
+
+        rs = RadioSettingValueList(LIST_TIME50,
+                                   LIST_TIME50[_settings2.dwell_time])
+        rset = RadioSetting("settings2.dwell_time", "Dwell Time", rs)
+        scanning.append(rset)
+
+        # Embedded Message
+        embedded = RadioSettingGroup("embedded", "Embedded Message")
+        group.append(embedded)
+
+        rs = RadioSettingValueString(0, 5, _filter(_embedded.radio_type))
+        rs.set_mutable(False)
+        rset = RadioSetting("embedded_msg.radio_type", "Radio Type", rs)
+        embedded.append(rset)
+
+        if str(_embedded.radio_type) == "RT98V":
+            options = LIST_RT98V_MODES
+        else:
+            options = LIST_RT98U_MODES
+        rs = RadioSettingValueList(options, options[_embedded.mode])
+        rs.set_mutable(False)
+        rset = RadioSetting("embedded_msg.mode", "Mode", rs)
+        embedded.append(rset)
+
+        # frequency
+        if str(_embedded.radio_type) == "RT98V":
+            options = LIST_RT98V_FREQS
+        else:
+            options = LIST_RT98U_FREQS
+        rs = RadioSettingValueList(options, options[_settings3.bandlimit])
+        rs.set_mutable(False)
+        rset = RadioSetting("settings3.bandlimit", "Frequency", rs)
+        embedded.append(rset)
+
+        rs = RadioSettingValueString(0, 10, _filter(_embedded.date_mfg))
+        rs.set_mutable(False)
+        rset = RadioSetting("embedded_msg.date_mfg", "Production Date", rs)
+        embedded.append(rset)
+
+        rs = RadioSettingValueString(0, 4, _filter(_embedded.mcu_version))
+        rs.set_mutable(False)
+        rset = RadioSetting("embedded_msg.mcu_version", "MCU Version", rs)
+        embedded.append(rset)
+
+        return group
+
+    def get_settings(self):
+        try:
+            return self._get_settings()
+        except:
+            import traceback
+            LOG.error("failed to parse settings")
+            traceback.print_exc()
+            return None
+
+    def set_settings(self, settings):
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            else:
+                try:
+                    if "." in element.get_name():
+                        bits = element.get_name().split(".")
+                        obj = self._memobj
+                        for bit in bits[:-1]:
+                            obj = getattr(obj, bit)
+                        setting = bits[-1]
+                    else:
+                        obj = self._memobj.settings
+                        setting = element.get_name()
+
+                    if element.has_apply_callback():
+                        LOG.debug("using apply callback")
+                        element.run_apply_callback()
+                    elif setting == "ch_number":
+                        setattr(obj, setting, int(element.value) - 1)
+                    elif setting == "forbid_initialize":
+                        setattr(obj, setting, not int(element.value))
+                    elif setting == "priority_ch1":
+                        setattr(obj, setting, int(element.value) - 1)
+                    elif setting == "priority_ch2":
+                        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
+
+
+@directory.register
+class Rt98Radio(Rt98BaseRadio):
+    """Retevis RT98"""
+    VENDOR = "Retevis"
+    MODEL = "RT98"
+    # Allowed radio types is a dict keyed by model of a list of version
+    # strings
+    ALLOWED_RADIO_TYPES = {'RT98V': ['V100'],
+                           'RT98U': ['V100']}
diff -r f586574bc878 -r 0eab8146b294 tools/cpep8.manifest
--- a/tools/cpep8.manifest      Thu Jun 17 21:57:05 2021 -0400
+++ b/tools/cpep8.manifest      Sat Jul 03 14:13:35 2021 -0400
@@ -83,6 +83,7 @@
 ./chirp/drivers/retevis_rt23.py
 ./chirp/drivers/retevis_rt26.py
 ./chirp/drivers/retevis_rt76p.py
+./chirp/drivers/retevis_rt98.py
 ./chirp/drivers/rfinder.py
 ./chirp/drivers/tdxone_tdq8a.py
 ./chirp/drivers/template.py