Project

General

Profile

New Model #4395 » tk981.py

Thomas P, 03/16/2023 10:13 AM

 
# Copyright 2016 Pavel Milanes, CO7WT, <pavelmc@gmail.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 logging
import struct
import time
import sys

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

LOG = logging.getLogger(__name__)

##### IMPORTANT DATA ##########################################
# This radios have a span of
# 0x00000 - 0x08000 => Radio Memory / Settings data
# 0x08000 - 0x10000 => FIRMWARE... hum...
###############################################################

MEM_FORMAT = """
#seekto 0x0000;
struct {
u8 unknown0[14]; // x00-x0d unknown
u8 banks; // x0e how many banks are programmed
u8 channels; // x0f how many total channels are programmed
// --
ul16 tot; // x10 TOT value: range(15, 600, 15); x04b0 = off
u8 tot_rekey; // x12 TOT Re-key value range(0, 60); off= 0
u8 unknown1; // x13 unknown
u8 tot_reset; // x14 TOT Re-key value range(0, 60); off= 0
u8 unknown2; // x15 unknows
u8 tot_alert; // x16 TOT pre alert: range(0,10); 0 = off
u8 unknown3[7]; // x17-x1d unknown
u8 sql_level; // x1e SQ reference level
u8 battery_save; // Only for portable: FF = off, x32 = on
// --
u8 unknown4[10]; // x20
u8 unknown5:3, // x2d
c2t:1, // 1 bit clear to transpond: 1-off
// This is relative to DTMF / 2-Tone settings
unknown6:4;
u8 unknown7[5]; // x2b-x2f
// --
u8 unknown8[16]; // x30 ?
u8 unknown9[16]; // x40 ?
u8 unknown10[16]; // x50 ?
u8 unknown11[16]; // x60 ?
// --
u8 add[16]; // x70-x7f 128 bits corresponding add/skip values
// --
u8 unknown12:4, // x80
off_hook_decode:1, // 1 bit off hook decode enabled: 1-off
off_hook_horn_alert:1, // 1 bit off hook horn alert: 1-off
unknown13:2;
u8 unknown14; // x81
u8 unknown15:3, // x82
self_prog:1, // 1 bit Self programming enabled: 1-on
clone:1, // 1 bit clone enabled: 1-on
firmware_prog:1, // 1 bit firmware programming enabled: 1-on
unknown16:1,
panel_test:1; // 1 bit panel test enabled
u8 unknown17; // x83
u8 unknown18:5, // x84
warn_tone:1, // 1 bit warning tone, enabled: 1-on
control_tone:1, // 1 bit control tone (key tone), enabled: 1-on
poweron_tone:1; // 1 bit power on tone, enabled: 1-on
u8 unknown19[5]; // x85-x89
u8 min_vol; // minimum volume posible: range(0,32); 0 = off
u8 tone_vol; // minimum tone volume posible:
// xff = continous, range(0, 31)
u8 unknown20[4]; // x8c-x8f
// --
u8 unknown21[4]; // x90-x93
char poweronmesg[8]; // x94-x9b power on mesg 8 bytes, off is "\FF" * 8
u8 unknown22[4]; // x9c-x9f
// --
u8 unknown23[7]; // xa0-xa6
char ident[8]; // xa7-xae radio identification string
u8 unknown24; // xaf
// --
u8 unknown26[11]; // xaf-xba
char lastsoftversion[5]; // software version employed to program the radio
} settings;

#seekto 0xd0;
struct {
u8 unknown[4];
char radio[6];
char data[6];
} passwords;

#seekto 0x0110;
struct {
u8 kA; // Portable > Closed circle
u8 kDA; // Protable > Triangle to Left
u8 kGROUP_DOWN; // Protable > Triangle to Right
u8 kGROUP_UP; // Protable > Side 1
u8 kSCN; // Portable > Open Circle
u8 kMON; // Protable > Side 2
u8 kFOOT;
u8 kCH_UP;
u8 kCH_DOWN;
u8 kVOL_UP;
u8 kVOL_DOWN;
u8 unknown30[5];
// --
u8 unknown31[4];
u8 kP_KNOB; // Just portable: channel knob
u8 unknown32[11];
} keys;

#seekto 0x0140;
struct {
lbcd tf01_rx[4];
lbcd tf01_tx[4];
u8 tf01_u_rx;
u8 tf01_u_tx;
lbcd tf02_rx[4];
lbcd tf02_tx[4];
u8 tf02_u_rx;
u8 tf02_u_tx;
lbcd tf03_rx[4];
lbcd tf03_tx[4];
u8 tf03_u_rx;
u8 tf03_u_tx;
lbcd tf04_rx[4];
lbcd tf04_tx[4];
u8 tf04_u_rx;
u8 tf04_u_tx;
lbcd tf05_rx[4];
lbcd tf05_tx[4];
u8 tf05_u_rx;
u8 tf05_u_tx;
lbcd tf06_rx[4];
lbcd tf06_tx[4];
u8 tf06_u_rx;
u8 tf06_u_tx;
lbcd tf07_rx[4];
lbcd tf07_tx[4];
u8 tf07_u_rx;
u8 tf07_u_tx;
lbcd tf08_rx[4];
lbcd tf08_tx[4];
u8 tf08_u_rx;
u8 tf08_u_tx;
lbcd tf09_rx[4];
lbcd tf09_tx[4];
u8 tf09_u_rx;
u8 tf09_u_tx;
lbcd tf10_rx[4];
lbcd tf10_tx[4];
u8 tf10_u_rx;
u8 tf10_u_tx;
lbcd tf11_rx[4];
lbcd tf11_tx[4];
u8 tf11_u_rx;
u8 tf11_u_tx;
lbcd tf12_rx[4];
lbcd tf12_tx[4];
u8 tf12_u_rx;
u8 tf12_u_tx;
lbcd tf13_rx[4];
lbcd tf13_tx[4];
u8 tf13_u_rx;
u8 tf13_u_tx;
lbcd tf14_rx[4];
lbcd tf14_tx[4];
u8 tf14_u_rx;
u8 tf14_u_tx;
lbcd tf15_rx[4];
lbcd tf15_tx[4];
u8 tf15_u_rx;
u8 tf15_u_tx;
lbcd tf16_rx[4];
lbcd tf16_tx[4];
u8 tf16_u_rx;
u8 tf16_u_tx;
} test_freq;

#seekto 0x200;
struct {
char line1[32];
char line2[32];
} message;

#seekto 0x2000;
struct {
u8 bnumb; // mem number
u8 bank; // to which bank it belongs
char name[8]; // name 8 chars
u8 unknown20[2]; // unknown yet
lbcd rxfreq[4]; // rx freq
// --
lbcd txfreq[4]; // tx freq
u8 rx_unkw; // unknown yet
u8 tx_unkw; // unknown yet
ul16 rx_tone; // rx tone
ul16 tx_tone; // tx tone
u8 unknown23[5]; // unknown yet
u8 signaling; // xFF = off, x30 DTMF, x31 2-Tone
// See the zone on x7000
// --
u8 ptt_id:2, // ??? BOT = 0, EOT = 1, Both = 2, NONE = 3
beat_shift:1, // 1 = off
unknown26:2 // ???
power:1, // power: 0 low / 1 high
compander:1, // 1 = off
wide:1; // wide 1 / 0 narrow
u8 unknown27:6, // ???
busy_lock:1, // 1 = off
unknown28:1; // ???
u8 unknown29[14]; // unknown yet
} memory[128];

#seekto 0x5900;
struct {
char model[8];
u8 unknown50[4];
char type[2];
u8 unknown51[2];
// --
char serial[8];
u8 unknown52[8];
} id;

#seekto 0x6000;
struct {
u8 code[8];
u8 unknown60[7];
u8 count;
} bot[128];

#seekto 0x6800;
struct {
u8 code[8];
u8 unknown61[7];
u8 count;
} eot[128];

#seekto 0x7000;
struct {
lbcd dt2_id[5]; // DTMF lbcd ID (000-9999999999)
// 2-Tone = "11 f1 ff ff ff" ???
// None = "00 f0 ff ff ff"
} dtmf;
"""

