Project

General

Profile

New Model #11708 » kdh_uv110D-BT(512)x.py

Adds NAM and VFO A&B settings incl Freq and Tuning Step and code cleanups for RT-900 BT - Fred Trimble, 06/23/2025 04:28 PM

 
# Copyright 2024:
# * Pavel Moravec, OK2MOP <moravecp.cz@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging

from chirp import (
bitwise,
chirp_common,
directory,
errors,
memmap,
util,
)
from chirp.settings import (
RadioSetting,
RadioSettingGroup,
RadioSettingSubGroup,
# RadioSettings,
RadioSettingValueBoolean,
# RadioSettingValueInteger,
RadioSettingValueList,
RadioSettingValueString,
RadioSettingValueMap,
InvalidValueError
)

from chirp.drivers import (
baofeng_uv17Pro,
mml_jc8810,
baofeng_common as bfc
)

import struct

LOG = logging.getLogger(__name__)

REMOVE_GROUPS = ['ani']
REMOVE_SETTINGS = ['ani', 'skey2_sp', 'skey2_lp', 'skey3_sp',
'skey3_lp', 'rxendtail']

MEM_FORMAT_KDH110D = """
#seekto 0xB200;
struct {
char name[10]; // 10-character Custom CH Names (Talkpod A36plus)
u8 unused[6];
} customnames[30];

// vfo settings, 32 bytes
struct vfo {
u8 freq[8]; // vfo freq
ul16 rxtone; // coded RX CTCSS/DTC tone
ul16 txtone; // coded TX CTCSS/DTC tone
u8 unknown0[2];
u8 unknown1:2,
sftd:2, // offset direction, 0 == OFF, 1 == +, 2 == -
scode:4; // scode 0-15
u8 unknown2;
u8 unknown3:3,
scramble:1, // 0b0 == OFF, 0b1 == ON
unknown4:2,
txpower:2; // TX power, 0b0 == HIGH, 0b1 == Middle, 0b2 == LOW
u8 unknown5:1,
widenarr:1, // Bandwidth, 0b0 == WIDE, 0b1 == NARROW
voicepri:2, // Voice privacy encryption, 0b00 == OFF, 0b01 == ENCRY1, 2 == ENCRY2, 0b11 == ENCRY3
unknown6:2,
rxmod:1, // RX Modulation, 0b0 == FM, 0b1 == AM
unknown7:1;
u8 unknown8;
u8 step; // vfo tuning step size index: 0 == 2.5K, ... 6 == 50K, 7 == 8.33K: 0x8013, 0x8033
u8 offset[6]; // TX freq offset: 0x814, 0x834
u8 unknown9[6];
};

#seekto 0x8000; // vfo a & b
struct {
struct vfo a;
struct vfo b;
} vfo;

#seekto 0xD000; // radio operationg mode
struct {
u8 radio_mode;
} opmode;

"""

def _enter_programming_mode(radio):
serial = radio.pipe
mml_jc8810._enter_programming_mode(radio)

try:
ident = serial.read(8)
serial.write(radio._cryptsetup)
ack = serial.read(1)
if ack != mml_jc8810.CMD_ACK:
raise errors.RadioError("Error setting up encryption")
except Exception:
raise errors.RadioError("Error communicating with radio")
if ident not in radio._fingerprint2:
LOG.debug(util.hexprint(ident))
raise errors.RadioError("Radio returned unknown secondary"
" identification string")


def _exit_programming_mode(radio):
mml_jc8810._exit_programming_mode(radio)


def _read_block(radio, block_addr, block_size):
block_data = mml_jc8810._read_block(radio, block_addr, block_size)
if block_addr >= 0xf000:
return block_data
else:
return baofeng_uv17Pro._crypt(1, block_data)


def _write_block(radio, block_addr, block_size):
# Unfortunately we cannot use the original method as data is mmaped here
serial = radio.pipe

cmd = struct.pack(">cHb", b'W', block_addr, block_size)
data = radio.get_mmap()[block_addr:block_addr + block_size]
if block_addr < 0xf000:
data = baofeng_uv17Pro._crypt(1, data)

