Project

General

Profile

New Model #11854 » radtel_rt493.py

Paul O., 02/25/2025 01:12 PM

 
# Copyright 2025 Paul Oberosler <paul@paulober.dev>
#
# 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 struct
import logging
from chirp import bitwise, chirp_common, directory, errors, memmap, util
from chirp.settings import (
RadioSetting,
RadioSettings,
RadioSettingGroup,
RadioSettingValueBoolean,
RadioSettingValueList,
RadioSettingValueInteger,
RadioSettingValueString
)


LOG = logging.getLogger(__name__)

COMMAND_ACCEPT = b"\x06"
CHARSET_HEX = "0123456789ABCDEFabcdef"


def _clean_buffer(radio):
radio.pipe.timeout = 0.005
junk = radio.pipe.read(256)
radio.pipe.timeout = 1 # 1000ms

if junk:
LOG.debug("Got %i bytes of junk before starting" % len(junk))


def _recv(radio, addr, length):
"""Get data from the radio """
hdr = radio.pipe.read(4) # Read 4 byte header
if not hdr:
raise errors.RadioNoContactLikelyK1()
data = radio.pipe.read(length) # Read the data
if not data:
raise errors.RadioNoContactLikelyK1()

# DEBUG
LOG.info("Response:")
LOG.debug(util.hexprint(hdr + data))

c, a, resp_length = struct.unpack(">BHB", hdr)
if a != int.from_bytes(addr, byteorder="big") \
or resp_length != length or c != ord("W"):
LOG.error("Invalid answer for block 0x%04s:" % util.hexprint(addr))
LOG.debug("CMD: %s ADDR: %04x SIZE: %02x" % (c, a, resp_length))
raise errors.RadioError("Unknown response from the radio")

return data


def _enter_programming_mode(radio):
_clean_buffer(radio)

radio.pipe.write(radio._magic)
LOG.debug("Sent magic sequence")
ack = radio.pipe.read(2)
if not ack:
raise errors.RadioNoContactLikelyK1()
LOG.debug("Received magic sequence response")
if len(ack) != 2 or ack[1:2] != COMMAND_ACCEPT:
if ack:
LOG.error("Received: Len=%i Data=%s"
% (len(ack), util.hexprint(ack)))
raise errors.RadioError("Radio refused to enter programming mode")

radio.pipe.write(b"\x02")
ident = radio.pipe.read(radio._magic_response_length)
if not ident:
raise errors.RadioNoContactLikelyK1()
elif not ident.startswith(radio._fingerprint):
raise errors.RadioError("Radio returned unknown identification string")

LOG.info("Radio entered programming mode")
LOG.debug("Radio identification: %s" % util.hexprint(ident))

radio.pipe.write(COMMAND_ACCEPT)
ack = radio.pipe.read(1)
if not ack:
raise errors.RadioNoContactLikelyK1()
elif ack != COMMAND_ACCEPT:
if ack:
LOG.error("Got %s" % util.hexprint(ack))
raise errors.RadioError("Radio refused to enter programming mode")

return ident


def _exit_programming_mode(radio):
try:
radio.pipe.write(b"\x45")
except Exception:
raise errors.RadioError("Radio refused to exit programming mode")


def _get_memory_address(channel_id):
block = (channel_id - 1) // 16 # Determines high byte
offset = ((channel_id - 1) % 16) * 0x10 # Determines low byte

return (block, offset)


def _read_block(radio, channel_id):
address = bytes(_get_memory_address(channel_id))
# 52=read | 20 = hex(32 bytes) = length of 2 channels
# + settings without header
cmd = struct.pack(">BBBB", 0x52, address[0], address[1], 0x20)

radio.pipe.write(cmd)
data = _recv(radio, address, 32)

# Sending an accept after reading the first line will crash
# the communication. The accept is sent when entering programming
# mdoe, so before reading the first block
if address != b"\x00\x00":
radio.pipe.write(COMMAND_ACCEPT)
response = radio.pipe.read(1)
if not response:
raise errors.RadioNoContactLikelyK1()
elif response != COMMAND_ACCEPT:
raise errors.RadioError("Radio refused to read block at %04s"
% util.hexprint(address))

return data


def _write_block(radio, channel_id, data):
address = bytes(_get_memory_address(channel_id))
cmd = struct.pack(">BBBB", 0x57, address[0], address[1], 0x10) + data

