|
# Copyright 2025 Mike Iacovacci <ascendr@linuxmail.org>
|
|
#
|
|
# 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 chirp_common, directory, memmap
|
|
from chirp import bitwise, errors
|
|
from chirp.settings import RadioSetting, RadioSettingGroup, \
|
|
RadioSettingValueInteger, RadioSettingValueList, \
|
|
RadioSettingValueBoolean, RadioSettingValueString, RadioSettings
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
MEM_FORMAT = """
|
|
struct{
|
|
u8 unknown02[256];
|
|
} unknown02[16];
|
|
struct{
|
|
u8 bootscrmode;
|
|
u8 bsmodepad[15];
|
|
u8 bootscreen1[10];
|
|
u8 bs1pad[6];
|
|
u8 bootscreen2[10];
|
|
u8 bs2pad[6];
|
|
u8 unused[16];
|
|
u8 timeout;
|
|
u8 squelch;
|
|
u8 vox_level;
|
|
u8 batt_save:4,
|
|
unk_bits:2,
|
|
work_mode:1,
|
|
voice_alert:1;
|
|
u8 backlight;
|
|
u8 beep_tone:1,
|
|
auto_key_lock:1,
|
|
unk_bit_2:1,
|
|
ctcss_revert:1,
|
|
scan_type:2,
|
|
side_tone:2;
|
|
u8 unk_bit_3:1,
|
|
standby:1,
|
|
roger:1,
|
|
alarm_mode:2,
|
|
alarm_sound:1,
|
|
fm_radio:1,
|
|
unk_bit_4:1;
|
|
u8 tail_revert;
|
|
u8 tail_delay;
|
|
u8 tbst;
|
|
u8 unk_bits_5:6,
|
|
a_ch_disp:1,
|
|
b_ch_disp:1;
|
|
u8 unk_sett[30];
|
|
u8 passw_w_ena;
|
|
u8 passw_r_ena;
|
|
u8 passw_w_val[8];
|
|
u8 passw_r_val[8];
|
|
u8 unk_sett_2[5];
|
|
} settings;
|
|
struct{
|
|
u8 unusedsettings[32];
|
|
} unusedsettings[124];
|
|
struct{
|
|
u8 entry[5];
|
|
} dtmf_list[15];
|
|
struct{
|
|
u8 dtmf_l_pad[5];
|
|
u8 radio_id[5];
|
|
u8 unk_dtmf[11];
|
|
u8 unk_dtmf_bits:6,
|
|
press_send:1,
|
|
release_send:1;
|
|
u8 delay_time;
|
|
u8 digit_dur;
|
|
u8 inter_dur;
|
|
u8 unk_dtmf_2[28];
|
|
} dtmf;
|
|
struct{
|
|
u8 unkdtmf[128];
|
|
} unuseddtmf[31];
|
|
struct{
|
|
u8 unk_mem[16];
|
|
} tuning;
|
|
struct {
|
|
lbcd rx_freq[4];
|
|
lbcd tx_freq[4];
|
|
u8 mem_changed;
|
|
u8 rx_tone[2];
|
|
u8 tx_tone[2];
|
|
u8 unk_mem2:1,
|
|
busy_lock:1,
|
|
ptt_id:2,
|
|
unk_mem3:1,
|
|
mode:1,
|
|
power:1,
|
|
unk_mem4:1;
|
|
u8 signal:4,
|
|
unk_mem5:2,
|
|
freq_hop:1,
|
|
scan:1;
|
|
u8 unk_mem6;
|
|
} vfomemory[2];
|
|
struct {
|
|
lbcd rx_freq[4];
|
|
lbcd tx_freq[4];
|
|
u8 mem_changed;
|
|
u8 rx_tone[2];
|
|
u8 tx_tone[2];
|
|
u8 unk_mem2:1,
|
|
busy_lock:1,
|
|
ptt_id:2,
|
|
unk_mem3:1,
|
|
mode:1,
|
|
power:1,
|
|
unk_mem4:1;
|
|
u8 signal:4,
|
|
unk_mem5:2,
|
|
freq_hop:1,
|
|
scan:1;
|
|
u8 unk_mem6;
|
|
} memory[250];
|
|
struct{
|
|
u8 unused_memch[16];
|
|
} unused_mem[3];
|
|
struct{
|
|
u8 unknown17[256];
|
|
} unknown17[16];
|
|
struct{
|
|
u8 unknown18[256];
|
|
} unknown18[16];
|
|
struct{
|
|
u8 unknown19[256];
|
|
} unknown19[16];
|
|
struct{
|
|
u8 name[6];
|
|
u8 pad[5];
|
|
} memnames[250];
|
|
struct{
|
|
u8 unknames[64];
|
|
} unknownnames [21];
|
|
struct{
|
|
u8 unknown25[256];
|
|
} unknown25[16];
|
|
struct{
|
|
u8 unknown26[256];
|
|
} unknown26[16];
|
|
"""
|
|
|
|
MEM_TYPES = [
|
|
(0x02, "unknown02"),
|
|
(0x04, "settings"),
|
|
(0x06, "dtmf"),
|
|
(0x16, "memories"),
|
|
(0x17, "unknown17"),
|
|
(0x18, "unknown18"),
|
|
(0x19, "unknown19"),
|
|
(0x24, "chan_names"),
|
|
(0x25, "unknown25"),
|
|
(0x26, "unknown26")
|
|
]
|
|
|
|
WRITE_MAP = [
|
|
(0x04, "settings", 0x1000),
|
|
(0x06, "dtmf", 0x2000),
|
|
(0x16, "memories", 0x3000),
|
|
(0x24, "chan_names", 0x7000)
|
|
]
|
|
|
|
|
|
def do_ack_ack(serial):
|
|
serial.write(b'\x06')
|
|
ack = serial.read(1)
|
|
if ack != b'\x06':
|
|
err = f"Error expected 06 ack got {ack}"
|
|
LOG.debug(err)
|
|
raise errors.RadioError(err)
|
|
|
|
|
|
def raw_send(serial, data, exlen):
|
|
serial.write(data)
|
|
return serial.read(exlen)
|
|
|
|
|
|
def do_read_cmd(serial, cmd, exlen):
|
|
echo_ack = len(cmd) + 1 # ack 0x57 + echo of cmd
|
|
resp = raw_send(serial, b'\x52' + cmd, exlen + echo_ack)
|
|
if resp[0] != 0x57:
|
|
raise errors.RadioError(f"Read CMD resp failed got {resp}")
|
|
if len(resp[echo_ack:]) != exlen:
|
|
raise errors.RadioError(f"Read CMD resp expect len={exlen} got {resp}")
|
|
do_ack_ack(serial)
|
|
return resp[echo_ack:]
|
|
|
|
|
|
def enter_prog(serial):
|
|
cmd = bytes.fromhex('50534541524348')
|
|
resp = raw_send(serial, cmd, 8) # PSEARCH
|
|
if resp is None:
|
|
errors.RadioError(f"Radio Init Failed {resp}")
|
|
return False
|
|
if resp[0] != 0x06:
|
|
errors.RadioError(f"Radio Init Failed {resp}")
|
|
return False
|
|
return resp[1:]
|
|
|
|
|
|
def exit_prog(serial):
|
|
try:
|
|
serial.write(b'\x06')
|
|
serial.write(b'\x06')
|
|
serial.write(b'\x00')
|
|
serial.close()
|
|
LOG.debug("Exited programming")
|
|
except Exception as e:
|
|
raise errors.RadioError(f"Error exiting programming {e}")
|
|
|
|
|
|
def check_ident(data):
|
|
if data == b'P13GMRS':
|
|
LOG.info(f"Radio is: {data}")
|
|
else:
|
|
err = (f"Ident returned unknown Radio: {data}")
|
|
LOG.debug = (err)
|
|
raise errors.RadioError(err)
|
|
|
|
|
|
def do_sysinfo(serial):
|
|
cmd = bytes.fromhex('50415353535441') # PASSSTA
|
|
resp = raw_send(serial, cmd, 3)
|
|
if resp != b'\x50\x00\x00':
|
|
raise errors.RadioError(f"Expected 0x500000 got {resp}")
|
|
cmd = bytes.fromhex('535953494e464f') # SYSINFO
|
|
resp = raw_send(serial, cmd, 1)
|
|
if resp != b'\x06':
|
|
raise errors.RadioError(f"ACK expected got {resp}")
|
|
LOG.debug('SYSINFO', resp)
|
|
|
|
|
|
def do_readconfig(serial):
|
|
# cmds start with 56, and expect 06 ack after recv ack
|
|
for addr, _len in [(0x00000a0d, 13), (0x00100a0d, 13),
|
|
(0x00200a0d, 13), (0x0000000a, 11)]:
|
|
cmd = struct.pack('>BL', 0x56, addr)
|
|
resp = raw_send(serial, cmd, _len)
|
|
if len(resp) != _len:
|
|
raise errors.RadioError(f"Expected (_len) Bytes got {resp}")
|
|
do_ack_ack(serial)
|
|
return True
|
|
|
|
|
|
def do_prog2(serial):
|
|
cmd = struct.pack('>LB', 0xffffffff, 0x0c)
|
|
serial.write(cmd) # no resp expected
|
|
cmd = bytes.fromhex('503133474d5253') # P13GMRS
|
|
resp = raw_send(serial, cmd, 1)
|
|
if resp != b'\x06':
|
|
raise errors.RadioError(f"Error expected 06 ack got {resp}")
|
|
resp = raw_send(serial, b'\x02', 8)
|
|
if len(resp) != 8:
|
|
raise errors.RadioError(f"Error expected len 8 got {resp}")
|
|
do_ack_ack(serial)
|
|
|
|
|
|
def do_read_tlmap(serial):
|
|
# build the data type / addr location map
|
|
tl_map = {m[0]: 0 for m in MEM_TYPES}
|
|
for i in range(1, 16):
|
|
addr = (i << 4 | 0x0f)
|
|
cmd = struct.pack('>4B', 0xff, addr, 0x00, 0x01)
|
|
resp = do_read_cmd(serial, cmd, 1)
|
|
r_int = int.from_bytes(resp, byteorder='big')
|
|
if r_int in tl_map:
|
|
tl_map[r_int] = (i << 4)
|
|
return tl_map
|
|
|
|
|
|
def do_read_ranges(serial, loc, radio, status):
|
|
data = b''
|
|
for i in range(16):
|
|
for pre in range(0x0, 0xc1, 0x40):
|
|
addr = struct.pack('>BBBB', pre, loc + i, 0x00, 0x40)
|
|
data += do_read_cmd(serial, addr, 0x40)
|
|
status.cur += 0x40
|
|
radio.status_fn(status)
|
|
return data
|
|
|
|
|
|
def do_upload_block(serial, loc, offset, radio, status):
|
|
_mem = radio._memobj.get_raw()
|
|
for i in range(16):
|
|
for pre in range(0x0, 0xc1, 0x40):
|
|
x = offset + (i * 0x100) + pre
|
|
block = _mem[x:x + 0x40]
|
|
addr = struct.pack('>BBBBB', 0x57, pre, loc + i, 0x00, 0x40)
|
|
data = addr + block
|
|
ack = raw_send(serial, data, 1)
|
|
if ack != b'\x06':
|
|
err = f"Bad ack on write expect 0x06 got {ack}"
|
|
LOG.debug(err)
|
|
raise errors.RadioError(err)
|
|
status.cur += len(block)
|
|
radio.status_fn(status)
|
|
|
|
|
|
def do_download(radio):
|
|
data = b''
|
|
try:
|
|
status = chirp_common.Status()
|
|
serial = radio.pipe
|
|
serial.flush()
|
|
ident = enter_prog(serial)
|
|
check_ident(ident)
|
|
do_sysinfo(serial)
|
|
do_readconfig(serial)
|
|
do_prog2(serial)
|
|
tl_map = do_read_tlmap(serial)
|
|
status.max = len(tl_map) * 0x1000
|
|
status.msg = "Downloading..."
|
|
for t, loc in tl_map.items():
|
|
if loc == 0:
|
|
raise errors.RadioError(f"TL Map failed {t} {loc}")
|
|
data += do_read_ranges(serial, loc, radio, status)
|
|
except Exception as e:
|
|
raise errors.RadioError(f"Error during download {e}")
|
|
finally:
|
|
exit_prog(serial)
|
|
return memmap.MemoryMapBytes(data)
|
|
|
|
|
|
def do_upload(radio):
|
|
try:
|
|
status = chirp_common.Status()
|
|
status.max = len(WRITE_MAP) * 0x1000
|
|
serial = radio.pipe
|
|
serial.flush()
|
|
ident = enter_prog(serial)
|
|
check_ident(ident)
|
|
do_sysinfo(serial)
|
|
do_readconfig(serial)
|
|
do_prog2(serial)
|
|
tl_map = do_read_tlmap(serial)
|
|
for wt, label, offset in WRITE_MAP:
|
|
status.msg = f"Uploading: {label}..."
|
|
loc = tl_map[wt]
|
|
do_upload_block(serial, loc, offset, radio, status)
|
|
except Exception as e:
|
|
raise errors.RadioError(f"Error during upload {e}")
|
|
finally:
|
|
exit_prog(serial)
|
|
|
|
|
|
@directory.register
|
|
class RadioddityRGM30(
|
|
chirp_common.CloneModeRadio,
|
|
chirp_common.ExperimentalRadio):
|
|
"""Radioddity GM30"""
|
|
VENDOR = "Radioddity"
|
|
MODEL = "GM30"
|
|
BAUD_RATE = 57600
|
|
|
|
POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=0.50),
|
|
chirp_common.PowerLevel("High", watts=3.00)]
|
|
VALID_MODES = ["NFM", "FM"]
|
|
|
|
_range = [(136000000, 174000000), (400000000, 470000000)]
|
|
|
|
GMRS_RPTR = [462550000, 462575000, 462600000, 462625000, 462650000,
|
|
462675000, 462700000, 462725000]
|
|
|
|
VALID_TONES = chirp_common.TONES
|
|
VALID_DCS = [i for i in range(
|
|
0, 778) if '9' not in str(i) and '8' not in str(i)]
|
|
VALID_CHARSET = "".join(chr(i)
|
|
for i in range(32, 127) if chr(i) not in '\\`~')
|
|
VALID_DTMF = [str(i) for i in range(0, 10)] + \
|
|
["A", "B", "C", "D", "*", "#"]
|
|
ASCII_NUM = [str(i) for i in range(10)] + [' ']
|
|
|
|
@classmethod
|
|
def get_prompts(cls):
|
|
rp = chirp_common.RadioPrompts()
|
|
rp.experimental = \
|
|
('This Radioddity GM30 driver is experimental, '
|
|
'please report any issues')
|
|
return rp
|
|
|
|
def get_features(self):
|
|
rf = chirp_common.RadioFeatures()
|
|
rf.has_settings = True
|
|
rf.has_bank = False
|
|
rf.has_tuning_step = False
|
|
rf.has_name = True
|
|
rf.valid_characters = self.VALID_CHARSET
|
|
rf.valid_name_length = 6
|
|
rf.has_offset = True
|
|
rf.has_mode = True
|
|
rf.has_dtcs = True
|
|
rf.has_rx_dtcs = True
|
|
rf.has_dtcs_polarity = True
|
|
rf.has_ctone = True
|
|
rf.has_cross = True
|
|
rf.can_odd_split = False
|
|
rf.can_delete = True
|
|
rf.valid_modes = self.VALID_MODES
|
|
rf.valid_duplexes = ["", "-", "+", "off"]
|
|
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
|
|
rf.valid_cross_modes = [
|
|
"Tone->DTCS",
|
|
"DTCS->Tone",
|
|
"->Tone",
|
|
"Tone->Tone",
|
|
"->DTCS",
|
|
"DTCS->",
|
|
"DTCS->DTCS"]
|
|
rf.valid_power_levels = self.POWER_LEVELS
|
|
rf.valid_skips = ["", "S"]
|
|
rf.valid_bands = self._range
|
|
rf.memory_bounds = (1, 252)
|
|
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0]
|
|
return rf
|
|
|
|
def process_mmap(self):
|
|
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
|
|
|
def sync_in(self):
|
|
try:
|
|
data = do_download(self)
|
|
except errors.RadioError:
|
|
raise
|
|
except Exception as e:
|
|
err = f'Error during download {e}'
|
|
LOG.error(err)
|
|
raise errors.RadioError(err)
|
|
self._mmap = data
|
|
self.process_mmap()
|
|
|
|
def sync_out(self):
|
|
do_upload(self)
|
|
|
|
def val_or_def(self, memitem):
|
|
return memitem if memitem is not None else 0
|
|
|
|
def get_str_name(self, _name):
|
|
return "".join(chr(int(i)) if 31 < int(i) < 126
|
|
else ' ' for i in _name)
|
|
|
|
def get_memory(self, number):
|
|
mem = chirp_common.Memory()
|
|
mem.number = number
|
|
if 0 < mem.number <= 250:
|
|
_mem = self._memobj.memory[number-1]
|
|
_name = self._memobj.memnames[number-1]["name"]
|
|
str_name = self.get_str_name(_name)
|
|
mem.name = str_name.rstrip()
|
|
mem.immutable = []
|
|
else:
|
|
_mem = self._memobj.vfomemory[number-251]
|
|
mem.name = "VFO-A" if number == 251 else "VFO-B"
|
|
|
|
if _mem.rx_freq.get_raw() == b'\xff\xff\xff\xff':
|
|
mem.empty = True
|
|
return mem
|
|
|
|
mem.freq = int(_mem.rx_freq) * 10
|
|
|
|
if _mem.tx_freq.get_raw() == b'\xff\xff\xff\xff':
|
|
mem.duplex = ''
|
|
|
|
elif int(_mem.tx_freq) - int(_mem.rx_freq) > 0:
|
|
# '+' duplex
|
|
mem.duplex = '+'
|
|
mem.offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
|
|
|
|
elif int(_mem.tx_freq) - int(_mem.rx_freq) < 0:
|
|
# '-' duplex
|
|
mem.duplex = '-'
|
|
mem.offset = (int(_mem.rx_freq) - int(_mem.tx_freq)) * 10
|
|
|
|
mem.mode = self.VALID_MODES[self.val_or_def(_mem.mode)]
|
|
mem.power = self.POWER_LEVELS[self.val_or_def(_mem.power)]
|
|
mem.skip = "" if _mem.scan else "S"
|
|
|
|
if 23 <= mem.number <= 54:
|
|
mem.offset = 5 * 1000000
|
|
mem.duplex = '+'
|
|
else:
|
|
mem.offset = 0
|
|
mem.duplex = ""
|
|
mem.immutable = ['offset', 'duplex']
|
|
if 0 < mem.number <= 54:
|
|
mem.immutable += ['empty']
|
|
if mem.number > 250:
|
|
mem.immutable += ['empty', 'name']
|
|
if 0 < mem.number <= 30:
|
|
mem.immutable = mem.immutable + ['freq']
|
|
if 8 <= mem.number <= 14:
|
|
mem.immutable = mem.immutable + ['mode', 'power']
|
|
|
|
self.get_tone__mem(_mem, mem)
|
|
|
|
mem.extra = RadioSettingGroup("Extra", "extra")
|
|
|
|
rs = RadioSettingValueBoolean(_mem.busy_lock)
|
|
rset = RadioSetting("busy_lock", "Busy Lock", rs)
|
|
mem.extra.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(_mem.freq_hop)
|
|
rset = RadioSetting("freq_hop", "Freq. Hop", rs)
|
|
mem.extra.append(rset)
|
|
|
|
_current = _mem.signal if _mem.signal else 1
|
|
rs = RadioSettingValueInteger(1, 15, current=_current)
|
|
rset = RadioSetting("signal", "DTMF ID", rs)
|
|
mem.extra.append(rset)
|
|
|
|
options = ['Off', 'BOT', 'EOT', 'BOTH']
|
|
rs = RadioSettingValueList(options, current_index=_mem.ptt_id)
|
|
rset = RadioSetting("ptt_id", "PTT ID", rs)
|
|
mem.extra.append(rset)
|
|
|
|
return mem
|
|
|
|
def _bch_to_int(self, _array):
|
|
# [0x54,0xc7] -> 12754 [0x72, 0x80] -> 8072
|
|
value = []
|
|
for bch in _array:
|
|
hn, ln = f'{int(bch):x}'.rjust(2, '0')
|
|
value.append(int(ln, 16))
|
|
value.append(int(hn, 16))
|
|
return int("".join(str(i) for i in value[::-1]))
|
|
|
|
def _int_to_bch(self, _int):
|
|
# 8072 -> 0x72 0x80 , 12754 -> 0x54 0xc7
|
|
if _int >= 10000:
|
|
a, b, c, d, e = str(_int)
|
|
ab = f'{int(a+b):x}'
|
|
return [int(d+e, 16), int(ab+c, 16)]
|
|
else:
|
|
str_i = str(_int).rjust(4, '0')
|
|
return [int(str_i[2:], 16), int(str_i[:2], 16)]
|
|
|
|
def get_tone__mem(self, _mem, mem):
|
|
_memrxtone = self._bch_to_int(_mem.rx_tone)
|
|
if _memrxtone == 15151515 or _memrxtone == 0:
|
|
rxtone = ("", 0, None)
|
|
elif 8000 < _memrxtone < 12000:
|
|
# DCS Normal
|
|
rxtone = ("DTCS", _memrxtone % 8000, "N")
|
|
elif _memrxtone > 12000:
|
|
# DCS Inverted
|
|
rxtone = ("DTCS", _memrxtone % 12000, "R")
|
|
else:
|
|
# CTCSS
|
|
rxtone = ("Tone", _memrxtone / 10, None)
|
|
|
|
_memtxtone = self._bch_to_int(_mem.tx_tone)
|
|
if _memtxtone == 15151515 or _memtxtone == 0:
|
|
txtone = ("", 0, None)
|
|
elif 8000 < _memtxtone < 12000:
|
|
# DCS Normal
|
|
txtone = ("DTCS", _memtxtone % 8000, "N")
|
|
elif _memtxtone > 12000:
|
|
# DCS Inverted
|
|
txtone = ("DTCS", _memtxtone % 12000, "R")
|
|
else:
|
|
# CTCSS
|
|
txtone = ("Tone", _memtxtone / 10, None)
|
|
|
|
chirp_common.split_tone_decode(mem, txtone, rxtone)
|
|
|
|
def validate_memory(self, mem):
|
|
if 31 <= mem.number <= 54:
|
|
# DIY RPTR
|
|
if mem.freq not in self.GMRS_RPTR:
|
|
return [chirp_common.ValidationError(
|
|
'Only GMRS repeater freq. permitted'
|
|
' on channels 31 - 54')]
|
|
return super().validate_memory(mem)
|
|
|
|
def set_memory(self, mem):
|
|
number = mem.number
|
|
if 0 < mem.number <= 250:
|
|
_mem = self._memobj.memory[number-1]
|
|
_name = self._memobj.memnames[number-1]
|
|
newname = [ord(c) for c in mem.name]
|
|
newname = newname + [0] * (6 - len(newname))
|
|
_name.name = newname
|
|
else:
|
|
_mem = self._memobj.vfomemory[number-251]
|
|
|
|
if mem.empty:
|
|
_mem.fill_raw(b'\xff')
|
|
return
|
|
|
|
_mem.rx_freq = mem.freq // 10
|
|
|
|
if mem.duplex == "+":
|
|
_mem.tx_freq = (mem.freq + mem.offset) // 10
|
|
|
|
elif mem.duplex == "-":
|
|
_mem.tx_freq = (mem.freq - mem.offset) // 10
|
|
|
|
elif mem.duplex == "":
|
|
_mem.tx_freq = 16666665
|
|
|
|
if mem.mode is not None:
|
|
_mem.mode = self.VALID_MODES.index(mem.mode)
|
|
else:
|
|
_mem.mode = 0
|
|
if mem.power is not None:
|
|
_mem.power = self.POWER_LEVELS.index(mem.power)
|
|
else:
|
|
_mem.power = 0
|
|
_mem.scan = False if mem.skip == "S" else True
|
|
|
|
self.set_tones__mem(_mem, mem)
|
|
|
|
# extra settings
|
|
for setting in mem.extra:
|
|
setattr(_mem, setting.get_name(), setting.value)
|
|
|
|
def set_tones__mem(self, _mem, mem):
|
|
# sets tones in _mem from ui edit
|
|
((txmode, txval, txpol),
|
|
(rxmode, rxval, rxpol)) = chirp_common.split_tone_encode(mem)
|
|
if txmode == "":
|
|
_mem.tx_tone = [0xFF, 0xFF]
|
|
if rxmode == "":
|
|
_mem.rx_tone = [0xFF, 0xFF]
|
|
if txmode == "Tone":
|
|
_mem.tx_tone = self._int_to_bch(int(txval * 10))
|
|
if rxmode == "Tone":
|
|
_mem.rx_tone = self._int_to_bch(int(rxval * 10))
|
|
if txmode == "DTCS" and txpol == "N":
|
|
_mem.tx_tone = self._int_to_bch(int(txval + 8000))
|
|
if rxmode == "DTCS" and rxpol == "N":
|
|
_mem.rx_tone = self._int_to_bch(int(rxval + 8000))
|
|
if txmode == "DTCS" and txpol == "R":
|
|
_mem.tx_tone = self._int_to_bch(int(txval + 12000))
|
|
if rxmode == "DTCS" and rxpol == "R":
|
|
_mem.rx_tone = self._int_to_bch(int(rxval + 12000))
|
|
|
|
def get_settings(self):
|
|
|
|
_settings = self._memobj.settings
|
|
_dtmf = self._memobj.dtmf
|
|
_dtmf_list = self._memobj.dtmf_list
|
|
|
|
gsettings = RadioSettingGroup("gsettings", "General Settings")
|
|
group = RadioSettings(gsettings)
|
|
|
|
_options = ["Logo", "Message", "Voltage"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.bootscrmode)
|
|
rset = RadioSetting("settings.bootscrmode", "Boot Screen Mode", rs)
|
|
gsettings.append(rset)
|
|
|
|
_current = "".join(chr(i) for i in _settings.bootscreen1
|
|
if chr(i) in self.VALID_CHARSET)
|
|
rs = RadioSettingValueString(minlength=0, maxlength=10,
|
|
current=_current,
|
|
charset=self.VALID_CHARSET,
|
|
mem_pad_char=' ')
|
|
rset = RadioSetting("settings.bootscreen1",
|
|
"Boot Screen 1", rs)
|
|
gsettings.append(rset)
|
|
|
|
_current = "".join(chr(i) for i in _settings.bootscreen2
|
|
if chr(i) in self.VALID_CHARSET)
|
|
rs = RadioSettingValueString(minlength=0, maxlength=10,
|
|
current=_current,
|
|
charset=self.VALID_CHARSET,
|
|
mem_pad_char=' ')
|
|
rset = RadioSetting("settings.bootscreen2",
|
|
"Boot Screen 2", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = [str(i) for i in range(0, 601, 15)]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.timeout)
|
|
rset = RadioSetting("settings.timeout", "Timeout (s)", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueInteger(minval=0, maxval=9,
|
|
current=_settings.squelch, step=1)
|
|
rset = RadioSetting("settings.squelch", "Squelch Level", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueInteger(minval=0, maxval=9,
|
|
current=_settings.vox_level, step=1)
|
|
rset = RadioSetting("settings.vox_level", "Vox Level", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.voice_alert, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.voice_alert", "Voice Alert", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["Freq. Mode", "Ch. Mode"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.work_mode)
|
|
rset = RadioSetting("settings.work_mode", "Display Mode", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["None", "1:1", "1:2", "1:3", "1:4"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.batt_save)
|
|
rset = RadioSetting("settings.batt_save", "Battery Save Mode", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["Bright", "1", "2", "3", "4", "5",
|
|
"6", "7", "8", "9", "10"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.backlight)
|
|
rset = RadioSetting("settings.backlight", "Backlight", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.auto_key_lock, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.auto_key_lock", "Auto Key Lock", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["Off", "DT-ST", "ANI-ST", "DT+ANI"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.side_tone)
|
|
rset = RadioSetting("settings.side_tone", "DTMF Side Tone", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["Time", "Carrier", "Search"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.scan_type)
|
|
rset = RadioSetting("settings.scan_type", "Scan Type", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.ctcss_revert, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.ctcss_revert", "CTCSS Tail Revert", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.beep_tone, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.beep_tone", "Beep Tone", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["On Site", "Send Sound", "Send Code"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.alarm_mode)
|
|
rset = RadioSetting("settings.alarm_mode", "Alarm Mode", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.fm_radio, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.fm_radio", "FM Radio", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.roger, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.roger", "Roger Beep", rs)
|
|
gsettings.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.standby, mem_vals=(0, 1))
|
|
rset = RadioSetting("settings.standby", "Dual Standby", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = [str(i) for i in range(0, 1001, 100)]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.tail_revert)
|
|
rset = RadioSetting("settings.tail_revert",
|
|
"Repeater Tail Revert (ms)", rs)
|
|
gsettings.append(rset)
|
|
|
|
# same options as tail_rvt
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.tail_delay)
|
|
rset = RadioSetting("settings.tail_delay",
|
|
"Repeater Tail Delay (ms)", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["1000", "1450", "1750", "2100"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.tbst)
|
|
rset = RadioSetting("settings.tbst", "Tone Burst", rs)
|
|
gsettings.append(rset)
|
|
|
|
_options = ["Name + Number", "Freq. + Number"]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.a_ch_disp)
|
|
rset = RadioSetting("settings.a_ch_disp", "A Channel Display Type", rs)
|
|
gsettings.append(rset)
|
|
|
|
# same options as a_chan_disp
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_settings.b_ch_disp)
|
|
rset = RadioSetting("settings.b_ch_disp", "B Channel Display Type", rs)
|
|
gsettings.append(rset)
|
|
|
|
# DTMF Menu
|
|
dtmf = RadioSettingGroup("dtmf", "DTMF")
|
|
group.append(dtmf)
|
|
|
|
def _dtmf_decode(setting, pad_len):
|
|
_map = list(range(10)) + ['A', 'B', 'C', 'D', '*', '#']
|
|
s = ""
|
|
for i in setting:
|
|
_i = int(i)
|
|
if _i < len(_map):
|
|
s += str(_map[_i])
|
|
return s.ljust(pad_len)
|
|
|
|
_current = _dtmf_decode(_dtmf.radio_id, 5)
|
|
rs = RadioSettingValueString(
|
|
minlength=5, maxlength=5, current=_current)
|
|
rset = RadioSetting("dtmf.radio_id", "Radio ID", rs)
|
|
dtmf.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_dtmf.press_send, mem_vals=(0, 1))
|
|
rset = RadioSetting("dtmf.press_send", "PTT Press Send", rs)
|
|
dtmf.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_dtmf.release_send, mem_vals=(0, 1))
|
|
rset = RadioSetting("dtmf.release_send", "PTT Release Send", rs)
|
|
dtmf.append(rset)
|
|
|
|
_options = [str(i) for i in range(100, 1010, 50)]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_dtmf.delay_time)
|
|
rset = RadioSetting("dtmf.delay_time", "Delay Time (ms)", rs)
|
|
dtmf.append(rset)
|
|
|
|
_options = [str(i) for i in range(80, 2010, 10)]
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_dtmf.digit_dur)
|
|
rset = RadioSetting("dtmf.digit_dur", "Digit Duration (ms)", rs)
|
|
dtmf.append(rset)
|
|
|
|
# uses same options as digit_dur
|
|
rs = RadioSettingValueList(_options,
|
|
current_index=_dtmf.inter_dur)
|
|
rset = RadioSetting(
|
|
"dtmf.inter_dur", "Digit Interval Duration (ms)", rs)
|
|
dtmf.append(rset)
|
|
|
|
# DTMF Entries List
|
|
dtmflist = RadioSettingGroup("dtmflist", "DTMF List")
|
|
group.append(dtmflist)
|
|
for i in range(0, 15): # Entries # 1-15
|
|
rs = RadioSettingValueString(minlength=0, maxlength=5,
|
|
current=_dtmf_decode(
|
|
_dtmf_list[i].entry, 5),
|
|
charset=self.VALID_DTMF + [" "])
|
|
rset = RadioSetting(f"dtmf_list[{i}].entry", f"Entry {i+1}", rs)
|
|
dtmflist.append(rset)
|
|
|
|
# Password Menu
|
|
pro = RadioSettingGroup("protect", "Protect")
|
|
group.append(pro)
|
|
|
|
def _ascii_num_filter(setting):
|
|
s = ""
|
|
for i in setting:
|
|
if chr(i) in self.ASCII_NUM:
|
|
s += str(chr(i))
|
|
return s
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.passw_w_ena, mem_vals=(0, 1))
|
|
rs.set_mutable(False)
|
|
rset = RadioSetting("settings.passw_w_ena", "Write Protect", rs)
|
|
pro.append(rset)
|
|
|
|
_current = _ascii_num_filter(_settings.passw_w_val)
|
|
rs = RadioSettingValueString(
|
|
minlength=0, maxlength=8, current=_current,
|
|
charset=self.ASCII_NUM)
|
|
rs.set_mutable(False)
|
|
rset = RadioSetting("settings.passw_w_val", "Write Password", rs)
|
|
pro.append(rset)
|
|
|
|
rs = RadioSettingValueBoolean(
|
|
current=_settings.passw_r_ena, mem_vals=(0, 1))
|
|
rs.set_mutable(False)
|
|
rset = RadioSetting("settings.passw_r_ena", "Read Protect", rs)
|
|
pro.append(rset)
|
|
|
|
_current = _ascii_num_filter(_settings.passw_r_val)
|
|
rs = RadioSettingValueString(
|
|
minlength=0, maxlength=8, current=_current,
|
|
charset=self.ASCII_NUM)
|
|
rs.set_mutable(False)
|
|
rset = RadioSetting("settings.passw_r_val", "Read Password", rs)
|
|
pro.append(rset)
|
|
|
|
return group
|
|
|
|
def ff_pad__mem(self, obj, setting, element, charset):
|
|
""" set_settings helper for 0xff padded elements
|
|
also remove space chars
|
|
"""
|
|
_charset = [c for c in list(charset) if c != ' ']
|
|
_val = [0xff] * len(obj[setting])
|
|
for i in range(len(obj[setting])):
|
|
if element.value[i] in _charset:
|
|
_val[i] = ord(element.value[i])
|
|
setattr(obj, setting, _val)
|
|
|
|
def set_settings(self, settings):
|
|
for element in settings:
|
|
if not isinstance(element, RadioSetting):
|
|
self.set_settings(element)
|
|
continue
|
|
else:
|
|
try:
|
|
if "." in element.get_name():
|
|
toks = element.get_name().split(".")
|
|
obj = self._memobj
|
|
for tok in toks[:-1]:
|
|
if '[' in tok:
|
|
t, i = tok.split("[")
|
|
i = int(i[:-1])
|
|
obj = getattr(obj, t)[i]
|
|
else:
|
|
obj = getattr(obj, tok)
|
|
setting = toks[-1]
|
|
else:
|
|
obj = self._memobj.settings
|
|
setting = element.get_name()
|
|
|
|
if element.has_apply_callback():
|
|
LOG.debug("applying callback")
|
|
element.run_apply_callback()
|
|
|
|
if element.value.get_mutable():
|
|
if setting in ['passw_w_val', 'passw_r_val']:
|
|
self.ff_pad__mem(
|
|
obj, setting, element, self.ASCII_NUM)
|
|
elif setting in ['bootscreen1', 'bootscreen2']:
|
|
self.ff_pad__mem(
|
|
obj, setting, element, self.VALID_CHARSET)
|
|
elif setting == 'entry':
|
|
self.ff_pad__mem(
|
|
obj, setting, element, self.VALID_DTMF)
|
|
else:
|
|
setattr(obj, setting, element.value)
|
|
except Exception:
|
|
LOG.debug(element.get_name())
|
|
raise
|