LOG.debug("Writing Data:")
LOG.debug(util.hexprint(cmd + data))

try:
serial.write(cmd + data)
if serial.read(1) != mml_jc8810.CMD_ACK:
raise Exception("No ACK")
except Exception:
raise errors.RadioError("Failed to send block "
"to radio at %04x" % block_addr)


def do_download(radio):
LOG.debug("download")
_enter_programming_mode(radio)

data = b""

status = chirp_common.Status()
status.msg = "Cloning from radio"

status.cur = 0
status.max = radio._memsize

for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
status.cur = addr + radio.BLOCK_SIZE
radio.status_fn(status)

block = _read_block(radio, addr, radio.BLOCK_SIZE)
data += block

LOG.debug("Address: %04x" % addr)
LOG.debug(util.hexprint(block))

_exit_programming_mode(radio)

return memmap.MemoryMapBytes(data)


def do_upload(radio):
status = chirp_common.Status()
status.msg = "Uploading to radio"

_enter_programming_mode(radio)

status.cur = 0
status.max = radio._memsize

for start_addr, end_addr in radio._ranges:
for addr in range(start_addr, end_addr, radio.BLOCK_SIZE_UP):
status.cur = addr + radio.BLOCK_SIZE_UP
radio.status_fn(status)
_write_block(radio, addr, radio.BLOCK_SIZE_UP)

_exit_programming_mode(radio)


@directory.register
class KDHUV110D(mml_jc8810.JC8810base):
"""KDH BT8000 / KSUN UV-110D"""
VENDOR = "KSUN"
MODEL = "UV-110D_BT"
BAUD_RATE = 57600

# ==========
# Notice to developers:
# The BT8000 support in this driver is currently based upon v0.05
# firmware.
# ==========

POWER_LEVELS = [chirp_common.PowerLevel("High", watts=8.00),
chirp_common.PowerLevel("Low", watts=1.00),
chirp_common.PowerLevel("Medium", watts=4.00)]

SKEY_LIST = ["FM Radio",
"TX Power Level",
"Scan",
"Search",
"NOAA Weather",
"SOS",
# "Flashlight", # Not used in BT model
]

AB = ['Off', 'A', 'B']

VALID_BANDS = [(108000000, 136000000), # AM mode
# (136000000, 174000000),
# (200000000, 260000000),
# (350000000, 390000000),
(136000000, 260000000),
(260000000, 330000000), # TX inhibited
(330000000, 520000000)]

self = (118000000, 136000000)

_has_bt_denoise = True
_has_am_switch = True

_magic = b"PROGRAMBT80U"
_fingerprint = [b"\x01\x36\x01\x80\x04\x00\x05\x20",
b"\x01\x00\x01\x80\x04\x00\x05\x20",
] # fw 0.0.5 and newer for BT80
_fingerprint2 = [b"\x02\x00\x02\x60\x01\x03\x30\x04",
] # fw 0.0.5 and newer for BT80
_cryptsetup = (b'SEND \x01\x01\x00\x00\x00\x00\x00\x00\x00\x00' +
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

_ranges = [
(0x0000, 0x2000),
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB440),
(0xD000, 0xD040) # Radio mode hidden setting
]
# Radio modes (in general) are: 0xff - default setting, 0 (?), 0xa5 (GMRS),
# 0x66 (PMR), 0x56 (Super mode), 0x28 (?)
_calibration = (0xF000, 0xF250) # Calibration data
_memsize = 0xF250 # Including calibration data
_aninames = 0

def sync_in(self):
"""Download from radio"""
try:
data = do_download(self)
except errors.RadioError:
# Pass through any real errors we raise
raise
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during download')
raise errors.RadioError('Unexpected error communicating '
'with the radio')
self._mmap = data
self.process_mmap()

def sync_out(self):
"""Upload to radio"""
try:
do_upload(self)
except Exception:
# If anything unexpected happens, make sure we raise
# a RadioError and log the problem
LOG.exception('Unexpected error during upload')
raise errors.RadioError('Unexpected error communicating '
'with the radio')

