Project

General

Profile

Bug #10093 » tk8180.py

Dan Smith, 10/23/2022 05:35 PM

 
# Copyright 2019 Dan Smith <dsmith@danplanet.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import struct
import os
import time
import logging
from collections import OrderedDict

from chirp import chirp_common, directory, memmap, errors, util
from chirp import bitwise
from chirp.settings import RadioSettingGroup, RadioSetting
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
from chirp.settings import RadioSettingValueString, RadioSettingValueInteger
from chirp.settings import RadioSettings

LOG = logging.getLogger(__name__)

# Gross hack to handle missing future module on un-updatable
# platforms like MacOS. Just avoid registering these radio
# classes for now.
try:
from builtins import bytes
has_future = True
except ImportError:
has_future = False
LOG.debug('python-future package is not '
'available; %s requires it' % __name__)


HEADER_FORMAT = """
#seekto 0x0100;
struct {
char sw_name[7];
char sw_ver[5];
u8 unknown1[4];
char sw_key[12];
u8 unknown2[4];
char model[5];
u8 variant;
u8 unknown3[10];
} header;

#seekto 0x0140;
struct {
// 0x0140
u8 unknown1;
u8 sublcd;
u8 unknown2[30];

// 0x0160
char pon_msgtext[12];
u8 min_volume;
u8 max_volume;
u8 lo_volume;
u8 hi_volume;

// 0x0170
u8 tone_volume_offset;
u8 poweron_tone;
u8 control_tone;
u8 warning_tone;
u8 alert_tone;
u8 sidetone;
u8 locator_tone;
u8 unknown3[2];
u8 ignition_mode;
u8 ignition_time; // In tens of minutes (6 = 1h)
u8 micsense;
ul16 modereset;
u8 min_vol_preset;
u8 unknown4;

// 0x0180
u8 unknown5[16];

// 0x0190
u8 unknown6[3];
u8 pon_msgtype;
u8 unknown7[8];
u8 unknown8_1:2,
ssi:1,
busy_led:1,
power_switch_memory:1,
scrambler_memory:1,
unknown8_2:1,
off_hook_decode:1;
u8 unknown9_1:5,
clockfmt:1,
datefmt:1,
ignition_sense:1;
u8 unknownA[2];

// 0x01A0
u8 unknownB[8];
u8 ptt_timer;
u8 unknownB2[3];
u8 ptt_proceed:1,
unknownC_1:3,
tone_off:1,
ost_memory:1,
unknownC_2:1,
ptt_release:1;
u8 unknownD[3];
} settings;

#seekto 0x01E0;
struct {
char name[12];
ul16 rxtone;
ul16 txtone;
} ost_tones[40];

#seekto 0x0A00;
ul16 zone_starts[128];

struct zoneinfo {
u8 number;
u8 zonetype;
u8 unknown1[2];
u8 count;
char name[12];
u8 unknown2[2];
ul16 timeout; // 15-1200
ul16 tot_alert; // 10
ul16 tot_rekey; // 60
ul16 tot_reset; // 15
u8 unknown3[3];
u8 unknown21:2,
bcl_override:1,
unknown22:5;
u8 unknown5;
};

struct memory {
u8 number;
lbcd rx_freq[4];
lbcd tx_freq[4];
u8 unknown1[2];
ul16 rx_tone;
ul16 tx_tone;
char name[12];
u8 unknown2[19];
u8 unknown3_1:4,
highpower:1,
unknown3_2:1,
wide:1,
unknown3_3:1;
u8 unknown4;
};

#seekto 0xC570; // Fixme
u8 skipflags[64];
"""


SYSTEM_MEM_FORMAT = """
#seekto 0x%(addr)x;
struct {
struct zoneinfo zoneinfo;
struct memory memories[%(count)i];
} zone%(index)i;
"""

STARTUP_MODES = ['Text', 'Clock']

VOLUMES = OrderedDict([(str(x), x) for x in range(0, 30)])
VOLUMES.update({'Selectable': 0x30,
'Current': 0xFF})
VOLUMES_REV = {v: k for k, v in VOLUMES.items()}

MIN_VOL_PRESET = {'Preset': 0x30,
'Lowest Limit': 0x31}
MIN_VOL_PRESET_REV = {v: k for k, v in MIN_VOL_PRESET.items()}

SUBLCD = ['Zone Number', 'CH/GID Number', 'OSD List Number']
CLOCKFMT = ['12H', '24H']
DATEFMT = ['Day/Month', 'Month/Day']
MICSENSE = ['On']
ONLY_MOBILE_SETTINGS = ['power_switch_memory', 'off_hook_decode',
'ignition_sense', 'mvp', 'it', 'ignition_mode']


POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
chirp_common.PowerLevel("High", watts=50)]


