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