def remove_extras(self, settings):
for element in settings:
name = element.get_name()
if not isinstance(element, RadioSetting):
settings.remove(element)
if name in REMOVE_GROUPS:
LOG.debug("Deleting group: " + name + " from list")
else:
settings.append(self.remove_extra_items(element))
settings.sort()

def remove_extra_items(self, group):
tmp = RadioSettingGroup(group.get_name(), group.get_shortname())
for element in group:
name = element.get_name()
if not isinstance(element, RadioSetting):
if name in REMOVE_GROUPS:
LOG.debug("Deleting subgroup: " + name + " from list")
else:
tmp.append(self.remove_extra_items(element))
elif name not in REMOVE_SETTINGS:
tmp.append(element)
return tmp

def get_settings(self):
_settings = self._memobj.settings
_radio_mode = self._memobj.opmode.radio_mode
group = super().get_settings()
self.remove_extras(group)
spec = RadioSettingGroup("bt800", "Model Specific")

# UNLOCK SW CH - does it actually do anything?
# rs = RadioSettingValueBoolean(_settings.unlock_sw_ch)
# rset = RadioSetting("unlock_sw_ch",
# "Override KB Lock for Channel Keys (?)", rs)
# spec.append(rset)

if self.MODEL in "RT-900_BT":
# secret radio mode setting (no menu). PTT + 8 for Super Mode
rs = RadioSettingValueMap(self._RADIO_MODE_MAP, _radio_mode)
rset = RadioSetting("opmode.radio_mode", "Radio Mode", rs)
rset.set_warning(_(
'This should only be used to change the operating MODE of your '
'radio if you understand the legalities and implications of '
'doing so. The change may enable the radio to transmit on '
'frequencies it is not Type Accepted to do and my be in '
'violation of FCC and other governing agency regulations.\n\n'
'It may make your saved image files incompatible with the radio '
'and non-usable until you change the radio MODE back to the '
'MODE in effect when the image file was saved. After the '
'changed image is uploaded, the radio may have to turned OFF '
'and back ON to have the MODE changes take full effect.\n'
'DO NOT attempt to edit any settings until uploading to and '
'downloading from the radio with the new operating MODE.'))
spec.append(rset)

if self._has_bt_denoise:
# Menu 46: Noise reduction
rs = RadioSettingValueBoolean(_settings.unknown_9031)
rset = RadioSetting("unknown_9031", "Noise reduction", rs)
spec.append(rset)

# Menu 47: Bluetooth
rs = RadioSettingValueBoolean(_settings.unknown_901d)
rset = RadioSetting("unknown_901d", "Bluetooth", rs)
spec.append(rset)

# Menu 23: PF2 Short
rs = RadioSettingValueList(self.SKEY_LIST, current_index=_settings.ani)
rset = RadioSetting("ani", "PF2 Key (Short Press)", rs)
spec.append(rset)

# Menu 24: PF2 Long - one byte shift in settings data
rs = RadioSettingValueList(
self.SKEY_LIST, current_index=_settings.skey2_sp)
rset = RadioSetting("skey2_sp", "PF2 Key (Long Press)", rs)
spec.append(rset)

# Menu 25: PF3 Short
rs = RadioSettingValueList(
self.SKEY_LIST, current_index=_settings.skey2_lp)
rset = RadioSetting("skey2_lp", "PF3 Key (Short Press)", rs)
spec.append(rset)

if self.MODEL not in "RT-900_BT":
# Menu 50: AM/FM Mode
if self._has_am_switch:
rs = RadioSettingValueBoolean(_settings.skey3_lp)
rset = RadioSetting("skey3_lp", "AM Mode", rs)
spec.append(rset)
group.append(spec)

if self.MODEL in "RT-900_BT":
# Menu 7: TDR - Dual freq standby
rs = RadioSettingValueBoolean(_settings.tdr)
rset = RadioSetting("tdr", "TDR - Dual frequency standby", rs)
spec.append(rset)