def set_choice(setting, obj, key, choices, default='Off'):
settingstr = str(setting.value)
if settingstr == default:
val = 0xFF
else:
val = choices.index(settingstr) + 0x30
setattr(obj, key, val)


def get_choice(obj, key, choices, default='Off'):
val = getattr(obj, key)
if val == 0xFF:
return default
else:
return choices[val - 0x30]


def make_frame(cmd, addr, data=b""):
return struct.pack(">BH", ord(cmd), addr) + data


def send(radio, frame):
# LOG.debug("%04i P>R:\n%s" % (len(frame), util.hexprint(frame)))
radio.pipe.write(frame)


def do_ident(radio):
radio.pipe.baudrate = 9600
radio.pipe.stopbits = 2
radio.pipe.timeout = 1
send(radio, b'PROGRAM')
ack = radio.pipe.read(1)
LOG.debug('Read %r from radio' % ack)
if ack != b'\x16':
raise errors.RadioError('Radio refused hi-speed program mode')
radio.pipe.baudrate = 19200
ack = radio.pipe.read(1)
if ack != b'\x06':
raise errors.RadioError('Radio refused program mode')
radio.pipe.write(b'\x02')
ident = radio.pipe.read(8)
LOG.debug('Radio ident is %r' % ident)
radio.pipe.write(b'\x06')
ack = radio.pipe.read(1)
if ack != b'\x06':
raise errors.RadioError('Radio refused program mode')
if ident[:6] not in (radio._model,):
model = ident[:5].decode()
variants = {b'\x06': 'K, K1, K3 (450-520MHz)',
b'\x07': 'K2, K4 (400-470MHz)'}
if model == 'P3180':
model += ' ' + variants.get(ident[5], '(Unknown)')
raise errors.RadioError('Unsupported radio model %s' % model)


def checksum_data(data):
_chksum = 0
for byte in data:
_chksum = (_chksum + byte) & 0xFF
return _chksum


def do_download(radio):
do_ident(radio)

data = bytes()

def status():
status = chirp_common.Status()
status.cur = len(data)
status.max = radio._memsize
status.msg = "Cloning from radio"
radio.status_fn(status)
LOG.debug('Radio address 0x%04x' % len(data))

# Addresses 0x0000-0xBF00 pulled by block number (divide by 0x100)
for block in range(0, 0xBF + 1):
send(radio, make_frame('R', block))
cmd = radio.pipe.read(1)
chunk = b''
if cmd == b'Z':
data += bytes(b'\xff' * 256)
LOG.debug('Radio reports empty block %02x' % block)
elif cmd == b'W':
chunk = bytes(radio.pipe.read(256))
if len(chunk) != 256:
LOG.error('Received %i for block %02x' % (len(chunk), block))
raise errors.RadioError('Radio did not send block')
data += chunk
else:
LOG.error('Radio sent %r (%02x), expected W(0x57)' % (cmd,
chr(cmd)))
raise errors.RadioError('Radio sent unexpected response')

LOG.debug('Read block index %02x' % block)
status()

chksum = radio.pipe.read(1)
if len(chksum) != 1:
LOG.error('Checksum was %r' % chksum)
raise errors.RadioError('Radio sent invalid checksum')
_chksum = checksum_data(chunk)

if chunk and _chksum != ord(chksum):
LOG.error(
'Checksum failed for %i byte block 0x%02x: %02x != %02x' % (
len(chunk), block, _chksum, ord(chksum)))
raise errors.RadioError('Checksum failure while reading block. '
'Check serial cable.')

radio.pipe.write(b'\x06')
if radio.pipe.read(1) != b'\x06':
raise errors.RadioError('Post-block exchange failed')

# Addresses 0xC000 - 0xD1F0 pulled by address
for block in range(0x0100, 0x1200, 0x40):
send(radio, make_frame('S', block, b'\x40'))
x = radio.pipe.read(1)
if x != b'X':
raise errors.RadioError('Radio did not send block')
chunk = radio.pipe.read(0x40)
data += chunk

LOG.debug('Read memory address %04x' % block)
status()

radio.pipe.write(b'\x06')
if radio.pipe.read(1) != b'\x06':
raise errors.RadioError('Post-block exchange failed')

radio.pipe.write(b'E')
if radio.pipe.read(1) != b'\x06':
raise errors.RadioError('Radio failed to acknowledge completion')

LOG.debug('Read %i bytes total' % len(data))
return data


def do_upload(radio):
do_ident(radio)

def status(addr):
status = chirp_common.Status()
status.cur = addr
status.max = radio._memsize
status.msg = "Cloning to radio"
radio.status_fn(status)

