[chirp_devel] Fwd: [PATCH] [RT98] Add support for Retevis RT98 Single Band Mini Mobile Radios
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
participants (1)
-
Jim Unroe