radio.pipe.write(cmd)
response = radio.pipe.read(1)
if not response:
raise errors.RadioNoContactLikelyK1()
elif response != COMMAND_ACCEPT:
raise errors.RadioError("Radio refused to write block at %04s"
% util.hexprint(address))


def do_download(radio):
"""Expects caller to exit programming mode"""

LOG.debug("Downloading data from radio")

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

status.cur = 0
status.max = radio._upper
radio.status_fn(status)

data = bytearray()

_enter_programming_mode(radio)
for i in range(1, radio.MEM_ROWS, 2):
status.cur = i
radio.status_fn(status)

result = _read_block(radio, i)
data.extend(result)

LOG.debug("Downloaded memory channel %i" % i)

LOG.debug("Downloaded %i bytes of data" % len(data))

return bytes(data)


def do_upload(radio):
"""Expects caller to exit programming mode"""

LOG.debug("Uploading data to radio")
_enter_programming_mode(radio)

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

status.cur = 0
status.max = radio._upper
radio.status_fn(status)

radio_mem = radio.get_mmap()
for i in range(1, radio.MEM_ROWS, 1):
status.cur = i
radio.status_fn(status)

block_offset = (i - 1) * radio.BLOCK_SIZE
block = radio_mem[block_offset:block_offset + radio.BLOCK_SIZE]
_write_block(radio, i, block)

LOG.debug("Uploaded memory channel %i" % i)


DTCS_BYTE_LOOKUP_LOW = {
0x13: 0, 0x15: 1, 0x16: 2, 0x19: 3, 0x1A: 4, 0x1E: 5,
0x23: 6, 0x27: 7, 0x29: 8, 0x2B: 9, 0x2C: 10, 0x35: 11,
0x39: 12, 0x3A: 13, 0x3B: 14, 0x3C: 15, 0x4C: 16, 0x4D: 17,
0x4E: 18, 0x52: 19, 0x55: 20, 0x59: 21, 0x5A: 22, 0x5C: 23,
0x63: 24, 0x65: 25, 0x6A: 26, 0x6D: 27, 0x6E: 28, 0x72: 29,
0x75: 30, 0x7A: 31, 0x7C: 32, 0x85: 33, 0x8A: 34, 0x93: 35,
0x95: 36, 0x96: 37, 0xA3: 38, 0xA4: 39, 0xA5: 40, 0xA6: 41,
0xA9: 42, 0xAA: 43, 0xAD: 44, 0xB1: 45, 0xB3: 46, 0xB5: 47,
0xB6: 48, 0xB9: 49, 0xBC: 50, 0xC6: 51, 0xC9: 52, 0xCD: 53,
0xD5: 54, 0xD9: 55, 0xDA: 56, 0xE3: 57, 0xE6: 58, 0xE9: 59,
0xEE: 60, 0xF4: 61, 0xF5: 62, 0xF9: 63
}

DTCS_BYTE_LOOKUP_HIGH = {
0x09: 64, 0x0A: 65, 0x0B: 66, 0x13: 67, 0x19: 68, 0x1A: 69,
0x25: 70, 0x26: 71, 0x2A: 72, 0x2C: 73, 0x2D: 74, 0x32: 75,
0x34: 76, 0x35: 77, 0x36: 78, 0x43: 79, 0x46: 80, 0x4E: 81,
0x53: 82, 0x56: 83, 0x5A: 84, 0x66: 85, 0x75: 86, 0x86: 87,
0x8A: 88, 0x94: 89, 0x97: 90, 0x99: 91, 0x9A: 92, 0xA5: 93,
0xAC: 94, 0xB2: 95, 0xB4: 96, 0xC3: 97, 0xCA: 98, 0xD3: 99,
0xD9: 100, 0xDA: 101, 0xDC: 102, 0xE3: 103, 0xEC: 104,
}


# Reverse lookup for encoding
INDEX_TO_DTCS_BYTE_LOW = {v: k for k, v in DTCS_BYTE_LOOKUP_LOW.items()}
INDEX_TO_DTCS_BYTE_HIGH = {v: k for k, v in DTCS_BYTE_LOOKUP_HIGH.items()}