for block in range(0, 0xBF + 1):
addr = block * 0x100
chunk = bytes(radio._mmap[addr:addr + 0x100])
if all(byte == b'\xff' for byte in chunk):
LOG.debug('Sending zero block %i, range 0x%04x' % (block, addr))
send(radio, make_frame('Z', block, b'\xFF'))
else:
checksum = checksum_data(chunk)
send(radio, make_frame('W', block, chunk + chr(checksum)))

ack = radio.pipe.read(1)
if ack != b'\x06':
LOG.error('Radio refused block 0x%02x with %r' % (block, ack))
raise errors.RadioError('Radio refused data block')

status(addr)

addr_base = 0xC000
for addr in range(addr_base, radio._memsize, 0x40):
block_addr = addr - addr_base + 0x0100
chunk = radio._mmap[addr:addr + 0x40]
send(radio, make_frame('X', block_addr, b'\x40' + chunk))

ack = radio.pipe.read(1)
if ack != b'\x06':
LOG.error('Radio refused address 0x%02x with %r' % (block_addr,
ack))
raise errors.RadioError('Radio refused data block')

status(addr)

radio.pipe.write(b'E')
if radio.pipe.read(1) != b'\x06':
raise errors.RadioError('Radio failed to acknowledge completion')


def reset(self):
try:
self.pipe.baudrate = 9600
self.pipe.write(b'E')
time.sleep(0.5)
self.pipe.baudrate = 19200
self.pipe.write(b'E')
except Exception:
LOG.error('Unable to send reset sequence')


class KenwoodTKx180Radio(chirp_common.CloneModeRadio):
"""Kenwood TK-x180"""
VENDOR = 'Kenwood'
MODEL = 'TK-x180'
BAUD_RATE = 9600
NEEDS_COMPAT_SERIAL = False

_system_start = 0x0B00
_memsize = 0xD100

def __init__(self, *a, **k):
self._zones = []
chirp_common.CloneModeRadio.__init__(self, *a, **k)

def sync_in(self):
try:
data = do_download(self)
self._mmap = memmap.MemoryMapBytes(data)
except errors.RadioError:
reset(self)
raise
except Exception as e:
reset(self)
LOG.exception('General failure')
raise errors.RadioError('Failed to download from radio: %s' % e)
self.process_mmap()

def sync_out(self):
try:
do_upload(self)
except Exception as e:
reset(self)
LOG.exception('General failure')
raise errors.RadioError('Failed to upload to radio: %s' % e)

@property
def is_portable(self):
return self._model.startswith(b'P')

def probe_layout(self):
start_addrs = []
tmp_format = '#seekto 0x0A00; ul16 zone_starts[128];'
mem = bitwise.parse(tmp_format, self._mmap)
zone_format = """struct zoneinfo {
u8 number;
u8 zonetype;
u8 unknown1[2];
u8 count;
char name[12];
u8 unknown2[15];
};"""

zone_addresses = []
for i in range(0, 128):
if mem.zone_starts[i] == 0xFFFF:
break
zone_addresses.append(mem.zone_starts[i])
zone_format += '#seekto 0x%x; struct zoneinfo zone%i;' % (
mem.zone_starts[i], i)

zoneinfo = bitwise.parse(zone_format, self._mmap)
zones = []
for i, addr in enumerate(zone_addresses):
zone = getattr(zoneinfo, 'zone%i' % i)
if zone.zonetype != 0x31:
LOG.error('Zone %i is type 0x%02x; '
'I only support 0x31 (conventional)')
raise errors.RadioError(
'Unsupported non-conventional zone found in radio; '
'Refusing to load to safeguard your data!')
zones.append((addr, zone.count))

LOG.debug('Zones: %s' % zones)
return zones

def process_mmap(self):
self._zones = self.probe_layout()

mem_format = HEADER_FORMAT
for index, (addr, count) in enumerate(self._zones):
mem_format += '\n\n' + (
SYSTEM_MEM_FORMAT % {
'addr': addr,
'count': max(count, 2), # bitwise bug, one-element array
'index': index})

self._memobj = bitwise.parse(mem_format, self._mmap)

def expand_mmap(self, zone_sizes):
"""Remap memory into zones of the specified sizes, copying things
around to keep the contents, as appropriate."""
old_zones = self._zones
old_memobj = self._memobj

self._mmap = memmap.MemoryMapBytes(bytes(self._mmap.get_packed()))

new_format = HEADER_FORMAT
addr = self._system_start
self._zones = []
for index, count in enumerate(zone_sizes):
new_format += SYSTEM_MEM_FORMAT % {
'addr': addr,
'count': max(count, 2), # bitwise bug
'index': index}
self._zones.append((addr, count))
addr += 0x20 + (count * 0x30)

self._memobj = bitwise.parse(new_format, self._mmap)