NOTE = """ MENTAL NOTE ABOUT RADIO MEM

The OEM insist on not reading/writing some mem segments, see below

read: (hex)
00 - 03
07 - 08
10
20 - 21
58 - 7F

write: (hex)
00 - 03
07 - 08
10
20 - 21
60 - 7F


This can be an artifact to just read/write in the needed mem space and speed
up things, if so the first read blocks has all the data about channel groups
and freq/tones & names employed.

This is a copied trick from the "60G series" ones and may use the same schema.

I must investigate further on this.
"""

MEM_SIZE = 0x08000 # 32,768 bytes (128 blocks of 256 bytes)
BLOCK_SIZE = 256
MEM_BLOCKS = range(0, MEM_SIZE / BLOCK_SIZE)
# undefined yet...
#~ RO_BLOCKS = range(0x10, 0x1F) + range(0x59, 0x5f)

# define and empty block of data, as it will be used a lot in this code
EMPTY_BLOCK = "\xFF" * 256

ACK_CMD = "\x06"

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

MODES = ["NFM", "FM"] # 12.5 / 25 Khz
VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "_-*()/\-+=)"
SKIP_VALUES = ["", "S"]

TONES = chirp_common.TONES
# TONES.remove(254.1)
DTCS_CODES = chirp_common.DTCS_CODES

TOT = ["off"] + ["%s" % x for x in range(15, 615, 15)]
TOT_PRE = ["off"] + ["%s" % x for x in range(1, 11)]
TOT_REKEY = ["off"] + ["%s" % x for x in range(1, 61)]
TOT_RESET = ["off"] + ["%s" % x for x in range(1, 16)]
VOL = ["off"] + ["%s" % x for x in range(1, 32)]
TVOL = ["%s" % x for x in range(0, 33)]
TVOL[32] = "Continous"
SQL = ["off"] + ["%s" % x for x in range(1, 10)]

## BOT = 0, EOT = 1, Both = 2, NONE = 3
#PTTID = ["BOT", "EOT", "Both", "none"]

# For debugging purposes
debug = False

#~ KEYS = {
#~ 0x33: "Display character",
#~ 0x35: "Home Channel", # Posible portable only, chek it
#~ 0x37: "CH down",
#~ 0x38: "CH up",
#~ 0x39: "Key lock",
#~ 0x3a: "Lamp", # Portable only
#~ 0x3b: "Public address",
#~ 0x3c: "Reverse", # Just in updated firmwares (768G)
#~ 0x3d: "Horn alert",
#~ 0x3e: "Selectable QT", # Just in updated firmwares (768G)
#~ 0x3f: "2-tone encode",
#~ 0x40: "Monitor A: open mommentary",
#~ 0x41: "Monitor B: Open Toggle",
#~ 0x42: "Monitor C: Carrier mommentary",
#~ 0x43: "Monitor D: Carrier toogle",
#~ 0x44: "Operator selectable tone",
#~ 0x45: "Redial",
#~ 0x46: "RF Power Low", # portable only ?
#~ 0x47: "Scan",
#~ 0x48: "Scan del/add",
#~ 0x4a: "GROUP down",
#~ 0x4b: "GROUP up",
#~ #0x4e: "Tone off (Experimental)", # undocumented !!!!
#~ 0x4f: "None",
#~ 0x50: "VOL down",
#~ 0x51: "VOL up",
#~ 0x52: "Talk around",
#~ 0x5d: "AUX",
#~ 0xa1: "Channel Up/Down" # Knob for portables only
#~ }


