Project

General

Profile

Bug #11300 » alinco_dr735t.py

Second, potentially dangerous DR-735T driver - Jacob Calvert, 04/21/2024 07:43 AM

 
# Copyright 2024 Jacob Calvert <jcalvert@jacobncalvert.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 3 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/>.

from chirp import chirp_common, bitwise, errors, memmap, directory
from chirp.settings import RadioSettingGroup, RadioSetting
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
from chirp.drivers.alinco import ALINCO_TONES, CHARSET

import logging
import codecs

MEM_FORMAT = """
struct {
u8 used;
u8 skip;
u8 favorite;
u8 unknown3;
ul32 frequency;
ul32 shift;
u8 shift_direction;
u8 subtone_selection;
u8 rx_tone_index;
u8 tx_tone_index;
u8 dcs_index;
u8 unknown17;
u8 power_index;
u8 busy_channel_lockout;
u8 mode;
u8 heterodyne_mode;
u8 unknown22;
u8 bell;
u8 name[6];
u8 dcs_off;
u8 unknown31;
u8 standby_screen_color;
u8 rx_screen_color;
u8 tx_screen_color;
u8 unknown35_to_63[29];
} memory[1000];
"""


LOG = logging.getLogger(__name__)


@directory.register
class AlincoDR735T(chirp_common.CloneModeRadio):
"""Base class for DR735T radio"""

"""Alinco DR735T"""
VENDOR = "Alinco"
MODEL = "DR735T"
BAUD_RATE = 38400
NEEDS_COMPAT_SERIAL = False

TONE_MODE_MAP = {
0x00: "",
0x01: "Tone",
0x03: "TSQL",
0x0C: "DTCS"
}

SHIFT_DIR_MAP = ["", "-", "+"]

POWER_MAP = [
chirp_common.PowerLevel("High", watts=50.0),
chirp_common.PowerLevel("Mid", watts=25.00),
chirp_common.PowerLevel("Low", watts=5.00),
]
MODE_MAP = {
0x00: "FM",
0x01: "NFM",
0x02: "AM",
0x03: "NAM",
0x80: "Auto"
}

HET_MODE_MAP = ["Normal", "Reverse"]

SCREEN_COLOR_MAP = [f"Color {n+1}" for n in range(16)]

_freq_ranges = [
(108000000, 136000000),
(136000000, 174000000),
(400000000, 480000000)
]
_no_channels = 1000

_model = b"DR735TN"

def get_features(self):
rf = chirp_common.RadioFeatures()
# convert to list to deal with dict_values unsubscriptable
rf.valid_tmodes = list(self.TONE_MODE_MAP.values())
rf.valid_modes = list(self.MODE_MAP.values())
rf.valid_skips = ["", "S"]
rf.valid_bands = self._freq_ranges
rf.memory_bounds = (0, self._no_channels-1)
rf.has_ctone = True
rf.has_bank = False
rf.has_dtcs_polarity = False
rf.has_tuning_step = False

rf.can_delete = False
rf.valid_name_length = 6
rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC
rf.valid_power_levels = self.POWER_MAP
rf.valid_dtcs_codes = chirp_common.DTCS_CODES

return rf

def _identify(self) -> bool:
command = b"AL~WHO\r\n"
self.pipe.write(command)
self.pipe.read(len(command))
# expect DR735TN\r\n
radio_id = self.pipe.read(9).strip()
return radio_id in (b"DR735TN", b"DR735TE")

def do_download(self):
if not self._identify():
raise errors.RadioError("Unsupported radio model.")

channel_data = b""

for channel_no in range(0, self._no_channels):

command = f"AL~EEPEL{channel_no<<6 :04X}R\r\n".encode()
self.pipe.write(command)
self.pipe.read(len(command))
channel_spec = self.pipe.read(128) # 64 bytes, as hex
self.pipe.read(2) # \r\n
channel_spec = codecs.decode(channel_spec, "hex")
if len(channel_spec) != 64:
exit(1)
channel_data += channel_spec

if self.status_fn:
status = chirp_common.Status()
status.cur = channel_no
status.max = self._no_channels
status.msg = f"Downloading channel {channel_no} from radio"
self.status_fn(status)