# Set all known zone addresses and clear the rest
for index in range(0, 128):
try:
self._memobj.zone_starts[index] = self._zones[index][0]
except IndexError:
self._memobj.zone_starts[index] = 0xFFFF

for zone_number, count in enumerate(zone_sizes):
dest_zone = getattr(self._memobj, 'zone%i' % zone_number)
dest = dest_zone.memories
dest_zoneinfo = dest_zone.zoneinfo

if zone_number < len(old_zones):
LOG.debug('Copying existing zone %i' % zone_number)
_, old_count = old_zones[zone_number]
source_zone = getattr(old_memobj, 'zone%i' % zone_number)
source = source_zone.memories
source_zoneinfo = source_zone.zoneinfo

if old_count != count:
LOG.debug('Zone %i going from %i to %i' % (zone_number,
old_count,
count))

# Copy the zone record from the source, but then update
# the count
dest_zoneinfo.set_raw(source_zoneinfo.get_raw())
dest_zoneinfo.count = count

source_i = 0
for dest_i in range(0, min(count, old_count)):
dest[dest_i].set_raw(source[dest_i].get_raw())
else:
LOG.debug('New zone %i' % zone_number)
dest_zone.zoneinfo.number = zone_number + 1
dest_zone.zoneinfo.zonetype = 0x31
dest_zone.zoneinfo.count = count
dest_zone.zoneinfo.name = (
'Zone %i' % (zone_number + 1)).ljust(12)

def shuffle_zone(self):
"""Sort the memories in the zone according to logical channel number"""
# FIXME: Move this to the zone
raw_memories = self.raw_memories
memories = [(i, raw_memories[i].number)
for i in range(0, self.raw_zoneinfo.count)]
current = memories[:]
memories.sort(key=lambda t: t[1])
if current == memories:
LOG.debug('Shuffle not required')
return
raw_data = [raw_memories[i].get_raw() for i, n in memories]
for i, raw_mem in enumerate(raw_data):
raw_memories[i].set_raw(raw_mem)

@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.info = ('This radio is zone-based, which is different from how '
'most radios work (that CHIRP supports). The zone count '
'can be adjusted in the Settings tab, but you must save '
'and re-load the file after changing that value in order '
'to be able to add/edit memories there.')
rp.experimental = ('This driver is very experimental. Every attempt '
'has been made to be overly pedantic to avoid '
'destroying data. However, you should use caution, '
'maintain backups, and proceed at your own risk.')
return rp

def get_features(self):
rf = chirp_common.RadioFeatures()
rf.has_ctone = True
rf.has_cross = True
rf.has_tuning_step = False
rf.has_settings = True
rf.has_bank = False
rf.has_sub_devices = True
rf.has_rx_dtcs = True
rf.can_odd_split = True
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = ['Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS',
'DTCS->Tone', '->Tone', 'DTCS->DTCS']
rf.valid_bands = self.VALID_BANDS
rf.valid_modes = ['FM', 'NFM']
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 12.5, 10.0, 15.0, 20.0,
25.0, 50.0, 100.0]
rf.valid_duplexes = ['', '-', '+', 'split', 'off']
rf.valid_power_levels = POWER_LEVELS
rf.valid_name_length = 12
rf.valid_characters = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'abcdefghijklmnopqrstuvwxyz'
'0123456789'
'!"#$%&\'()~+-,./:;<=>?@[\\]^`{}*| ')
rf.memory_bounds = (1, 512)
return rf

@property
def raw_zone(self):
return getattr(self._memobj, 'zone%i' % self._zone)

@property
def raw_zoneinfo(self):
return self.raw_zone.zoneinfo

@property
def raw_memories(self):
return self.raw_zone.memories

@property
def max_mem(self):
return self.raw_memories[self.raw_zoneinfo.count].number

def _get_raw_memory(self, number):
for i in range(0, self.raw_zoneinfo.count):
if self.raw_memories[i].number == number:
return self.raw_memories[i]
return None

def get_raw_memory(self, number):
return repr(self._get_raw_memory(number))

@staticmethod
def _decode_tone(toneval):
# DCS examples:
# D024N - 2814 - 0010 1000 0001 0100
# ^--DCS
# D024I - A814 - 1010 1000 0001 0100
# ^----inverted
# D754I - A9EC - 1010 1001 1110 1100
# code in octal-------^^^^^^^^^^^

pol = toneval & 0x8000 and 'R' or 'N'
if toneval == 0xFFFF:
return '', None, None
elif toneval & 0x2000:
# DTCS
code = int('%o' % (toneval & 0x1FF))
return 'DTCS', code, pol
else:
return 'Tone', toneval / 10.0, None