def _decode_dtcs_byte(byte: int, level: int) -> int:
"""
Translates a DTCS byte to an index (starting from 0).

:param byte: The first byte of the DTCS value.
:param level: The DTCS level / second byte (0x28 or 0x29).
:return: The corresponding index (0-based) or -1 if not found.
"""
if level == 0x28 or level == 0xA8:
return DTCS_BYTE_LOOKUP_LOW.get(byte, -1)
elif level == 0x29 or level == 0xA9:
return DTCS_BYTE_LOOKUP_HIGH.get(byte, -1)
else:
return -1


def _encode_dtcs_byte(index: int) -> int:
"""
Translates an index back to a DTCS first byte.

:param index: The index to look up.
:return: The corresponding DTCS first byte, or 0xFF if not found.
"""
if index <= 63:
return INDEX_TO_DTCS_BYTE_LOW.get(index, 0xFF)
elif index <= 104:
return INDEX_TO_DTCS_BYTE_HIGH.get(index, 0xFF)
else:
return 0xFF


@directory.register
class Radtel493Radio(chirp_common.CloneModeRadio):
"""Acme Template"""
VENDOR = "Radtel"
MODEL = "RT-493"
BAUD_RATE = 9600

# All new drivers should be "Byte Clean" so leave this in place.

# sometimes second last bute is 06 sometimes 05
_fingerprint = b"\x50\x33\x32\x30\x37\x33" # + \x05\xff or + \x06\xff
_magic = b"\x50\x48\x4f\x47\x52\x89\x83"
_magic_response_length = 8
_upper = 199

BLOCK_SIZE = 16
MEM_ROWS = 256

# NOTE: wats are randomly chosen for now to allow comparison
POWER_LEVELS = [
chirp_common.PowerLevel("High", watts=10.0),
chirp_common.PowerLevel("Low", watts=5.0),
]

DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))

MEM_FORMAT = """
struct {
lbcd rxfreq[4]; // RX Frequency (BCD format)
lbcd txfreq[4]; // TX Frequency (BCD format)
ul16 rxtone; // RX Tone (CTCSS/DCS) - 0xFFFF if OFF
ul16 txtone; // TX Tone (CTCSS/DCS) - 0xFFFF if OFF
u8 compander:1, // Compander (1=ON, 0=OFF)
special_qt_dqt:2, // Special QT/DQT (00=OFF, 01=A, 10=B)
wn_mode:1, // Wide/Narrow mode (1=Wide, 0=Narrow)
unknown1:1, // Unknown bit
scramble:3; // Scramble mode (0-7, 0 = OFF)
u8 global1; // Global settings byte 1 (varies per channel)
u8 global2; // Global settings byte 2
u8 global3; // Global settings byte 3
} memory[199];

struct {
ul32 special_code[4]; // Special Codes for 4 channels per row
} special_codes[50]; // Covers 199 channels (50 * 4 = 200,
// last ignored / unused)

struct {
u8 scan_add1[16]; // "Scan Add" settings for channels 1-128
u8 scan_add2[16]; // "Scan Add" settings for channels 129-199
} scan_add_settings;

struct {
u8 learn_code1[16]; // "Learn Code" settings for channels 1-128
u8 learn_code2[16]; // "Learn Code" settings for channels 129-199
} learn_code_settings;

struct {
u8 power_level1[16]; // Power level settings (1-128)
u8 power_level2[16]; // Power level settings (129-199)
} power_settings;

u8 unknown1[16]; // Unknown data
u8 unknown2[16]; // Unknown data
"""

# Return information about this radio's features, including
# how many memories it has, what bands it supports, etc
def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = False
rf.has_rx_dtcs = True
rf.has_dtcs = True
rf.has_ctone = True
rf.has_dtcs_polarity = True
rf.has_name = False
rf.can_odd_split = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_variable_power = False
rf.has_offset = True
rf.has_mode = False

rf.valid_modes = ["NFM", "WFM"]
rf.valid_duplexes = ["", "-", "+", "split"]
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
rf.valid_skips = ["", "S"]
rf.valid_power_levels = self.POWER_LEVELS
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS",
# "Tone->"
]
rf.valid_dtcs_codes = self.DTCS_CODES
rf.memory_bounds = (1, 199)
# TODO: maybe limit to EU version (430000000, 440000000)
rf.valid_bands = [(400000000, 470000000)]
return rf