# VFO A/B settings
abblock = RadioSettingGroup("abblock", "VFO A/B Channel")
spec.append(abblock)
vfo = self._memobj.vfo
# Menu 42: TX-A/B
rs = RadioSettingValueList(
self.AB, current_index=_settings.unknown_9018)
rset = RadioSetting("unknown_9018", "TX-A/B", rs)
abblock.append(rset)

# Menu 21: VFO A/B BCL (Busy lock)
rs = RadioSettingValueBoolean(_settings.unknown_900f)
rset = RadioSetting("unknown_900f", "VFO A/B Channel BCL", rs)
abblock.append(rset)

# VFO A channel sub menu
achannel = RadioSettingSubGroup("achannel", "VFO A Channel")
abblock.append(achannel)

# VFO A Freq
def freq_validate(value):
_radio_mode = self._memobj.opmode.radio_mode
_vhf_lower = 0.0
_uhf_upper = 0.0
if _radio_mode in [0x00, 0x56, 0xff]:
_vhf_lower = 18.0
else:
_vhf_lower = 108.0
_vhf_upper = 299.99875
_uhf_lower = 300.0
if _radio_mode in [0x00, 0x56]:
_uhf_upper = 999.99875
else:
_uhf_upper = 519.99875

value = chirp_common.parse_freq(value)
msg = ("Can't be less than %i.0000")
if value < _vhf_lower * 1000000:
raise InvalidValueError(msg % _vhf_lower)
msg = ("Can't be between %i.9975-%i.0000")
if _vhf_upper * 1000000 <= value and value < _uhf_lower * 1000000:
raise InvalidValueError(msg % (_vhf_upper - 1, _uhf_lower))
msg = ("Can't be greater than %i.9975")
if value > _uhf_upper * 1000000:
raise InvalidValueError(msg % (_uhf_upper - 1))

return chirp_common.format_freq(value)

def apply_freq(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 10
for i in range(7, -1, -1):
obj.freq[i] = value % 10
value /= 10

rs = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(vfo.a.freq))
rs.set_validate_callback(freq_validate)
rset = RadioSetting("vfo.a.freq", "Frequency", rs)
rset.set_apply_callback(apply_freq, vfo.a)
abblock.append(rset)

# Menu 01: A Step
rs = RadioSettingValueList(
self._step_list, current_index = vfo.a.step)
rset = RadioSetting("vfo.a.step", "Tuning Step", rs)
abblock.append(rset)

# Menu 02: TX Power
rs = RadioSettingValueList(
[str(x) for x in self.POWER_LEVELS],
current_index = vfo.a.txpower)
rset = RadioSetting("vfo.a.txpower", "TX Power", rs)
abblock.append(rset)

# Menu 05: Wide/Narrow Band
rs = RadioSettingValueList(
self._bandwidth_list, current_index = vfo.a.widenarr)
rset = RadioSetting("vfo.a.widenarr", "Bandwidth", rs)
abblock.append(rset)

def convert_bytes_to_offset(bytes):
real_offset = 0
for byte in bytes:
real_offset = (real_offset * 10) + byte
return chirp_common.format_freq(real_offset * 1000)

def apply_offset(setting, obj):
value = chirp_common.parse_freq(str(setting.value)) / 1000
for i in range(5, -1, -1):
obj.offset[i] = value % 10
value /= 10

# Menu 12,13: RX ctcss/dtsc
rs = RadioSettingValueList(
self._code_list, current_index = self._code_list.index(
self.decode(vfo.a.rxtone)))
rset = RadioSetting("vfo.a.rxtone", "RX CTCSS/DCS", rs)
abblock.append(rset)

# Menu 14,15: TX ctcss/dtsc
rs = RadioSettingValueList(
self._code_list, current_index = self._code_list.index(
self.decode(vfo.a.txtone)))
rset = RadioSetting("vfo.a.txtone", "TX CTCSS/DCS", rs)
abblock.append(rset)

# Menu 16: Voice Privacy (encryption)
rs = RadioSettingValueList(
self._voicepri_list, current_index = vfo.a.voicepri)
rset = RadioSetting("vfo.a.voicepri",
"Voice Privary - Subtone Encryption", rs)
abblock.append(rset)