@staticmethod
def _encode_tone(mode, val, pol):
if not mode:
return 0xFFFF
elif mode == 'Tone':
return int(val * 10)
elif mode == 'DTCS':
code = int('%i' % val, 8)
code |= 0x2800
if pol == 'R':
code |= 0x8000
return code
else:
raise errors.RadioError('Unsupported tone mode %r' % mode)

def get_memory(self, number):
mem = chirp_common.Memory()
mem.number = number
_mem = self._get_raw_memory(number)
if _mem is None:
mem.empty = True
return mem

mem.name = str(_mem.name).rstrip('\x00')
mem.freq = int(_mem.rx_freq) * 10
chirp_common.split_tone_decode(mem,
self._decode_tone(_mem.tx_tone),
self._decode_tone(_mem.rx_tone))
if _mem.wide:
mem.mode = 'FM'
else:
mem.mode = 'NFM'

mem.power = POWER_LEVELS[_mem.highpower]

offset = (int(_mem.tx_freq) - int(_mem.rx_freq)) * 10
if offset == 0:
mem.duplex = ''
elif abs(offset) < 10000000:
mem.duplex = offset < 0 and '-' or '+'
mem.offset = abs(offset)
else:
mem.duplex = 'split'
mem.offset = int(_mem.tx_freq) * 10

skipbyte = self._memobj.skipflags[(mem.number - 1) // 8]
skipbit = skipbyte & (1 << (mem.number - 1) % 8)
mem.skip = skipbit and 'S' or ''

return mem

def set_memory(self, mem):
_mem = self._get_raw_memory(mem.number)
if _mem is None:
LOG.debug('Need to expand zone %i' % self._zone)

# Calculate the new zone sizes and remap memory
new_zones = [x[1] for x in self._parent._zones]
new_zones[self._zone] = new_zones[self._zone] + 1
self._parent.expand_mmap(new_zones)

# Assign the new memory (at the end) to the desired
# number
_mem = self.raw_memories[self.raw_zoneinfo.count - 1]
_mem.number = mem.number

# Sort the memory into place
self.shuffle_zone()

# Now find it in the right spot
_mem = self._get_raw_memory(mem.number)
if _mem is None:
raise errors.RadioError('Internal error after '
'memory allocation')

# Default values for unknown things
_mem.unknown1[0] = 0x36
_mem.unknown1[1] = 0x36
_mem.unknown2 = [0xFF for i in range(0, 19)]
_mem.unknown3_1 = 0xF
_mem.unknown3_2 = 0x1
_mem.unknown3_3 = 0x0
_mem.unknown4 = 0xFF

if mem.empty:
LOG.debug('Need to shrink zone %i' % self._zone)
# Make the memory sort to the end, and sort the zone
_mem.number = 0xFF
self.shuffle_zone()

# Calculate the new zone sizes and remap memory
new_zones = [x[1] for x in self._parent._zones]
new_zones[self._zone] = new_zones[self._zone] - 1
self._parent.expand_mmap(new_zones)
return

_mem.name = mem.name[:12].encode().rstrip().ljust(12, b'\x00')
_mem.rx_freq = mem.freq // 10

txtone, rxtone = chirp_common.split_tone_encode(mem)
_mem.tx_tone = self._encode_tone(*txtone)
_mem.rx_tone = self._encode_tone(*rxtone)

_mem.wide = mem.mode == 'FM'
_mem.highpower = mem.power == POWER_LEVELS[1]

if mem.duplex == '':
_mem.tx_freq = mem.freq // 10
elif mem.duplex == 'split':
_mem.tx_freq = mem.offset // 10
elif mem.duplex == 'off':
_mem.tx_freq.set_raw(b'\xff\xff\xff\xff')
elif mem.duplex == '-':
_mem.tx_freq = (mem.freq - mem.offset) // 10
elif mem.duplex == '+':
_mem.tx_freq = (mem.freq + mem.offset) // 10
else:
raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex)