def _decode_qt_dqt(self, qt_bytes):
assert len(qt_bytes) == 2

value = int.from_bytes(qt_bytes, "little")
byte1 = qt_bytes[0]
byte2 = qt_bytes[1]

if value == 0xFFFF or value == 0x0000:
return None, None, None
elif byte2 <= 9:
if value / 10 in chirp_common.TONES:
return "Tone", value / 10, None
else:
raise ValueError("Invalid tone value: %i" % value)

# DTCS Normal Polarity (DxxxN) and DTCS Inverted Polarity (DxxxI)
dtcs_code_idx = _decode_dtcs_byte(byte1, byte2)
assert dtcs_code_idx != -1
dtcs_polarity = "N" if byte2 == 0x28 or byte2 == 0x29 else "R"
if 0 <= dtcs_code_idx <= len(self.DTCS_CODES) - 1:
return "DTCS", self.DTCS_CODES[dtcs_code_idx], dtcs_polarity
else:
return None, None, None

def get_scan_add_status(self, channel):
"""
Get the Scan Add setting for a given channel number.

Args:
channel (int): The channel number (1-199).

Returns:
True -> If the channel is included in the scan list.
False -> If the channel is excluded from the scan list.
"""
if not 1 <= channel <= 199:
raise ValueError("Channel number must be between 1 and 199")

raw_bytes = self._memobj.scan_add_settings.get_raw()

# Each byte represents 8 channels, find the correct byte and bit
byte_index = (channel - 1) // 8
bit_index = (channel - 1) % 8

# Check if the bit for the channel is set (1 = Add, 0 = Del)
return bool(raw_bytes[byte_index] & (1 << bit_index))

def set_scan_add_status(self, channel, status):
"""
Set the Scan Add setting for a given channel number.

Args:
channel (int): The channel number (1-199).
status (bool): "Add" to include in scan list, "Del"
to remove from scan list.

Raises:
ValueError: If channel is out of range.
"""
if not 1 <= channel <= 199:
raise ValueError("Channel number must be between 1 and 199")

# Fetch current scan add settings as raw bytes
scan_add_data = self._memobj.scan_add_settings
raw_bytes = bytearray(scan_add_data.get_raw())

# Determine byte and bit position
byte_index = (channel - 1) // 8
bit_index = (channel - 1) % 8

# Modify the bit based on the status
if status:
raw_bytes[byte_index] |= (1 << bit_index) # Set bit to 1
else:
raw_bytes[byte_index] &= ~(1 << bit_index) # Clear bit to 0

# Apply the new settings back to memory
scan_add_data.set_raw(bytes(raw_bytes))

def get_learn_code_status(self, channel):
"""
Get the Learn Code setting for a given channel number.

Returns:
True -> If Learn Code is enabled.
False -> If Learn Code is disabled.

Raises:
ValueError: If channel is out of range.
"""
if not 1 <= channel <= 199:
raise ValueError("Channel number must be between 1 and 199")

# Get Learn Code memory
raw_bytes = self._memobj.learn_code_settings.get_raw()

# Calculate byte and bit index
byte_index = (channel - 1) // 8
bit_index = (channel - 1) % 8

# Extract the relevant bit
return bool(raw_bytes[byte_index] & (1 << bit_index))

def set_learn_code_status(self, channel, status):
"""
Set the Learn Code setting for a given channel number.

Args:
channel (int): The channel number (1-199).
status (bool): True="ON" to enable Learn Code,
False="OFF" to disable.

Raises:
ValueError: If channel is out of range.
"""
if not 1 <= channel <= 199:
raise ValueError("Channel number must be between 1 and 199")

# Fetch Learn Code settings as raw bytes
learn_code_data = self._memobj.learn_code_settings
raw_bytes = bytearray(learn_code_data.get_raw())

# Determine byte and bit position
byte_index = (channel - 1) // 8
bit_index = (channel - 1) % 8

# Modify the bit based on the status
if status:
raw_bytes[byte_index] |= (1 << bit_index) # Set bit to 1
else:
raw_bytes[byte_index] &= ~(1 << bit_index) # Clear bit to 0

# Apply the new settings back to memory
learn_code_data.set_raw(bytes(raw_bytes))