# Menu 26: Offset
rs = RadioSettingValueString(
0, 10, convert_bytes_to_offset(vfo.a.offset))
rset = RadioSetting("vfo.a.offset", "Offset (MHz)", rs)
rset.set_apply_callback(apply_offset, vfo.a)
abblock.append(rset)

# Menu 27: Offset direction
rs = RadioSettingValueList(
self._offset_list, current_index = vfo.a.sftd)
rset = RadioSetting("vfo.a.sftd", "Offset Direction", rs)
abblock.append(rset)

# Menu 29: S-Code DTMF 1-15
rs = RadioSettingValueList(
self._scode_list, current_index = vfo.a.scode)
rset = RadioSetting("vfo.a.scode", "S-CODE", rs)
abblock.append(rset)

# Menu 45: Scramble (On/Off: bool)
rs = RadioSettingValueBoolean(vfo.a.scramble)
rset = RadioSetting("vfo.a.scramble", "Scramble", rs)
abblock.append(rset)

# Menu 50: RX Modulation
if self._has_am_switch:
rs = RadioSettingValueList(
self._rx_modulation_list, current_index = vfo.a.rxmod)
rset = RadioSetting("vfo.a.rxmod", "RX Modulation", rs)
abblock.append(rset)

# VFO B channel sub menu
bchannel = RadioSettingSubGroup("bchannel", "VFO B Channel")
abblock.append(bchannel)

# VFO B Freq
rs = RadioSettingValueString(0, 10,
bfc.bcd_decode_freq(vfo.b.freq))
rs.set_validate_callback(freq_validate)
rset = RadioSetting("vfo.b.freq", "Frequency", rs)
rset.set_apply_callback(apply_freq, vfo.b)
abblock.append(rset)

# Menu 01: B Step
rs = RadioSettingValueList(
self._step_list, current_index = vfo.b.step)
rset = RadioSetting("vfo.b.step", "Tuning Step", rs)
abblock.append(rset)

# Menu 02: TX Power
rs = RadioSettingValueList(
[str(x) for x in self.POWER_LEVELS],
current_index = vfo.b.txpower)
rset = RadioSetting("vfo.b.txpower", "TX Power", rs)
abblock.append(rset)

# Menu 05: Wide/Narrow Band
rs = RadioSettingValueList(
self._bandwidth_list, current_index = vfo.b.widenarr)
rset = RadioSetting("vfo.b.widenarr", "Bandwidth", rs)
abblock.append(rset)

# Menu 12,13: RX ctcss/dtsc
rs = RadioSettingValueList(
self._code_list, current_index = self._code_list.index(
self.decode(vfo.b.rxtone)))
rset = RadioSetting("vfo.b.rxtone", "RX CTCSS/DCS", rs)
abblock.append(rset)

# Menu 14,15: TX ctcss/dtsc
rs = RadioSettingValueList(
self._code_list, current_index = self._code_list.index(
self.decode(vfo.b.txtone)))
rset = RadioSetting("vfo.b.txtone", "TX CTCSS/DCS", rs)
abblock.append(rset)

# Menu 16: Voice Privacy (encryption)
rs = RadioSettingValueList(
self._voicepri_list, current_index = vfo.b.voicepri)
rset = RadioSetting("vfo.b.voicepri",
"Voice Privary - Subtone Encryption", rs)
abblock.append(rset)

# Menu 26: Offset
rs = RadioSettingValueString(
0, 10, convert_bytes_to_offset(vfo.b.offset))
rset = RadioSetting("vfo.b.offset", "Offset (MHz)", rs)
rset.set_apply_callback(apply_offset, vfo.b)
abblock.append(rset)

# Menu 27: Offset direction
rs = RadioSettingValueList(
self._offset_list, current_index = vfo.b.sftd)
rset = RadioSetting("vfo.b.sftd", "Offset Direction", rs)
abblock.append(rset)