skipbyte = self._memobj.skipflags[(mem.number - 1) // 8]
if mem.skip == 'S':
skipbyte |= (1 << (mem.number - 1) % 8)
else:
skipbyte &= ~(1 << (mem.number - 1) % 8)

def _pure_choice_setting(self, settings_key, name, choices, default='Off'):
if default is not None:
ui_choices = [default] + choices
else:
ui_choices = choices
s = RadioSetting(
settings_key, name,
RadioSettingValueList(
ui_choices,
get_choice(self._memobj.settings, settings_key,
choices, default)))
s.set_apply_callback(set_choice, self._memobj.settings,
settings_key, choices, default)
return s

def _inverted_flag_setting(self, key, name, obj=None):
if obj is None:
obj = self._memobj.settings

def apply_inverted(setting, key):
setattr(obj, key, not int(setting.value))

v = not getattr(obj, key)
s = RadioSetting(
key, name,
RadioSettingValueBoolean(v))
s.set_apply_callback(apply_inverted, key)
return s

def _get_common1(self):
settings = self._memobj.settings
common1 = RadioSettingGroup('common1', 'Common 1')

common1.append(self._pure_choice_setting('sublcd',
'Sub LCD Display',
SUBLCD,
default='None'))

def apply_clockfmt(setting):
settings.clockfmt = CLOCKFMT.index(str(setting.value))

clockfmt = RadioSetting(
'clockfmt', 'Clock Format',
RadioSettingValueList(CLOCKFMT,
CLOCKFMT[settings.clockfmt]))
clockfmt.set_apply_callback(apply_clockfmt)
common1.append(clockfmt)

def apply_datefmt(setting):
settings.datefmt = DATEFMT.index(str(setting.value))

datefmt = RadioSetting(
'datefmt', 'Date Format',
RadioSettingValueList(DATEFMT,
DATEFMT[settings.datefmt]))
datefmt.set_apply_callback(apply_datefmt)
common1.append(datefmt)

common1.append(self._pure_choice_setting('micsense',
'Mic Sense High',
MICSENSE))

def apply_modereset(setting):
val = int(setting.value)
if val == 0:
val = 0xFFFF
settings.modereset = val

_modereset = int(settings.modereset)
if _modereset == 0xFFFF:
_modereset = 0
modereset = RadioSetting(
'modereset', 'Mode Reset Timer',
RadioSettingValueInteger(0, 300, _modereset))
modereset.set_apply_callback(apply_modereset)
common1.append(modereset)

inverted_flags = [('power_switch_memory', 'Power Switch Memory'),
('scrambler_memory', 'Scrambler Memory'),
('off_hook_decode', 'Off-Hook Decode'),
('ssi', 'Signal Strength Indicator'),
('ignition_sense', 'Ingnition Sense')]
for key, name in inverted_flags:
if self.is_portable and key in ONLY_MOBILE_SETTINGS:
# Skip settings that are not valid for portables
continue
common1.append(self._inverted_flag_setting(key, name))

if not self.is_portable and 'ignition_mode' in ONLY_MOBILE_SETTINGS:
common1.append(self._pure_choice_setting('ignition_mode',
'Ignition Mode',
['Ignition & SW',
'Ignition Only'],
None))

def apply_it(setting):
settings.ignition_time = int(setting.value) / 600

_it = int(settings.ignition_time) * 600
it = RadioSetting(
'it', 'Ignition Timer (s)',
RadioSettingValueInteger(10, 28800, _it))
it.set_apply_callback(apply_it)
if not self.is_portable and 'it' in ONLY_MOBILE_SETTINGS:
common1.append(it)

return common1

def _get_common2(self):
settings = self._memobj.settings
common2 = RadioSettingGroup('common2', 'Common 2')

def apply_ponmsgtext(setting):
settings.pon_msgtext = (
str(setting.value)[:12].strip().ljust(12, '\x00'))

common2.append(
self._pure_choice_setting('pon_msgtype', 'Power On Message Type',
STARTUP_MODES))

_text = str(settings.pon_msgtext).rstrip('\x00')
text = RadioSetting('settings.pon_msgtext',
'Power On Text',
RadioSettingValueString(
0, 12, _text))
text.set_apply_callback(apply_ponmsgtext)
common2.append(text)

def apply_volume(setting, key):
setattr(settings, key, VOLUMES[str(setting.value)])

volumes = {'poweron_tone': 'Power-on Tone',
'control_tone': 'Control Tone',
'warning_tone': 'Warning Tone',
'alert_tone': 'Alert Tone',
'sidetone': 'Sidetone',
'locator_tone': 'Locator Tone'}
for value, name in volumes.items():
setting = getattr(settings, value)
volume = RadioSetting('settings.%s' % value, name,
RadioSettingValueList(
VOLUMES.keys(),
VOLUMES_REV.get(int(setting), 0)))
volume.set_apply_callback(apply_volume, value)
common2.append(volume)

def apply_vol_level(setting, key):
setattr(settings, key, int(setting.value))

levels = {'lo_volume': 'Low Volume Level (Fixed Volume)',
'hi_volume': 'High Volume Level (Fixed Volume)',
'min_volume': 'Minimum Audio Volume',
'max_volume': 'Maximum Audio Volume'}
for value, name in levels.items():
setting = getattr(settings, value)
if 'Audio' in name:
minimum = 0
else:
minimum = 1
volume = RadioSetting(
'settings.%s' % value, name,
RadioSettingValueInteger(minimum, 31, int(setting)))
volume.set_apply_callback(apply_vol_level, value)
common2.append(volume)

def apply_vo(setting):
val = int(setting.value)
if val < 0:
val = abs(val) | 0x80
settings.tone_volume_offset = val

_voloffset = int(settings.tone_volume_offset)
if _voloffset & 0x80:
_voloffset = abs(_voloffset & 0x7F) * -1
voloffset = RadioSetting(
'tvo', 'Tone Volume Offset',
RadioSettingValueInteger(
-5, 5,
_voloffset))
voloffset.set_apply_callback(apply_vo)
common2.append(voloffset)

def apply_mvp(setting):
settings.min_vol_preset = MIN_VOL_PRESET[str(setting.value)]

_volpreset = int(settings.min_vol_preset)
volpreset = RadioSetting(
'mvp', 'Minimum Volume Type',
RadioSettingValueList(MIN_VOL_PRESET.keys(),
MIN_VOL_PRESET_REV[_volpreset]))
volpreset.set_apply_callback(apply_mvp)
if not self.is_portable and 'mvp' in ONLY_MOBILE_SETTINGS:
common2.append(volpreset)

return common2

def _get_conventional(self):
settings = self._memobj.settings

conv = RadioSettingGroup('conv', 'Conventional')
inverted_flags = [('busy_led', 'Busy LED'),
('ost_memory', 'OST Status Memory'),
('tone_off', 'Tone Off'),
('ptt_release', 'PTT Release tone'),
('ptt_proceed', 'PTT Proceed Tone')]
for key, name in inverted_flags:
conv.append(self._inverted_flag_setting(key, name))

def apply_pttt(setting):
settings.ptt_timer = int(setting.value)

pttt = RadioSetting(
'pttt', 'PTT Proceed Tone Timer (ms)',
RadioSettingValueInteger(0, 6000, int(settings.ptt_timer)))
pttt.set_apply_callback(apply_pttt)
conv.append(pttt)

self._get_ost(conv)

return conv

def _get_zones(self):
zones = RadioSettingGroup('zones', 'Zones')

zone_count = RadioSetting('_zonecount',
'Number of Zones',
RadioSettingValueInteger(
1, 128, len(self._zones)))
zone_count.set_doc('Number of zones in the radio. '
'Requires a save and re-load of the '
'file to take effect. Reducing this number '
'will DELETE memories in affected zones!')
zones.append(zone_count)

for i in range(len(self._zones)):
zone = RadioSettingGroup('zone%i' % i, 'Zone %i' % (i + 1))

_zone = getattr(self._memobj, 'zone%i' % i).zoneinfo
_name = str(_zone.name).rstrip('\x00')
name = RadioSetting('name%i' % i, 'Name',
RadioSettingValueString(0, 12, _name))
zone.append(name)

def apply_timer(setting, key, zone_number):
val = int(setting.value)
if val == 0:
val = 0xFFFF
_zone = getattr(self._memobj, 'zone%i' % zone_number).zoneinfo
setattr(_zone, key, val)

def collapse(val):
val = int(val)
if val == 0xFFFF:
val = 0
return val

timer = RadioSetting(
'timeout', 'Time-out Timer',
RadioSettingValueInteger(15, 1200, collapse(_zone.timeout)))
timer.set_apply_callback(apply_timer, 'timeout', i)
zone.append(timer)

timer = RadioSetting(
'tot_alert', 'TOT Pre-Alert',
RadioSettingValueInteger(0, 10, collapse(_zone.tot_alert)))
timer.set_apply_callback(apply_timer, 'tot_alert', i)
zone.append(timer)

timer = RadioSetting(
'tot_rekey', 'TOT Re-Key Time',
RadioSettingValueInteger(0, 60, collapse(_zone.tot_rekey)))
timer.set_apply_callback(apply_timer, 'tot_rekey', i)
zone.append(timer)

timer = RadioSetting(
'tot_reset', 'TOT Reset Time',
RadioSettingValueInteger(0, 15, collapse(_zone.tot_reset)))
timer.set_apply_callback(apply_timer, 'tot_reset', i)
zone.append(timer)

zone.append(self._inverted_flag_setting(
'bcl_override', 'BCL Override',
_zone))

zones.append(zone)

return zones

def _get_ost(self, parent):
tones = chirp_common.TONES[:]

def apply_tone(setting, index, which):
if str(setting.value) == 'Off':
val = 0xFFFF
else:
val = int(float(str(setting.value)) * 10)
setattr(self._memobj.ost_tones[index], '%stone' % which, val)

def _tones():
return ['Off'] + [str(x) for x in tones]

for i in range(0, 40):
_ost = self._memobj.ost_tones[i]
ost = RadioSettingGroup('ost%i' % i,
'OST %i' % (i + 1))

cur = str(_ost.name).rstrip('\x00')
name = RadioSetting('name%i' % i, 'Name',
RadioSettingValueString(0, 12, cur))
ost.append(name)

if _ost.rxtone == 0xFFFF:
cur = 'Off'
else:
cur = round(int(_ost.rxtone) / 10.0, 1)
if cur not in tones:
LOG.debug('Non-standard OST rx tone %i %s' % (i, cur))
tones.append(cur)
tones.sort()
rx = RadioSetting('rxtone%i' % i, 'RX Tone',
RadioSettingValueList(_tones(),
str(cur)))
rx.set_apply_callback(apply_tone, i, 'rx')
ost.append(rx)

if _ost.txtone == 0xFFFF:
cur = 'Off'
else:
cur = round(int(_ost.txtone) / 10.0, 1)
if cur not in tones:
LOG.debug('Non-standard OST tx tone %i %s' % (i, cur))
tones.append(cur)
tones.sort()
tx = RadioSetting('txtone%i' % i, 'TX Tone',
RadioSettingValueList(_tones(),
str(cur)))
tx.set_apply_callback(apply_tone, i, 'tx')
ost.append(tx)

parent.append(ost)

def get_settings(self):
settings = self._memobj.settings

zones = self._get_zones()
common1 = self._get_common1()
common2 = self._get_common2()
conv = self._get_conventional()
top = RadioSettings(zones, common1, common2, conv)
return top

def set_settings(self, settings):
for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue
elif element.get_name() == '_zonecount':
new_zone_count = int(element.value)
zone_sizes = [x[1] for x in self._zones[:new_zone_count]]
if len(self._zones) > new_zone_count:
self.expand_mmap(zone_sizes[:new_zone_count])
elif len(self._zones) < new_zone_count:
self.expand_mmap(zone_sizes + (
[0] * (new_zone_count - len(self._zones))))
elif element.has_apply_callback():
element.run_apply_callback()

def get_sub_devices(self):
zones = []
for i, _ in enumerate(self._zones):
zone = getattr(self._memobj, 'zone%i' % i)

class _Zone(KenwoodTKx180RadioZone):
VENDOR = self.VENDOR
MODEL = self.MODEL
VALID_BANDS = self.VALID_BANDS
VARIANT = 'Zone %s' % (
str(zone.zoneinfo.name).rstrip('\x00').rstrip())
_model = self._model

zones.append(_Zone(self, i))
return zones


class KenwoodTKx180RadioZone(KenwoodTKx180Radio):
_zone = None

def __init__(self, parent, zone=0):
if isinstance(parent, KenwoodTKx180Radio):
self._parent = parent
else:
LOG.warning('Parent was not actually our parent, expect failure')
self._zone = zone

@property
def _zones(self):
return self._parent._zones

@property
def _memobj(self):
return self._parent._memobj

def load_mmap(self, filename):
self._parent.load_mmap(filename)

def get_features(self):
rf = KenwoodTKx180Radio.get_features(self)
rf.has_sub_devices = False
rf.memory_bounds = (1, 250)
return rf

def get_sub_devices(self):
return []


if has_future:
@directory.register
class KenwoodTK7180Radio(KenwoodTKx180Radio):
MODEL = 'TK-7180'
VALID_BANDS = [(136000000, 174000000)]
_model = b'M7180\x04'

@directory.register
class KenwoodTK8180Radio(KenwoodTKx180Radio):
MODEL = 'TK-8180'
VALID_BANDS = [(400000000, 520000000)]
_model = b'M8180\x06'

@directory.register
class KenwoodTK2180Radio(KenwoodTKx180Radio):
MODEL = 'TK-2180'
VALID_BANDS = [(136000000, 174000000)]
_model = b'P2180\x04'

# K1,K3 are technically 450-470 (K3 == keypad)
@directory.register
class KenwoodTK3180K1Radio(KenwoodTKx180Radio):
MODEL = 'TK-3180K'
VALID_BANDS = [(400000000, 520000000)]
_model = b'P3180\x06'

# K2,K4 are technically 400-470 (K4 == keypad)
@directory.register
class KenwoodTK3180K2Radio(KenwoodTKx180Radio):
MODEL = 'TK-3180K2'
VALID_BANDS = [(400000000, 520000000)]
_model = b'P3180\x07'

@directory.register
class KenwoodTK8180E(KenwoodTKx180Radio):
MODEL = 'TK-8180E'
VALID_BANDS = [(400000000, 520000000)]
_model = b'M8189\''

@directory.register
class KenwoodTK7180ERadio(KenwoodTKx180Radio):
MODEL = 'TK-7180E'
VALID_BANDS = [(136000000, 174000000)]
_model = b'M7189$'
(10-10/13)