def get_power_level(self, channel: int):
"""Get the power level setting for a given channel.
(True = High, False = Low)"""
if not (1 <= channel <= 199):
raise ValueError("Channel must be between 1 and 199")

index = (channel - 1) // 8
bit_pos = (channel - 1) % 8

raw_bytes = self._memobj.power_settings.get_raw()

return bool((raw_bytes[index] & (1 << bit_pos)))

def set_power_level(self, channel: int, level: bool):
"""Set the power level for a given channel
(True='High' or False='Low')."""
if not (1 <= channel <= 199):
raise ValueError("Channel must be between 1 and 199")

index = (channel - 1) // 8
bit_pos = (channel - 1) % 8

if channel <= 128:
current_byte = self._memobj.power_settings.power_level1[index]
else:
current_byte = self._memobj.power_settings \
.power_level2[index - 16]

# Modify the bit
if level:
new_byte = current_byte | (1 << bit_pos) # Set bit
else:
new_byte = current_byte & ~(1 << bit_pos) # Clear bit

# Write back the new byte
if channel <= 128:
self._memobj.power_settings.power_level1[index] = new_byte
else:
self._memobj.power_settings.power_level2[index - 16] = new_byte

def decode_mem_blob(self, _mem, mem):
if _mem.rxfreq[0].get_raw() == b'\xff' \
or _mem.rxfreq[3].get_raw() == b'\x00':
mem.empty = True
return

mem.freq = int(_mem.rxfreq) * 10
chirp_common.split_to_offset(mem, int(_mem.rxfreq), int(_mem.txfreq))
txtone = self._decode_qt_dqt(_mem.txtone.get_raw())
rxtone = self._decode_qt_dqt(_mem.rxtone.get_raw())
chirp_common.split_tone_decode(mem, txtone, rxtone)

# main_settings_byte = _mem.settings.get_raw()
# main_settings = _decode_channel_settings(main_settings_byte)
mem.mode = "WFM" if int(_mem.wn_mode) else "NFM"

power_level = self.get_power_level(mem.number)
mem.power = self.POWER_LEVELS[0 if power_level else 1]

scan_add = self.get_scan_add_status(mem.number)
if not scan_add:
mem.skip = "S"

mem.extra = RadioSettingGroup("Extra", "extra")

rs = RadioSetting(
"Compander", "Compander",
RadioSettingValueBoolean(_mem.compander))
mem.extra.append(rs)

rs = RadioSetting(
"Scramble Mode", "Scramble Mode",
RadioSettingValueList(
["OFF", "1", "2", "3", "4", "5", "6", "7"],
current_index=_mem.scramble
))
mem.extra.append(rs)

rs = RadioSetting(
"Special QT/DQT", "Special QT/DQT",
RadioSettingValueList(
["OFF", "A", "B"],
current_index=0 if _mem.special_qt_dqt == 0x00
else (1 if _mem.special_qt_dqt == 0x01 else 2)
))
mem.extra.append(rs)

LOG.debug("WN Mode: %i, Compander: %i, Scramble: %i, Special QT/DQT: %i" % (
int(_mem.wn_mode), int(_mem.compander), int(_mem.scramble),
int(_mem.special_qt_dqt)))

learn_code = self.get_learn_code_status(mem.number)
rs = RadioSetting(
"Learn Code", "Learn Code",
RadioSettingValueBoolean(learn_code))
mem.extra.append(rs)

special_code = self.get_special_code(mem.number)
rs = RadioSetting(
"Special Code", "Special Code",
RadioSettingValueString(
8, 8, special_code, True, CHARSET_HEX, "0"))
mem.extra.append(rs)

def get_memory(self, number):
if number <= 0 or number > self._upper:
raise errors.InvalidMemoryLocation(
"Number must be between 1 and %i (included)" % self._upper)

_mem = self._memobj.memory[number-1]

mem = chirp_common.Memory()
mem.number = number

self.decode_mem_blob(_mem, mem)

return mem

def _encode_qt_dqt(self, tone_mode, tone_value=None, tone_polarity=None):
if tone_mode == "Tone" or tone_mode == "TSQL":
assert tone_value is not None
return int(tone_value * 10).to_bytes(2, "little") # Encode as Hz

