|
# 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 time
|
|
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.reset_input_buffer()
|
|
radio.pipe.reset_output_buffer()
|
|
time.sleep(0.002)
|
|
|
|
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 _rawrecv(radio, amount):
|
|
"""Raw read from the radio device"""
|
|
data = bytearray()
|
|
try:
|
|
while len(data) < amount:
|
|
chunk = radio.pipe.read(amount - len(data))
|
|
if not chunk:
|
|
LOG.debug("No response from radio")
|
|
raise errors.RadioNoContactLikelyK1()
|
|
data.extend(chunk)
|
|
time.sleep(0.002) # 2ms delay
|
|
except Exception:
|
|
msg = "Generic error reading data from radio; \
|
|
restart radio and check your cable."
|
|
raise errors.RadioError(msg)
|
|
|
|
if len(data) != amount:
|
|
LOG.error("Expected %i bytes, got %i" % (amount, len(data)))
|
|
raise errors.RadioError("Invalid response from the radio")
|
|
|
|
return bytes(data)
|
|
|
|
|
|
def _rawsend(radio, data):
|
|
"""Raw send to the radio device"""
|
|
try:
|
|
radio.pipe.write(data)
|
|
time.sleep(0.002)
|
|
except Exception:
|
|
raise errors.RadioError("Error sending data to radio")
|
|
|
|
|
|
def _recv(radio, addr, length):
|
|
"""Get data from the radio """
|
|
hdr = _rawrecv(radio, 4) # Read 4 byte header
|
|
data = _rawrecv(radio, length) # Read data
|
|
|
|
# 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)
|
|
|
|
_rawsend(radio, radio._magic)
|
|
LOG.debug("Sent magic sequence")
|
|
ack = _rawrecv(radio, 2)
|
|
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.reset_input_buffer()
|
|
_rawsend(radio, b"\x02")
|
|
ident = _rawrecv(radio, radio._magic_response_length)
|
|
|
|
if 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))
|
|
|
|
_rawsend(radio, COMMAND_ACCEPT)
|
|
ack = _rawrecv(radio, 1)
|
|
if 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))
|
|
|
|
try:
|
|
# 52=read | 20 = hex(32 bytes) = length of 2 channels
|
|
# + settings without header
|
|
cmd = b"\x52" + address + b"\x20"
|
|
_rawsend(radio, cmd)
|
|
data = _recv(radio, address, 32)
|
|
|
|
if address != b"\x00\x00":
|
|
_rawsend(radio, COMMAND_ACCEPT)
|
|
response = _rawrecv(radio, 1)
|
|
if response != COMMAND_ACCEPT:
|
|
raise errors.RadioError("Radio refused to read block at %04s"
|
|
% util.hexprint(address))
|
|
|
|
return data
|
|
except Exception:
|
|
raise errors.RadioError("Failed to read block from radio at %04s"
|
|
% util.hexprint(address))
|
|
|
|
|
|
def _write_block(radio, channel_id, data):
|
|
address = bytes(_get_memory_address(channel_id))
|
|
|
|
try:
|
|
cmd = b"\x57" + address + b"\x10" + data
|
|
_rawsend(radio, cmd)
|
|
|
|
response = _rawrecv(radio, 1)
|
|
if response != COMMAND_ACCEPT:
|
|
raise errors.RadioError("Radio refused to write block at %04s"
|
|
% util.hexprint(address))
|
|
except Exception:
|
|
raise errors.RadioError("Failed to write block to radio at %04s"
|
|
% util.hexprint(address))
|
|
|
|
|
|
def _encode_frequency(freq_mhz):
|
|
"""
|
|
Encodes a frequency in MHz into 4 bytes.
|
|
Converts from:
|
|
MHz → Byte3 Byte2 . Byte1 Byte0
|
|
Example:
|
|
431.350 → 00 50 13 43
|
|
"""
|
|
# TODO: make limitation dynamic based on location
|
|
if freq_mhz == 0.0:
|
|
return b'\xff\xff\xff\xff'
|
|
|
|
freq = int(freq_mhz / 10)
|
|
|
|
hex_str = str(freq)
|
|
if len(hex_str) % 2 != 0:
|
|
hex_str += "0"
|
|
while len(hex_str) < 8:
|
|
hex_str = "00" + hex_str
|
|
return bytes.fromhex(hex_str)[::-1]
|
|
|
|
|
|
def _decode_freq(hex_bytes):
|
|
"""
|
|
Decodes a 4-byte frequency representation into MHz.
|
|
The bytes must be read **right to left** in HEX format (LBCD):
|
|
Byte3 Byte2 . Byte1 Byte0
|
|
Example:
|
|
00 50 13 43 -> 431.350 MHz
|
|
"""
|
|
b0, b1, b2, b3 = hex_bytes
|
|
|
|
if b0 == 0xFF and b1 == 0xFF and b2 == 0xFF and b3 == 0xFF:
|
|
return 0.0
|
|
|
|
# Extract the high and low nibbles (hex digits) of b2
|
|
b2_high = (b2 >> 4) & 0xF # Get the first hex digit (upper nibble)
|
|
b2_low = b2 & 0xF # Get the second hex digit (lower nibble)
|
|
|
|
form = f'{b3:02X}{b2_high:X}{b2_low:X}{b1:02X}{b0:02X}'
|
|
return int(form)
|
|
|
|
|
|
def _decode_channel_settings(byte):
|
|
"""
|
|
Decodes the main channel settings byte into a structured dictionary.
|
|
|
|
:param byte: The byte to decode (integer)
|
|
:return: Dictionary with settings
|
|
"""
|
|
return {
|
|
# (1=Wide, 0=Narrow)
|
|
"W/N Mode": "Wide" if ord(byte) & 0x10 else "Narrow",
|
|
"Compander": bool(ord(byte) & 0x80), # (1=ON, 0=OFF)
|
|
"Scramble Mode": ord(byte) & 0x07, # (0=OFF, 1-7=Scramble levels)
|
|
"Special QT/DQT": "A" if ord(byte) & 0x20 else
|
|
("B" if ord(byte) & 0x40 else "OFF"),
|
|
}
|
|
|
|
|
|
def _encode_channel_settings(wn, scramble, compander, special_qt_dqt):
|
|
"""
|
|
Encodes the main channel settings into a single byte.
|
|
|
|
:param wn: "wide" or "narrow"
|
|
:param scramble: Scramble mode (0 = OFF, 1-8 = Scramble levels)
|
|
:param compander: Boolean, True = ON, False = OFF
|
|
:param special_qt_dqt: "off", "A", "B"
|
|
|
|
:return: Encoded settings byte (integer)
|
|
"""
|
|
settings_byte = 0x00 # Start with 0000 0000
|
|
|
|
# Wide/Narrow: 1 = Wide, 0 = Narrow
|
|
if wn.lower() == "wide":
|
|
settings_byte |= 0x10
|
|
|
|
# Compander: 1 = ON, 0 = OFF
|
|
if compander:
|
|
settings_byte |= 0x80
|
|
|
|
# Scramble Mode
|
|
if scramble > 0:
|
|
settings_byte |= (scramble & 0x07)
|
|
|
|
# Special QT/DQT: 0 = OFF, A = 1, B = 2
|
|
if special_qt_dqt.upper() == "A":
|
|
settings_byte |= 0x02
|
|
elif special_qt_dqt.upper() == "B":
|
|
settings_byte |= 0x06
|
|
|
|
return settings_byte.to_bytes(1, "big")
|
|
|
|
|
|
def do_download(radio):
|
|
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)
|
|
|
|
# block = radio._cache_block(i, in_prog_mode=True)
|
|
result = _read_block(radio, i)
|
|
# block1, block2 = result[:16], result[16:]
|
|
data.extend(result)
|
|
|
|
LOG.debug("Downloaded memory channel %i" % i)
|
|
# LOG.debug(block)
|
|
|
|
_exit_programming_mode(radio)
|
|
|
|
LOG.debug("Downloaded %i bytes of data" % len(data))
|
|
|
|
return bytes(data)
|
|
|
|
|
|
def do_upload(radio):
|
|
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._upper, 1):
|
|
status.cur = i
|
|
radio.status_fn(status)
|
|
|
|
# block = radio._memcache[i]
|
|
# _write_block(radio, block.channel_id, block.pack())
|
|
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)
|
|
|
|
_exit_programming_mode(radio)
|
|
|
|
|
|
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 settings; // Bitmask (Wide/Narrow, Scramble, etc.)
|
|
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
|
|
|
|
rxfreq = _decode_freq(_mem.rxfreq.get_raw())
|
|
txfreq = _decode_freq(_mem.txfreq.get_raw())
|
|
mem.freq = rxfreq * 10
|
|
chirp_common.split_to_offset(mem, rxfreq, 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 main_settings["W/N Mode"] == "Wide" 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(main_settings["Compander"]))
|
|
mem.extra.append(rs)
|
|
|
|
rs = RadioSetting(
|
|
"Scramble Mode", "Scramble Mode",
|
|
RadioSettingValueList(
|
|
["OFF", "1", "2", "3", "4", "5", "6", "7"],
|
|
current_index=main_settings["Scramble Mode"]
|
|
))
|
|
mem.extra.append(rs)
|
|
|
|
rs = RadioSetting(
|
|
"Special QT/DQT", "Special QT/DQT",
|
|
RadioSettingValueList(
|
|
["OFF", "A", "B"],
|
|
current_index=0 if main_settings["Special QT/DQT"] == "OFF"
|
|
else (1 if main_settings["Special QT/DQT"] == "A" else 2)
|
|
))
|
|
mem.extra.append(rs)
|
|
|
|
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)
|
|
|
|
# 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):
|
|
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.set_raw(_encode_frequency(mem.freq))
|
|
|
|
if mem.duplex == "split":
|
|
print("Split mode with offset %i" % mem.offset)
|
|
_mem.txfreq.set_raw(_encode_frequency(mem.offset))
|
|
elif mem.duplex == "+":
|
|
_mem.txfreq.set_raw(_encode_frequency(mem.freq + mem.offset))
|
|
elif mem.duplex == "-":
|
|
_mem.txfreq.set_raw(_encode_frequency(mem.freq - mem.offset))
|
|
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()
|
|
|
|
compander = False
|
|
scramble = 0
|
|
special_qt_dqt = "OFF"
|
|
learn_code = False
|
|
special_code = "FF" * 4
|
|
for setting in mem.extra:
|
|
if setting.get_name() == "Compander":
|
|
compander = bool(setting.value)
|
|
elif setting.get_name() == "Scramble Mode":
|
|
scramble = int(setting.value) if str(
|
|
setting.value) != "OFF" else 0
|
|
elif setting.get_name() == "Special QT/DQT":
|
|
special_qt_dqt = str(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.settings.set_raw(_encode_channel_settings(
|
|
str("Narrow" if mem.mode == "NFM" else "Wide"),
|
|
scramble,
|
|
compander,
|
|
special_qt_dqt
|
|
))
|
|
|
|
_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")
|
|
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")
|