return memmap.MemoryMapBytes(channel_data)

def do_upload(self):
if not self._identify():
raise errors.RadioError("Unsupported radio model.")

command = b"AL~DR735J\r\n"
self.pipe.write(command)
self.pipe.read(len(command))
resp = self.pipe.read(4)
if resp != b"OK\r\n":
errors.RadioError("Could not go into download mode.")

for channel_no in range(0, self._no_channels):
write_data = self.get_mmap()[channel_no*64:(channel_no+1)*64]
write_data = codecs.encode(write_data, 'hex').upper()
command = f"AL~EEPEL{channel_no<<6 :04X}W".encode(
) + write_data + b"\r\n"
LOG.debug(f"COMM: {command}")
self.pipe.write(command)
back = self.pipe.read(len(command))
LOG.debug(f"BACK: {back}")
resp = self.pipe.read(4)
LOG.debug(f"RESP: {resp}")
if resp != b"OK\r\n":
raise errors.RadioError("failed to write to channel")

if self.status_fn:
status = chirp_common.Status()
status.cur = channel_no
status.max = self._no_channels
status.msg = f"Uploading channel {channel_no} to radio"
self.status_fn(status)

command = b"AL~RESET\r\n"
self.pipe.write(command)
self.pipe.read(len(command)) # command + OK\r\n
self.pipe.read(4)

def sync_in(self):
try:
self._mmap = self.do_download()
except Exception as exc:
raise errors.RadioError(f"Failed to download from radio: {exc}")
self.process_mmap()

def sync_out(self):
try:
self.do_upload()
except Exception as exc:
raise errors.RadioError(f"Failed to download from radio: {exc}")

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

def get_raw_memory(self, number):
return repr(self._memobj.memory[number])

def get_memory(self, number):

_mem = self._memobj.memory[number]
mem = chirp_common.Memory()
mem.number = number # Set the memory number
if _mem.used != 0x55:
mem.empty = True
mem.freq = 0
mem.name = ""

mem.tmode = self.TONE_MODE_MAP[0]
mem.duplex = self.SHIFT_DIR_MAP[0]
mem.offset = 0

mem.rtone = ALINCO_TONES[0]
mem.ctone = ALINCO_TONES[0]
mem.dtcs = chirp_common.DTCS_CODES[0]
mem.power = self.POWER_MAP[0]
mem.skip = ''
mem.mode = self.MODE_MAP[0]
return mem
else:

mem.empty = False
mem.freq = int(_mem.frequency)
mem.name = "".join([CHARSET[_mem.name[i]]
for i in range(6)]).strip()

mem.tmode = self.TONE_MODE_MAP[int(_mem.subtone_selection)]
mem.duplex = self.SHIFT_DIR_MAP[_mem.shift_direction]
mem.offset = _mem.shift

mem.rtone = ALINCO_TONES[_mem.rx_tone_index]
mem.ctone = ALINCO_TONES[_mem.tx_tone_index]
mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs_index]
mem.power = self.POWER_MAP[_mem.power_index]
mem.skip = 'S' if bool(_mem.skip) else ''
mem.mode = self.MODE_MAP[int(_mem.mode)]

self._get_extra(_mem, mem)

return mem

def set_memory(self, mem):
# Get a low-level memory object mapped to the image
_mem = self._memobj.memory[mem.number]

def find_key_in(d: dict, target_val):
for k, v in d.items():
if v == target_val:
return k

if not mem.empty:
mapped_name = [CHARSET.index(' ').to_bytes(1, 'little')]*6
for (i, c) in enumerate(mem.name.ljust(6)[:6].upper().strip()):
if c not in chirp_common.CHARSET_UPPER_NUMERIC:
c = " " # just make it a space
mapped_name[i] = CHARSET.index(c).to_bytes(1, 'little')
_mem.frequency = int(mem.freq)
_mem.name = b''.join(mapped_name)
_mem.mode = find_key_in(self.MODE_MAP, mem.mode)
_mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, mem.tmode)
_mem.shift = mem.offset
_mem.used = 0x00 if mem.empty else 0x55
_mem.power_index = self.POWER_MAP.index(
mem.power) if mem.power in self.POWER_MAP else 0
_mem.skip = 0x01 if mem.skip == "S" else 0x00
try:
_mem.rx_tone_index = ALINCO_TONES.index(
mem.rtone)
except ValueError:
raise errors.UnsupportedToneError("This radio does "
"not support "
"tone %.1fHz" % mem.rtone)
try:

_mem.tx_tone_index = ALINCO_TONES.index(
mem.ctone)
except ValueError:
raise errors.UnsupportedToneError("This radio does "
"not support "
"tone %.1fHz" % mem.ctone)
_mem.dcs_index = chirp_common.DTCS_CODES.index(
mem.dtcs if mem.dtcs else chirp_common.DTCS_CODES[0])
_mem.shift_direction = self.SHIFT_DIR_MAP.index(
mem.duplex if mem.duplex else self.SHIFT_DIR_MAP[0])
else:
_mem.frequency = 0
_mem.name = b"\x00"*6
_mem.mode = find_key_in(self.MODE_MAP, "Auto")
_mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, "")
_mem.shift = 0
_mem.used = 0x00 if mem.empty else 0x55
_mem.power_index = 0
_mem.skip = 0x01 if mem.skip == "S" else 0x00
_mem.rx_tone_index = 0
_mem.tx_tone_index = 0
_mem.dcs_index = 0
_mem.shift_direction = self.SHIFT_DIR_MAP.index("")

self._set_extra(_mem, mem)

def _get_extra(self, _mem, mem):
mem.extra = RadioSettingGroup("extra", "Extra")
het_mode = RadioSetting("heterodyne_mode", "Heterodyne Mode",
RadioSettingValueList(
self.HET_MODE_MAP,
current=self.HET_MODE_MAP[int(
_mem.heterodyne_mode)]
))
het_mode.set_doc("Heterodyne Mode")

bcl = RadioSetting("bcl", "BCL",
RadioSettingValueBoolean(
bool(_mem.busy_channel_lockout)
))
bcl.set_doc("Busy Channel Lockout")

stby_screen = RadioSetting("stby_screen", "Standby Screen Color",
RadioSettingValueList(
self.SCREEN_COLOR_MAP,
current=self.SCREEN_COLOR_MAP[int(
_mem.standby_screen_color)]
))
stby_screen.set_doc("Standby Screen Color")

rx_screen = RadioSetting("rx_screen", "RX Screen Color",
RadioSettingValueList(
self.SCREEN_COLOR_MAP,
current=self.SCREEN_COLOR_MAP[int(
_mem.rx_screen_color)]
))
rx_screen.set_doc("RX Screen Color")

tx_screen = RadioSetting("tx_screen", "TX Screen Color",
RadioSettingValueList(
self.SCREEN_COLOR_MAP,
current=self.SCREEN_COLOR_MAP[int(
_mem.tx_screen_color)]
))
tx_screen.set_doc("TX Screen Color")

mem.extra.append(het_mode)
mem.extra.append(bcl)
mem.extra.append(stby_screen)
mem.extra.append(rx_screen)
mem.extra.append(tx_screen)

def _set_extra(self, _mem, mem):
for setting in mem.extra:
if setting.get_name() == "heterodyne_mode":
_mem.heterodyne_mode = \
self.HET_MODE_MAP.index(
setting.value) if \
setting.value else self.HET_MODE_MAP[0]

if setting.get_name() == "bcl":
_mem.busy_channel_lockout = \
0x01 if setting.value else 0x00

if setting.get_name() == "stby_screen":
_mem.standby_screen_color = \
self.SCREEN_COLOR_MAP.index(setting.value) if \
setting.value else self.SCREEN_COLOR_MAP[0]

if setting.get_name() == "rx_screen":
_mem.rx_screen_color = \
self.SCREEN_COLOR_MAP.index(setting.value) if \
setting.value else self.SCREEN_COLOR_MAP[0]

if setting.get_name() == "tx_screen":
_mem.tx_screen_color = \
self.SCREEN_COLOR_MAP.index(setting.value) if \
setting.value else self.SCREEN_COLOR_MAP[0]
(7-7/15)