elif tone_mode == "DTCS":
assert tone_value is not None
if tone_polarity not in ("N", "R"):
raise ValueError("Invalid DTCS polarity. Must be 'N' or 'R'.")
dtcs_idx = self.DTCS_CODES.index(tone_value)
dtcs_code = _encode_dtcs_byte(dtcs_idx)
dtcs_polarity_bit = (0x28 if dtcs_idx <= 63 else 0x29) \
if tone_polarity == "N" else (
0xA8 if dtcs_idx <= 63 else 0xA9)
return bytes([dtcs_code, dtcs_polarity_bit]) # Encode

else:
return b"\xFF\xFF"

def encode_mem_blob(self, mem, _mem):
_mem.rxfreq = mem.freq / 10

if mem.duplex == "split":
_mem.txfreq = mem.offset / 10
elif mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
else:
_mem.txfreq = _mem.rxfreq

((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)

tx_value = self._encode_qt_dqt(txmode, txtone, txpol)
rx_value = self._encode_qt_dqt(rxmode, rxtone, rxpol)

_mem.rxtone.set_raw(rx_value)
_mem.txtone.set_raw(tx_value)

# maybe they have been edited while mem was in UI
memraw = self._memobj.memory[mem.number - 1]
global1 = memraw.global1.get_raw()
global2 = memraw.global2.get_raw()
global3 = memraw.global3.get_raw()

learn_code = False
special_code = "FF" * 4
for setting in mem.extra:
if setting.get_name() == "Compander":
_mem.compander = int(setting.value)
elif setting.get_name() == "Scramble Mode":
_mem.scramble = int(setting.value) if str(
setting.value) != "OFF" else 0
elif setting.get_name() == "Special QT/DQT":
_mem.special_qt_dqt = [0x00, 0x01, 0x10][int(setting.value)]
elif setting.get_name() == "Learn Code":
learn_code = bool(setting.value)
elif setting.get_name() == "Special Code":
special_code = str(setting.value)

_mem.wn_mode = 1 if mem.mode == "Wide" else 0

_mem.global1.set_raw(global1)
_mem.global2.set_raw(global2)
_mem.global3.set_raw(global3)

# Settings that are indepently stored from the memory channel
self.set_scan_add_status(mem.number, mem.skip != "S")
self.set_learn_code_status(mem.number, learn_code)
self.set_power_level(mem.number, mem.power == self.POWER_LEVELS[0])
self.set_special_code(mem.number, special_code)

def get_special_code(self, channel):
"""
Get the special code for a given channel.

Args:
channel (int): The channel number (1-199).

Returns:
str: The special code (8 characters long).
"""
if not 1 <= channel <= 199:
raise ValueError("Channel number must be between 1 and 199")

special_code = self._memobj.special_codes[(channel - 1) // 4] \
.special_code[(channel - 1) % 4].get_raw()
return special_code.hex().upper()

def set_special_code(self, channel, code):
"""
Set the special code for a given channel.

Args:
channel (int): The channel number (1-199).
code (str): The special code (8 characters long).

Raises:
ValueError: If channel is out of range or code is
not 8 characters long.
"""
if not 1 <= channel <= 199:
raise ValueError("Channel number must be between 1 and 199")

if len(code) != 8:
raise ValueError("Special code must be exactly 8 characters long")

# Get the special code memory
special_code = self._memobj.special_codes[(channel - 1) // 4] \
.special_code[(channel - 1) % 4]
special_code.set_raw(bytes.fromhex(code))

# 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):
_mem = self._memobj.memory[mem.number - 1]

if mem.empty:
_mem.set_raw(b"\xff" * 16)
# Also reset all externally stored settings
self.set_power_level(mem.number, False)
self.set_scan_add_status(mem.number, False)
self.set_learn_code_status(mem.number, False)
self.set_special_code(mem.number, "FF" * 4)
return

self.encode_mem_blob(mem, _mem)

def _get_bool(self, setting):
if setting == "BATS":
byte1 = self._memobj.memory[3-1].global1.get_raw()

return bool(int.from_bytes(byte1, byteorder="big") & 0x08)
elif setting == "BEEP":
byte1 = self._memobj.memory[3-1].global1.get_raw()

return bool(int.from_bytes(byte1, byteorder="big") & 0x04)
elif setting == "WARN":
byte1 = self._memobj.memory[7-1].global1.get_raw()

return bool(int.from_bytes(byte1, byteorder="big") & 0x01)
elif setting == "SCAN":
byte2 = self._memobj.memory[7-1].global2.get_raw()

return bool(int.from_bytes(byte2, byteorder="big") & 0x01)
elif setting == "CPYC":
byte1 = self._memobj.memory[6-1].global3.get_raw()

return bool(int.from_bytes(byte1, byteorder="big") & 0x01)
elif setting == "VOXF":
byte1 = self._memobj.memory[4-1].global1.get_raw()

return bool(int.from_bytes(byte1, byteorder="big") & 0x01)

def _set_bool(self, setting, value):
if setting == "BATS":
byte = self._memobj.memory[3-1].global1
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value:
byte.set_raw((num | 0x08).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x08).to_bytes(1, byteorder="big"))
elif setting == "BEEP":
byte = self._memobj.memory[3-1].global1
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value:
byte.set_raw((num | 0x04).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x04).to_bytes(1, byteorder="big"))
elif setting == "WARN":
byte = self._memobj.memory[7-1].global1
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value:
byte.set_raw((num | 0x01).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x01).to_bytes(1, byteorder="big"))
elif setting == "SCAN":
byte = self._memobj.memory[7-1].global2
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value:
byte.set_raw((num | 0x01).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x01).to_bytes(1, byteorder="big"))
elif setting == "CPYC":
byte = self._memobj.memory[6-1].global1
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value:
byte.set_raw((num | 0x01).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x01).to_bytes(1, byteorder="big"))
elif setting == "VOXF":
byte = self._memobj.memory[4-1].global1
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value:
byte.set_raw((num | 0x01).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x01).to_bytes(1, byteorder="big"))