def _raw_recv(radio, amount):
"""Raw read from the radio device"""
data = ""
try:
data = radio.pipe.read(amount)
except:
raise errors.RadioError("Error reading data from radio")

# DEBUG
if debug is True:
LOG.debug("<== (%d) bytes:\n\n%s" % (len(data), util.hexprint(data)))

return data


def _raw_send(radio, data):
"""Raw send to the radio device"""
try:
radio.pipe.write(data)
except:
raise errors.RadioError("Error sending data to radio")

# DEBUG
if debug is True:
LOG.debug("==> (%d) bytes:\n\n%s" % (len(data), util.hexprint(data)))


def _close_radio(radio):
"""Get the radio out of program mode"""
_raw_send(radio, "\x45")


def _checksum(data):
"""the radio block checksum algorithm"""
cs = 0
for byte in data:
cs += ord(byte)
return cs % 256


def _send(radio, frame):
"""Generic send data to the radio"""
_raw_send(radio, frame)


def _make_frame(cmd, addr):
"""Pack the info in the format it likes"""
return struct.pack(">BH", ord(cmd), addr)


def _handshake(radio, msg=""):
"""Make a full handshake"""
# send ACK
_raw_send(radio, ACK_CMD)
# receive ACK
ack = _raw_recv(radio, 1)
# check ACK
if ack != ACK_CMD:
_close_radio(radio)
mesg = "Handshake failed " + msg
# DEBUG
LOG.debug(mesg)
LOG.debug("ack: " + ack)
raise Exception(mesg)


def _check_write_ack(r, ack, addr):
"""Process the ack from the write process
this is half handshake needed in tx data block"""
# all ok
if ack == ACK_CMD:
return True

# Explicit BAD checksum
if ack == "\x15":
_close_radio(r)
raise errors.RadioError(
"Bad checksum in block %02x write" % addr)

# everything else
_close_radio(r)
raise errors.RadioError(
"Problem with the ack to block %02x write, ack %03i" %
(addr, int(ack)))


def _recv(radio):
"""Receive data from the radio, 258 bytes split in (cmd, data, checksum)
checking the checksum to be correct, and returning just
256 bytes of data or false if short empty block"""
rxdata = _raw_recv(radio, BLOCK_SIZE + 2)
# when the RX block has two bytes and the first is \x5A
# then the block is all \xFF
#if len(rxdata) == 2 and rxdata[0] == "\x5A":
if len(rxdata) < 258: #ignoring smaller blocks while testing 981
## just return false to flag about empty block
return False
elif len(rxdata) != 258:
_close_radio(radio)
# not the amount of data we want
msg = "The radio send %d bytes, we need 258" % len(rxdata)
# DEBUG
LOG.error(msg)
LOG.debug(rxdata)
raise errors.RadioError(msg)
else:
rcs = ord(rxdata[-1])
data = rxdata[1:-1]
ccs = _checksum(data)
LOG.debug("data: " + util.hexprint(rxdata))
#LOG.debug("ccs %02x" % ccs)
if rcs != ccs:
_close_radio(radio)
raise errors.RadioError(
"Block Checksum Error! real %02x, calculated %02x" %
(rcs, ccs))
_handshake(radio, "after checksum")
return data


def _open_radio(radio, status):
"""Open the radio into program mode and check if it's the correct model"""
# The OEM sets the timeout to 0.550 second, we tested it and no joy.
radio.pipe.baudrate = 9600
radio.pipe.timeout = 0.7
radio.pipe.parity = "E" # 'E' and 'O' work similarly but 'N' doesn't on 981

# DEBUG
LOG.debug("Entering program mode.")
# max tries
tries = 10

# UI
status.cur = 0
status.max = tries
status.msg = "Entering program mode..."

# try a few times to get the radio into program mode
exito = False
for i in range(0, tries):
# flush in pyserial 3.0 way
#radio.pipe.reset_input_buffer()
#radio.pipe.reset_output_buffer()
time.sleep(0.02)

# line dance between each try:
radio.pipe.rts = True
radio.pipe.dtr = True
time.sleep(0.02)
radio.pipe.dtr = False

# now send the magic
_raw_send(radio, "PROGRAM")
ack = _raw_recv(radio, 1)

if len(ack) == 0 or ack != ACK_CMD:
# DEBUG
#LOG.debug(ack % i)
LOG.debug("Try %s failed, traying again..." % i)
time.sleep(0.25)
else:
exito = True
break

status.cur += 1
radio.status_fn(status)


if exito is False:
_close_radio(radio)
LOG.debug("Radio did not accepted PROGRAM command in %s atempts" % tries)
raise errors.RadioError("The radio doesn't accept program mode")

# DEBUG
LOG.debug("Received ACK to the PROGRAM command, send ID query.")


# lest's check the radio model
_raw_send(radio, "\x02")
rid = _raw_recv(radio, 8)

if not (radio.TYPE in rid):
# bad response, properly close the radio before exception
_close_radio(radio)

# DEBUG
LOG.debug("Incorrect model ID:")
LOG.debug(util.hexprint(rid))

raise errors.RadioError(
"Incorrect model ID, got %s, it not contains %s" %
(rid.strip("\xff"), radio.TYPE))

# DEBUG
LOG.debug("Full ident string is:")
LOG.debug(util.hexprint(rid))
_handshake(radio)

# alert the user of the success
status.msg = "Radio ident success!"
radio.status_fn(status)