# Menu 29: S-Code DTMF 1-15
rs = RadioSettingValueList(
self._scode_list, current_index = vfo.b.scode)
rset = RadioSetting("vfo.b.scode", "S-CODE", rs)
abblock.append(rset)

# Menu 45: Scramble (On/Off: bool)
rs = RadioSettingValueBoolean(vfo.b.scramble)
rset = RadioSetting("vfo.b.scramble", "Scramble", rs)
abblock.append(rset)

# Menu 50: RX Modulation
if self._has_am_switch:
rs = RadioSettingValueList(
self._rx_modulation_list, current_index = vfo.b.rxmod)
rset = RadioSetting("vfo.b.rxmod", "RX Modulation", rs)
abblock.append(rset)

return group

def get_memory(self, number):
mem = super().get_memory(number)
if mem.freq == 0:
mem.empty = True
return mem

def process_mmap(self):
mem_format = mml_jc8810.MEM_FORMAT % self._mem_params + \
MEM_FORMAT_KDH110D
self._memobj = bitwise.parse(mem_format, self._mmap)


@directory.register
class RT900BT(KDHUV110D):
# ==========
# Notice to developers:
# The RT-900 BT support in this driver is currently based upon v1.15
# firmware.
# ==========

"""Radtel RT-900 BT"""
VENDOR = "Radtel"
MODEL = "RT-900_BT"
BAUD_RATE = 57600
_AIRBAND = (108000000, 136975000)
_MIL_AIRBAND = (225000000, 399950000)
_AIRBANDS = [_AIRBAND] + [_MIL_AIRBAND]
_RADIO_MODE_MAP = [("Default", 0xff), ("GMRS", 0xa5), \
("PMR", 0x66), ("Super", 0x56), \
("unknown Mode 1", 0x00), ("unknown Mode 2", 0x28)]
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=8.00),
chirp_common.PowerLevel("Middle", watts=4.00),
chirp_common.PowerLevel("Low", watts=1.00)]

_upper = 512 # fw 1.06_512 expands from 256 to 512 channels
_mem_params = (_upper, # number of channels
mml_jc8810.JC8810base._aninames, # number of aninames
)
_ranges = [
(0x0000, 0x4000),
(0x8000, 0x8040),
(0x9000, 0x9040),
(0xA000, 0xA140),
(0xB000, 0xB440),
(0xD000, 0xD040) # Radio mode hidden setting
]

_steps = [2.5, 5.0, 6.25, 12.5, 20.0, 25.0, 50.0, 8.33]
_step_list = ['%#.3g K' % x for x in _steps]
_bandwidth_list = ["Wide (25 KHz)", "Narrow (12.5 KHz)"]
_offset_list = ["Off", "+", "-"]
_rx_modulation_list = ["FM", "AM"]
_scode_list = ["%s" % x for x in range(1, 16)]
_voicepri_list = ["Off", "ENCRY1", "ENCRY2", "ENCRY3"]

_code_list_ctcss = ["%2.1fHz" % x for x in sorted(chirp_common.TONES)]
_code_list_ctcss.insert(0, "Off")
_dcs = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
_code_list_dcsn = ["D%03iN" % x for x in _dcs]
_code_list_dcsi = ["D%03iI" % x for x in _dcs]
_code_list = _code_list_ctcss + _code_list_dcsn + _code_list_dcsi

def decode(self, code):
if code in [0, 0xffff]:
tone = 'Off' # Off
elif code >= 0x0258: # CTCSS
tone = "%2.1fHz" % (int(code) / 10.0)
elif code <= 0x0258: # DCS
if code > 0x69: # inverse
index = code - 0x6a
dtcs_pol = 'I'
else: # normal
index = code - 1
dtcs_pol = 'N'
tone = 'D' + "%03i" % (self._dcs[index]) + dtcs_pol
else:
msg = "Invalid tone code from radio: %s" % hex(code)
LOG.exception(msg)
raise InvalidValueError(msg)

return tone