def _get_int(self, setting):
if setting == "SQL":
byte2 = self._memobj.memory[3-1].global2.get_raw()

return int.from_bytes(byte2, byteorder="big") & 0x0F
elif setting == "CH":
byte2 = self._memobj.memory[8-1].global2.get_raw()

return (int.from_bytes(byte2, byteorder="big") & 0xFF) + 1
elif setting == "DCSTM":
byte3 = self._memobj.memory[7-1].global3.get_raw()

return 1 if int.from_bytes(byte3, byteorder="big") & 0x01 else 0
elif setting == "TOT":
byte3 = self._memobj.memory[3-1].global3.get_raw()

return int.from_bytes(byte3, byteorder="big")
elif setting == "VP":
byte1 = self._memobj.memory[3-1].global1.get_raw()

if int.from_bytes(byte1, byteorder="big") & 0x01:
return 1
elif int.from_bytes(byte1, byteorder="big") & 0x02:
return 2
else:
return 0
elif setting == "LCDT":
byte1 = self._memobj.memory[8-1].global1.get_raw()

return int.from_bytes(byte1, byteorder="big")
elif setting == "BATR":
byte3 = self._memobj.memory[8-1].global3.get_raw()

return int.from_bytes(byte3, byteorder="big")
elif setting == "VOXL":
byte2 = self._memobj.memory[4-1].global2.get_raw()

return int.from_bytes(byte2, byteorder="big") & 0x0F
elif setting == "VOXD":
byte3 = self._memobj.memory[4-1].global3.get_raw()

return int.from_bytes(byte3, byteorder="big")

def _set_int(self, setting, value):
if setting == "SQL":
byte = self._memobj.memory[3-1].global2
num = int.from_bytes(byte.get_raw(), byteorder="big")
byte.set_raw(((num & 0xF0) | (value & 0x0F))
.to_bytes(1, byteorder="big"))
elif setting == "CH":
byte = self._memobj.memory[8-1].global2
byte.set_raw((value - 1).to_bytes(1, byteorder="big"))
elif setting == "DCSTM":
byte = self._memobj.memory[7-1].global3
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value == 1:
byte.set_raw((num | 0x01).to_bytes(1, byteorder="big"))
else:
byte.set_raw((num & ~0x01).to_bytes(1, byteorder="big"))
elif setting == "TOT":
byte = self._memobj.memory[3-1].global3
byte.set_raw(value.to_bytes(1, byteorder="big"))
elif setting == "VP":
byte = self._memobj.memory[3-1].global1
num = int.from_bytes(byte.get_raw(), byteorder="big")
if value == 1:
byte.set_raw((0x01).to_bytes(1, byteorder="big"))
elif value == 2:
byte.set_raw((0x02).to_bytes(1, byteorder="big"))
else:
byte.set_raw((0x00).to_bytes(1, byteorder="big"))
elif setting == "LCDT":
byte = self._memobj.memory[8-1].global1
byte.set_raw(value.to_bytes(1, byteorder="big"))
elif setting == "BATR":
byte = self._memobj.memory[8-1].global3
byte.set_raw(value.to_bytes(1, byteorder="big"))
elif setting == "VOXL":
byte = self._memobj.memory[4-1].global2
num = int.from_bytes(byte.get_raw(), byteorder="big")
new_byte = (num & 0xF0) | (value & 0x0F)
byte.set_raw(new_byte.to_bytes(1, byteorder="big"))
elif setting == "VOXD":
byte = self._memobj.memory[4-1].global3
byte.set_raw(value.to_bytes(1, byteorder="big"))