# this radios checks some kind of version (radio version?)
_raw_send(radio, "\x50") # 'P'
ver = _raw_recv(radio, 10)

# DEBUG
LOG.debug("Version returned by the radios is:")
LOG.debug(util.hexprint(ver))
_handshake(radio)
# the radio that was procesed returned this:
# v2.01k.... [76 32 2E 30 31 6B EF FF FF FF]

# now the OEM writes simpy "O" and get no answer...
# after that we are ready to receive the radio image or to write to it
_raw_send(radio, "\x4F") # 'O'


def do_download(radio):
""" The download function """
# UI progress
status = chirp_common.Status()
data = ""
count = 0

# open the radio
_open_radio(radio, status)

# reset UI data
status.cur = 0
status.max = MEM_SIZE / BLOCK_SIZE
status.msg = "Cloning from radio..."
radio.status_fn(status)

# set the timeout and if windows keep it bigger
if sys.platform in ["win32", "cygwin"]:
# bigger timeout
radio.pipe.timeout = 0.55
else:
# Linux can keep up, MAC?
radio.pipe.timeout = 0.55 #changed from 0.05 for testing

# DEBUG
LOG.debug("Starting the download from radio")

for addr in MEM_BLOCKS:
# send request, but before flush the rx buffer
radio.pipe.flush()
_send(radio, _make_frame("R", addr))

# now we get the data
d = _recv(radio)
# if empty block, it return false
# aka we asume a empty 256 xFF block
if d is False:
d = EMPTY_BLOCK

data += d

# UI Update
status.cur = count
radio.status_fn(status)

count += 1

_close_radio(radio)
return memmap.MemoryMap(data)


def do_upload(radio):
""" The upload function """

# DISABLED in this pre-alpha state_close_radio(radio)
_close_radio(radio)
raise errors.RadioError("No upload possible yet, this is a test driver")

#~ # UI progress
#~ status = chirp_common.Status()
#~ data = ""
#~ count = 0

#~ # open the radio
#~ _open_radio(radio, status)

#~ # update UI
#~ status.cur = 0
#~ status.max = MEM_SIZE / BLOCK_SIZE
#~ status.msg = "Cloning to radio..."
#~ radio.status_fn(status)

#~ # the default for the original soft as measured
#~ radio.pipe.timeout = 0.5

#~ # DEBUG
#~ LOG.debug("Starting the upload to the radio")

#~ count = 0
#~ raddr = 0
#~ for addr in MEM_BLOCKS:
#~ # this is the data block to write
#~ data = radio.get_mmap()[raddr:raddr+BLOCK_SIZE]

#~ # The blocks from x59-x5F are NOT programmable
#~ # The blocks from x11-x1F are writed only if not empty
#~ if addr in RO_BLOCKS:
#~ # checking if in the range of optional blocks
#~ if addr >= 0x10 and addr <= 0x1F:
#~ # block is empty ?
#~ if data == EMPTY_BLOCK:
#~ # no write of this block
#~ # but we have to continue updating the counters
#~ count += 1
#~ raddr = count * 256
#~ continue
#~ else:
#~ count += 1
#~ raddr = count * 256
#~ continue

#~ if data == EMPTY_BLOCK:
#~ frame = _make_frame("Z", addr) + "\xFF"
#~ else:
#~ cs = _checksum(data)
#~ frame = _make_frame("W", addr) + data + chr(cs)

#~ _send(radio, frame)

#~ # get the ACK
#~ ack = _raw_recv(radio, 1)
#~ _check_write_ack(radio, ack, addr)

#~ # DEBUG
#~ LOG.debug("Sending block %02x" % addr)

#~ # UI Update
#~ status.cur = count
#~ radio.status_fn(status)

#~ count += 1
#~ raddr = count * 256

#~ _close_radio(radio)


def model_match(cls, data):
"""Match the opened/downloaded image to the correct version"""
rid = data[0xA7:0xAE]
if (rid in cls.VARIANTS):
# correct model
return True
else:
return False


class Kenwood60GBankModel(chirp_common.BankModel):
"""Testing the bank model on kennwood"""
channelAlwaysHasBank = True

def get_num_mappings(self):
return self._radio._num_banks

def get_mappings(self):
banks = []
for i in range(0, self._radio._num_banks):
bindex = i + 1
bank = self._radio._bclass(self, i, "%03i" % bindex)
bank.index = i
banks.append(bank)
return banks

def add_memory_to_mapping(self, memory, bank):
self._radio._set_bank(memory.number, bank.index)

def remove_memory_from_mapping(self, memory, bank):
if self._radio._get_bank(memory.number) != bank.index:
raise Exception("Memory %i not in bank %s. Cannot remove." %
(memory.number, bank))

# We can't "Remove" it for good
# the kenwood paradigm don't allow it
# instead we move it to bank 0
self._radio._set_bank(memory.number, 0)

def get_mapping_memories(self, bank):
memories = []
for i in range(0, self._radio._upper):
if self._radio._get_bank(i) == bank.index:
memories.append(self._radio.get_memory(i))
return memories

def get_memory_mappings(self, memory):
index = self._radio._get_bank(memory.number)
return [self.get_mappings()[index]]


class memBank(chirp_common.Bank):
"""A bank model for kenwood"""
# Integral index of the bank (not to be confused with per-memory
# bank indexes
index = 0


class Kenwood_Serie_80(chirp_common.CloneModeRadio):
"""Kenwood Serie 80 Radios base class"""
VENDOR = "Kenwood"
BAUD_RATE = 9600
_memsize = MEM_SIZE
NAME_LENGTH = 8
_range = [902000000, 941000000]
_upper = 250
_chs_progs = 0
_num_banks = 250
_bclass = memBank
_kind = ""
VARIANT = ""
MODEL = ""