def encode(self, tone):
if tone == "Off":
code = 0
elif tone.endswith('Hz'): # CTCSS
code = int(float(tone[0:tone.index('Hz')]) * 10)
elif tone.startswith('D'): # DCS
index = self._dcs.index(int(tone[1:4]))
if tone.endswith('I'): # inverse
code = index + 0x6a
elif tone.endswith('N'): # normal
code = index + 1
else:
msg = "Unknown CTCSS/DTC tone: %s" % tone
LOG.exception(msg)
raise InvalidValueError(msg)
return code

def get_features(self):
rf = super().get_features()
rf.valid_bands = [(18000000, 1000000000)]
rf.valid_modes = ["FM", "NFM", "AM", "NAM"] # 25 kHz, 12.5 kHz, AM, NAM
rf.valid_tuning_steps = self._steps
return rf

def get_memory(self, number):
mem = super().get_memory(number)
_mem = self._memobj.memory[mem.number - 1]
if chirp_common.in_range(mem.freq, self._AIRBANDS) or \
_mem.unknown5 == 0b1:
mem.mode = _mem.narrow and "NAM" or 'AM'
mem.duplex = 'off' # AM mode is RX Only

# base class swaps these so swap Middle and Low back to match radio
if _mem.txpower == 1:
mem.power = self.POWER_LEVELS[1]
elif _mem.txpower == 2:
mem.power = self.POWER_LEVELS[2]
return mem

def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
super().set_memory(mem)

match mem.mode:
case 'AM':
_mem.unknown5 = 0b1
_mem.narrow = 0b0
case "NAM":
_mem.unknown5 = 0b1
_mem.narrow = 0b1
case 'FM':
_mem.unknown5 = 0b0
_mem.narrow = 0b0
case 'NFM':
_mem.unknown5 = 0b0
_mem.narrow = 0b1

# base class swaps these so swap Middle and Low back to match radio
if mem.power == self.POWER_LEVELS[1]:
_mem.txpower = 1
elif mem.power == self.POWER_LEVELS[2]:
_mem.txpower = 2

def validate_memory(self, mem):
msgs = []
in_range = chirp_common.in_range
AM_mode = 'AM' in mem.mode

if in_range(mem.freq, self._AIRBANDS) and not AM_mode:
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range requires AM mode')))
elif not in_range(mem.freq, self._AIRBANDS) and AM_mode:
msgs.append(chirp_common.ValidationWarning(
_('Frequency in this range should NOT be AM mode')))
return msgs + super().validate_memory(mem)

def set_settings(self, settings):
_settings = self._memobj.settings
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
else:
try:
name = element.get_name()
if "." in name:
bits = name.split(".")
obj = self._memobj
for bit in bits[:-1]:
if "/" in bit:
bit, index = bit.split("/", 1)
index = int(index)
obj = getattr(obj, bit)[index]
else:
obj = getattr(obj, bit)
setting = bits[-1]
else:
obj = _settings
setting = element.get_name()
if element.has_apply_callback():
LOG.debug("Using apply callback")
element.run_apply_callback()
elif setting == "fmradio":
setattr(obj, setting, not int(element.value))
elif setting in ["rxtone", "txtone"]:
setattr(obj, setting,
self.encode(self._code_list[int(element.value)]))
elif element.value.get_mutable():
LOG.debug("Setting %s = %s" % (setting, element.value))
setattr(obj, setting, element.value)
except Exception as e:
LOG.debug(element.get_name(), e)
raise


@directory.register
class RT900(RT900BT):
# ==========
# Notice to developers:
# The BT8000 support in this driver is currently based upon stock
# factory firmware.
# ==========

"""Radtel RT-900 (without Bluetooth)"""
VENDOR = "Radtel"
MODEL = "RT-900"
BAUD_RATE = 57600
_has_bt_denoise = False
#_has_am_switch = False # Stock firmware did not have yet - now has
#_upper = 512 # fw 1.04P expands from 256 to 512 channels

SKEY_LIST = KDHUV110D.SKEY_LIST + ["Flashlight"]

def get_features(self):
rf = super().get_features()
# So far no firmware update to test new range availability yet
# rf.valid_bands = [(18000000, 1000000000)]
return rf
(10-10/11)