_SETTINGS_OPTIONS = {
"DCSTM": ["Normal", "Special"],
"TOT": ["OFF"] + [str(x) for x in range(15, 615, 15)],
"VP": ["OFF", "Chinese", "English"],
"LCDT": ["OFF"] + [f"{i}S" for i in range(5, 30, 5)] + ["CONT"],
"BATR": ["1:3", "1:5", "1:7", "1:9", "1:12"],
"VOXL": ["OFF"] + [str(x) for x in range(1, 10)],
"VOXD": ["0.5", "1", "1.5", "2", "2.5", "3"],
}

def get_settings(self):
main = RadioSettingGroup("Main Settings", "Main")
vox = RadioSettingGroup("VOX Settings", "VOX")
radio_settings = RadioSettings(main, vox)\

lists = [
("DCSTM", main, "DCS Tail Mode"),
("TOT", main, "Time out(sec)"),
("VP", main, "Voice Prompts"),
("LCDT", main, "LCD Timeout"),
("BATR", main, "Battery Save"),
("VOXL", vox, "VOX Level"),
("VOXD", vox, "VOX Delay Time(sec)"),
]

bools = [
("BATS", main, "Battery Save"),
("BEEP", main, "Beep"),
("WARN", main, "Warn"),
("SCAN", main, "Scan"),
("CPYC", main, "Copy Channel"),
("VOXF", vox, "VOX Function"),
]

ints = [
("SQL", main, "Squelch", 0, 9),
("CH", main, "Channel", 1, 198),
]

for setting, group, name in bools:
value = self._get_bool(setting)
rs = RadioSetting(setting, name, RadioSettingValueBoolean(value))
group.append(rs)

for setting, group, name in lists:
value = self._get_int(setting)
options = self._SETTINGS_OPTIONS[setting]
rs = RadioSetting(setting, name,
RadioSettingValueList(options,
current_index=value))
group.append(rs)

for setting, group, name, minv, maxv in ints:
value = self._get_int(setting)
rs = RadioSetting(setting, name,
RadioSettingValueInteger(minv, maxv, value))
group.append(rs)

return radio_settings

def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
if not element.changed():
continue
elif isinstance(element.value, RadioSettingValueBoolean):
self._set_bool(element.get_name(), element.value)
elif isinstance(element.value, RadioSettingValueList):
# options = self._get_setting_options(element.get_name())
options = self._SETTINGS_OPTIONS[element.get_name()]
self._set_int(element.get_name(),
options.index(str(element.value)))
elif isinstance(element.value, RadioSettingValueInteger):
self._set_int(element.get_name(), int(element.value))
else:
LOG.error("Unknown setting type: %s" % element.value)

def sync_in(self):
try:
data = do_download(self)
except errors.RadioError:
raise
except Exception:
LOG.exception("Failed to download data to radio")
raise errors.RadioError("Failed to download data from radio")
finally:
_exit_programming_mode(self)
self._mmap = memmap.MemoryMapBytes(data)
self.process_mmap()

def process_mmap(self):
self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap)

def sync_out(self):
try:
do_upload(self)
except errors.RadioError:
raise
except Exception:
LOG.exception("Failed to upload data from radio")
raise errors.RadioError("Failed to upload data to radio")
finally:
_exit_programming_mode(self)
(2-2/3)