@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('This driver is experimental and for personal use only.'
'It has a limited set of features, but the most used.'
''
'The most notorius missing features are this:'
'=> PTT ID, in fact it is disabled if detected'
'=> Priority / Home channel'
'=> Bank names'
'=> Others'
''
'If you need one of this, get your official software to do it'
'and raise and issue on the chirp site about it and maybe'
'it will be implemented in the future.'
''
'The band limits on some of this radios are set here far from'
'the vendor limits to cover most of the ham bands. It is a'
'known fact that this radios can work safely outside the OEM'
'limits, but each radio is different, you may find in trouble'
'with some particular radios, but this is also possible with'
'the OEM software; as usual YMMV.'
''
'A detail: this driver is slow reading from the radio in'
'Windows, due to the way windows serial works.'
)
rp.pre_download = _(dedent("""\
Follow this instructions to download your info:
1 - Turn off your radio
2 - Connect your interface cable
3 - Turn on your radio (unblock it if password protected)
4 - Do the download of your radio data
"""))
rp.pre_upload = _(dedent("""\
Follow this instructions to upload your info:
1 - Turn off your radio
2 - Connect your interface cable
3 - Turn on your radio (unblock it if password protected)
4 - Do the upload of your radio data
"""))
return rp

def get_features(self):
"""Return information about this radio's features"""
rf = chirp_common.RadioFeatures()
rf.has_settings = True
rf.has_bank = True
rf.has_tuning_step = False
rf.has_name = True
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.valid_modes = MODES
rf.valid_duplexes = ["", "-", "+", "off"]
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
rf.valid_cross_modes = [
"Tone->Tone",
"DTCS->",
"->DTCS",
"Tone->DTCS",
"DTCS->Tone",
"->Tone",
"DTCS->DTCS"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = VALID_CHARS
rf.valid_skips = SKIP_VALUES
rf.valid_dtcs_codes = DTCS_CODES
rf.valid_bands = [self._range]
rf.valid_name_length = 8
rf.memory_bounds = (1, self._upper)
return rf

def _fill(self, offset, data):
"""Fill an specified area of the memmap with the passed data"""
for addr in range(0, len(data)):
self._mmap[offset + addr] = data[addr]

def _prep_data(self):
"""Prepare the areas in the memmap to do a consistend write
it has to make an update on the x300 area with banks and channel
info; other in the x1000 with banks and channel counts
and a last one in x7000 with flag data"""
rchs = 0
data = dict()

# sorting the data
for ch in range(0, self._upper):
mem = self._memobj.memory[ch]
bnumb = int(mem.bnumb)
bank = int(mem.bank)
if bnumb != 255 and (bank != 255 and bank != 0):
try:
data[bank].append(ch)
except:
data[bank] = list()
data[bank].append(ch)
data[bank].sort()
# counting the real channels
rchs = rchs + 1

# updating the channel/bank count
self._memobj.settings.channels = rchs
self._chs_progs = rchs
self._memobj.settings.banks = len(data)

# building the data for the memmap
fdata = ""

for k, v in data.iteritems():
# posible bad data
if k == 0:
k = 1
raise errors.InvalidValueError(
"Invalid bank value '%k', bad data in the image? \
Triying to fix this, review your bank data!" % k)
c = 1
for i in v:
fdata += chr(k) + chr(c) + chr(k - 1) + chr(i)
c = c + 1

# fill to match a full 256 bytes block
fdata += (len(fdata) % 256) * "\xFF"

# updating the data in the memmap [x300]
self._fill(0x300, fdata)

# update the info in x1000; it has 2 bytes with
# x00 = bank , x01 = bank's channel count
# the rest of the 14 bytes are \xff
bdata = ""
for i in range(1, len(data) + 1):
line = chr(i) + chr(len(data[i]))
line += "\xff" * 14
bdata += line

# fill to match a full 256 bytes block
bdata += (256 - (len(bdata)) % 256) * "\xFF"

# fill to match the whole area
bdata += (16 - len(bdata) / 256) * EMPTY_BLOCK

# updating the data in the memmap [x1000]
self._fill(0x1000, bdata)

# DTMF id for each channel, 5 bytes lbcd at x7000
# ############## TODO ###################
fldata = "\x00\xf0\xff\xff\xff" * self._chs_progs + \
"\xff" * (5 * (self._upper - self._chs_progs))

# write it
# updating the data in the memmap [x7000]
self._fill(0x7000, fldata)

def _set_variant(self):
"""Select and set the correct variables for the class acording
to the correct variant of the radio"""
rid = self._mmap[0xA7:0xAE]

# indentify the radio variant and set the enviroment to it's values
try:
self._upper, low, high, self._kind = self.VARIANTS[rid]
self._range = [low * 1000000, high * 1000000]

# setting the bank data in the features, 8 & 16 CH dont have banks
if self._upper < 32:
rf = chirp_common.RadioFeatures()
rf.has_bank = False

# put the VARIANT in the class, clean the model / CHs / Type
# in the same layout as the KPG program
self._VARIANT = self.MODEL + " [" + str(self._upper) + "CH]: "
self._VARIANT += self._kind + ", " + str(self._range[0]/1000000) + "-"
self._VARIANT += str(self._range[1]/1000000) + " Mhz"

except KeyError:
LOG.debug("Wrong Kenwood radio, ID or unknown variant")
LOG.debug(util.hexprint(rid))
raise errors.RadioError(
"Wrong Kenwood radio, ID or unknown variant, see LOG output.")
return False

def sync_in(self):
"""Do a download of the radio eeprom"""
self._mmap = do_download(self)
self.process_mmap()

def sync_out(self):
"""Do an upload to the radio eeprom"""

# chirp signature on the eprom ;-)
sign = "Chirp"
self._fill(0xbb, sign)

try:
self._prep_data()
do_upload(self)
except errors.RadioError:
raise
except Exception, e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)

def process_mmap(self):
"""Process the memory object"""
# how many channels are programed
self._chs_progs = ord(self._mmap[15])

# load the memobj
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)

# to set the vars on the class to the correct ones
self._set_variant()

def get_raw_memory(self, number):
"""Return a raw representation of the memory object, which
is very helpful for development"""
return repr(self._memobj.memory[number])

def _decode_tone(self, val):
"""Parse the tone data to decode from mem, it returns:
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
val = int(val)
if val == 65535:
return '', None, None
elif val >= 0x2800:
code = int("%03o" % (val & 0x07FF))
pol = (val & 0x8000) and "R" or "N"
return 'DTCS', code, pol
else:
a = val / 10.0
return 'Tone', a, None

def _encode_tone(self, memval, mode, value, pol):
"""Parse the tone data to encode from UI to mem"""
if mode == '':
memval.set_raw("\xff\xff")
elif mode == 'Tone':
memval.set_value(int(value * 10))
elif mode == 'DTCS':
val = int("%i" % value, 8) + 0x2800
if pol == "R":
val += 0xA000
memval.set_value(val)
else:
raise Exception("Internal error: invalid mode `%s'" % mode)

def _get_scan(self, chan):
"""Get the channel scan status from the 16 bytes array on the eeprom
then from the bits on the byte, return '' or 'S' as needed"""
result = "S"
byte = int(chan/8)
bit = chan % 8
res = self._memobj.settings.add[byte] & (pow(2, bit))
if res > 0:
result = ""

return result

def _set_scan(self, chan, value):
"""Set the channel scan status from UI to the mem_map"""
byte = int(chan/8)
bit = chan % 8

# get the actual value to see if I need to change anything
actual = self._get_scan(chan)
if actual != value:
# I have to flip the value
rbyte = self._memobj.settings.add[byte]
rbyte = rbyte ^ pow(2, bit)
self._memobj.settings.add[byte] = rbyte

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

# Create a high-level memory object to return to the UI
mem = chirp_common.Memory()

# Memory number
mem.number = number

# this radio has a setting about the amount of real chans of the 128
# olso in the channel has xff on the Rx freq it's empty
if (number > (self._chs_progs + 1)) or (_mem.get_raw()[0] == "\xFF"):
mem.empty = True
# but is not enough, you have to crear the memory in the mmap
# to get it ready for the sync_out process
_mem.set_raw("\xFF" * 48)
return mem

# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# tx freq can be blank
if _mem.get_raw()[16] == "\xFF":
# TX freq not set
mem.offset = 0
mem.duplex = "off"
else:
# TX feq set
offset = (int(_mem.txfreq) * 10) - mem.freq
if offset < 0:
mem.offset = abs(offset)
mem.duplex = "-"
elif offset > 0:
mem.offset = offset
mem.duplex = "+"
else:
mem.offset = 0

# name TAG of the channel
mem.name = str(_mem.name).rstrip()

# power
mem.power = POWER_LEVELS[_mem.power]

# wide/marrow
mem.mode = MODES[_mem.wide]

# skip
mem.skip = self._get_scan(number - 1)

# tone data
rxtone = txtone = None
txtone = self._decode_tone(_mem.tx_tone)
rxtone = self._decode_tone(_mem.rx_tone)
chirp_common.split_tone_decode(mem, txtone, rxtone)

# Extra
# bank and number in the channel
mem.extra = RadioSettingGroup("extra", "Extra")

# validate bank
b = int(_mem.bank)
if b > 127 or b == 0:
_mem.bank = b = 1

bank = RadioSetting("bank", "Bank it belongs",
RadioSettingValueInteger(1, 128, b))
mem.extra.append(bank)

# validate bnumb
if int(_mem.bnumb) > 127:
_mem.bank = mem.number

bnumb = RadioSetting("bnumb", "Ch number in the bank",
RadioSettingValueInteger(0, 127, _mem.bnumb))
mem.extra.append(bnumb)

bs = RadioSetting("beat_shift", "Beat shift",
RadioSettingValueBoolean(
not bool(_mem.beat_shift)))
mem.extra.append(bs)

cp = RadioSetting("compander", "Compander",
RadioSettingValueBoolean(
not bool(_mem.compander)))
mem.extra.append(cp)

bl = RadioSetting("busy_lock", "Busy Channel lock",
RadioSettingValueBoolean(
not bool(_mem.busy_lock)))
mem.extra.append(bl)

return mem

def set_memory(self, mem):
"""Set the memory data in the eeprom img from the UI
not ready yet, so it will return as is"""

# get the eprom representation of this channel
_mem = self._memobj.memory[mem.number - 1]

# if empty memmory
if mem.empty:
_mem.set_raw("\xFF" * 48)
return

# frequency
_mem.rxfreq = mem.freq / 10

# this are a mistery yet, but so falr there is no impact
# whit this default values for new channels
if int(_mem.rx_unkw) == 0xff:
_mem.rx_unkw = 0x35
_mem.tx_unkw = 0x32

# duplex
if mem.duplex == "+":
_mem.txfreq = (mem.freq + mem.offset) / 10
elif mem.duplex == "-":
_mem.txfreq = (mem.freq - mem.offset) / 10
elif mem.duplex == "off":
for byte in _mem.txfreq:
byte.set_raw("\xFF")
else:
_mem.txfreq = mem.freq / 10

# tone data
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
chirp_common.split_tone_encode(mem)
self._encode_tone(_mem.tx_tone, txmode, txtone, txpol)
self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol)

# name TAG of the channel
_namelength = self.get_features().valid_name_length
for i in range(_namelength):
try:
_mem.name[i] = mem.name[i]
except IndexError:
_mem.name[i] = "\x20"

# power
# default power is low
if mem.power is None:
mem.power = POWER_LEVELS[0]

_mem.power = POWER_LEVELS.index(mem.power)

# wide/marrow
_mem.wide = MODES.index(mem.mode)

# scan add property
self._set_scan(mem.number - 1, mem.skip)

# bank and number in the channel
if int(_mem.bnumb) == 0xff:
_mem.bnumb = mem.number - 1
_mem.bank = 1

# extra settings
for setting in mem.extra:
if setting != "bank" or setting != "bnumb":
setattr(_mem, setting.get_name(), not bool(setting.value))

# all data get sync after channel mod
self._prep_data()

return mem

@classmethod
def match_model(cls, filedata, filename):
match_size = False
match_model = False

# testing the file data size
if len(filedata) == MEM_SIZE:
match_size = True

# testing the firmware model fingerprint
match_model = model_match(cls, filedata)

if match_size and match_model:
return True
else:
return False

def get_settings(self):
"""Translate the bit in the mem_struct into settings in the UI"""
sett = self._memobj.settings
mess = self._memobj.message
keys = self._memobj.keys
idm = self._memobj.id
passwd = self._memobj.passwords

# basic features of the radio
basic = RadioSettingGroup("basic", "Basic Settings")
# dealer settings
dealer = RadioSettingGroup("dealer", "Dealer Settings")
# buttons
fkeys = RadioSettingGroup("keys", "Front keys config")

# TODO / PLANED
# adjust feqs
#freqs = RadioSettingGroup("freqs", "Adjust Frequencies")

top = RadioSettings(basic, dealer, fkeys)

# Basic
tot = RadioSetting("settings.tot", "Time Out Timer (TOT)",
RadioSettingValueList(TOT, TOT[
TOT.index(str(int(sett.tot)))]))
basic.append(tot)

totalert = RadioSetting("settings.tot_alert", "TOT pre alert",
RadioSettingValueList(TOT_PRE,
TOT_PRE[int(sett.tot_alert)]))
basic.append(totalert)

totrekey = RadioSetting("settings.tot_rekey", "TOT re-key time",
RadioSettingValueList(TOT_REKEY,
TOT_REKEY[int(sett.tot_rekey)]))
basic.append(totrekey)

totreset = RadioSetting("settings.tot_reset", "TOT reset time",
RadioSettingValueList(TOT_RESET,
TOT_RESET[int(sett.tot_reset)]))
basic.append(totreset)

# this feature is for mobile only
if self.TYPE[0] == "M":
minvol = RadioSetting("settings.min_vol", "Minimum volume",
RadioSettingValueList(VOL,
VOL[int(sett.min_vol)]))
basic.append(minvol)

tv = int(sett.tone_vol)
if tv == 255:
tv = 32
tvol = RadioSetting("settings.tone_vol", "Minimum tone volume",
RadioSettingValueList(TVOL, TVOL[tv]))
basic.append(tvol)

sql = RadioSetting("settings.sql_level", "SQL Ref Level",
RadioSettingValueList(
SQL, SQL[int(sett.sql_level)]))
basic.append(sql)

#c2t = RadioSetting("settings.c2t", "Clear to Transpond",
#RadioSettingValueBoolean(not sett.c2t))
#basic.append(c2t)

ptone = RadioSetting("settings.poweron_tone", "Power On tone",
RadioSettingValueBoolean(sett.poweron_tone))
basic.append(ptone)

ctone = RadioSetting("settings.control_tone", "Control (key) tone",
RadioSettingValueBoolean(sett.control_tone))
basic.append(ctone)

wtone = RadioSetting("settings.warn_tone", "Warning tone",
RadioSettingValueBoolean(sett.warn_tone))
basic.append(wtone)

# Save Battery only for portables?
if self.TYPE[0] == "P":
bs = int(sett.battery_save) == 0x32 and True or False
bsave = RadioSetting("settings.battery_save", "Battery Saver",
RadioSettingValueBoolean(bs))
basic.append(bsave)

ponm = str(sett.poweronmesg).strip("\xff")
pom = RadioSetting("settings.poweronmesg", "Power on message",
RadioSettingValueString(0, 8, ponm, False))
basic.append(pom)

# dealer
mstr = ""
valid_chars = [32, 44, 45, 47, 58, 91, 93] + range(48, 58) + \
range(65, 91) + range(97, 123)

for i in range(0, len(self._VARIANT)):
if ord(self._VARIANT[i]) in valid_chars:
mstr += self._VARIANT[i]

val = RadioSettingValueString(0, 35, mstr)
val.set_mutable(False)
mod = RadioSetting("not.mod", "Radio Version", val)
dealer.append(mod)

sn = str(idm.serial).strip(" \xff")
val = RadioSettingValueString(0, 8, sn)
val.set_mutable(False)
serial = RadioSetting("not.serial", "Serial number", val)
dealer.append(serial)

svp = str(sett.lastsoftversion).strip(" \xff")
val = RadioSettingValueString(0, 5, svp)
val.set_mutable(False)
sver = RadioSetting("not.softver", "Software Version", val)
dealer.append(sver)

l1 = str(mess.line1).strip(" \xff")
line1 = RadioSetting("message.line1", "Comment 1",
RadioSettingValueString(0, 32, l1))
dealer.append(line1)

l2 = str(mess.line2).strip(" \xff")
line2 = RadioSetting("message.line2", "Comment 2",
RadioSettingValueString(0, 32, l2))
dealer.append(line2)

sprog = RadioSetting("settings.self_prog", "Self program",
RadioSettingValueBoolean(sett.self_prog))
dealer.append(sprog)

clone = RadioSetting("settings.clone", "Allow clone",
RadioSettingValueBoolean(sett.clone))
dealer.append(clone)

panel = RadioSetting("settings.panel_test", "Panel Test",
RadioSettingValueBoolean(sett.panel_test))
dealer.append(panel)

fmw = RadioSetting("settings.firmware_prog", "Firmware program",
RadioSettingValueBoolean(sett.firmware_prog))
dealer.append(fmw)

# front keys
# The Mobile only parameters are wraped here
if self.TYPE[0] == "M":
vu = RadioSetting("keys.kVOL_UP", "VOL UP",
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kVOL_UP))]))
fkeys.append(vu)

vd = RadioSetting("keys.kVOL_DOWN", "VOL DOWN",
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kVOL_DOWN))]))
fkeys.append(vd)

chu = RadioSetting("keys.kCH_UP", "CH UP",
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kCH_UP))]))
fkeys.append(chu)

chd = RadioSetting("keys.kCH_DOWN", "CH DOWN",
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kCH_DOWN))]))
fkeys.append(chd)

foot = RadioSetting("keys.kFOOT", "Foot switch",
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kCH_DOWN))]))
fkeys.append(foot)

# this is the common buttons for all

# 260G model don't have the front keys
if not "P2600" in self.TYPE:
scn_name = "SCN"
if self.TYPE[0] == "P":
scn_name = "Open Circle"

scn = RadioSetting("keys.kSCN", scn_name,
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kSCN))]))
fkeys.append(scn)

a_name = "A"
if self.TYPE[0] == "P":
a_name = "Closed circle"

a = RadioSetting("keys.kA", a_name,
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kA))]))
fkeys.append(a)

da_name = "D/A"
if self.TYPE[0] == "P":
da_name = "< key"

da = RadioSetting("keys.kDA", da_name,
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kDA))]))
fkeys.append(da)

gu_name = "Triangle up"
if self.TYPE[0] == "P":
gu_name = "Side 1"

gu = RadioSetting("keys.kGROUP_UP", gu_name,
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kGROUP_UP))]))
fkeys.append(gu)

# Side keys on portables
gd_name = "Triangle Down"
if self.TYPE[0] == "P":
gd_name = "> key"

gd = RadioSetting("keys.kGROUP_DOWN", gd_name,
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kGROUP_DOWN))]))
fkeys.append(gd)

mon_name = "MON"
if self.TYPE[0] == "P":
mon_name = "Side 2"

mon = RadioSetting("keys.kMON", mon_name,
RadioSettingValueList(KEYS.values(),
KEYS.values()[KEYS.keys().index(
int(keys.kMON))]))
fkeys.append(mon)

return top

def set_settings(self, settings):
"""Translate the settings in the UI into bit in the mem_struct
I don't understand well the method used in many drivers
so, I used mine, ugly but works ok"""

mobj = self._memobj

for element in settings:
if not isinstance(element, RadioSetting):
self.set_settings(element)
continue

# Let's roll the ball
if "." in element.get_name():
inter, setting = element.get_name().split(".")
# you must ignore the settings with "not"
# this are READ ONLY attributes
if inter == "not":
continue

obj = getattr(mobj, inter)
value = element.value

# integers case + special case
if setting in ["tot", "tot_alert", "min_vol", "tone_vol",
"sql_level", "tot_rekey", "tot_reset"]:
# catching the "off" values as zero
try:
value = int(value)
except:
value = 0

# tot case step 15
if setting == "tot":
value = value * 15
# off is special
if value == 0:
value = 0x4b0

# Caso tone_vol
if setting == "tone_vol":
# off is special
if value == 32:
value = 0xff

# Bool types + inverted
if setting in ["c2t", "poweron_tone", "control_tone",
"warn_tone", "battery_save", "self_prog",
"clone", "panel_test"]:
value = bool(value)

# this cases are inverted
if setting == "c2t":
value = not value

# case battery save is special
if setting == "battery_save":
if bool(value) is True:
value = 0x32
else:
value = 0xff

# String cases
if setting in ["poweronmesg", "line1", "line2"]:
# some vars
value = str(value)
just = 8
# lines with 32
if "line" in setting:
just = 32

# empty case
if len(value) == 0:
value = "\xff" * just
else:
value = value.ljust(just)

# case keys, with special config
if inter == "keys":
value = KEYS.keys()[KEYS.values().index(str(value))]

# Apply al configs done
setattr(obj, setting, value)

def get_bank_model(self):
"""Pass the bank model to the UI part"""
rf = self.get_features()
if rf.has_bank is True:
return Kenwood60GBankModel(self)
else:
return None

def _get_bank(self, loc):
"""Get the bank data for a specific channel"""
mem = self._memobj.memory[loc - 1]
bank = int(mem.bank) - 1

if bank > self._num_banks or bank < 1:
# all channels must belong to a bank, even with just 1 bank
return 0
else:
return bank

def _set_bank(self, loc, bank):
"""Set the bank data for a specific channel"""
try:
b = int(bank)
if b > 127:
b = 0
mem = self._memobj.memory[loc - 1]
mem.bank = b + 1
except:
msg = "You can't have a channel without a bank, click another bank"
raise errors.InvalidDataError(msg)



@directory.register
class TK868G_Radios(Kenwood_Serie_80):
"""Kenwood TK-981 Radio """
MODEL = "TK-981"
TYPE = "M0981"
VARIANTS = {
"M0981\x05\xDF\xF1": (981, 136, 184, "Test model to build the class"),
}
(12-12/21)