Project

General

Profile

New Model #3363 » tkX90.py

Today's update, lots of improvements as described in my comment - Patrick Leiser, 12/11/2024 12:58 AM

 
# TK-690 support and improvements added in 2024 by Patrick Leiser, based on the work of:
# Copyright 2016-2024 Pavel Milanes CO7WT, <pavelmc@gmail.com>
#
# And with the help of Tom Hayward, who gently provided me with a driver he
# started and never finished for this radio.
#
# 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

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, RadioSettingValueMap

LOG = logging.getLogger(__name__)

# Note: the exported .dat files from the official KPG-44D software have a
# metadata preable for the first 64 bytes, then contain the desired image
# to export to the radio itself (which is what chirp handles).

# To edit files in KPG-44D, create a blank file for the desired model,
# then prepend the first 64 bytes of that file to the chirp img file


##### IMPORTANT MEM DATA #########################################
# This radios have a odd mem structure, it seems like you have to
# manage 3 memory sectors that we concatenate on just one big
# memmap, as follow
#
# Low memory (Main CPU)
# 0x0000 to 0x4000
# Mid memory (Unknown)
# 0x4000 to 0x4090
# High memory (Head)
# 0x4090 to 0x6090
###############################################################

MEM_FORMAT = """

//# seekto 0x0000;
struct {
u8 unknown[16];
u8 ch_name_length;
u8 grp_name_length;
} settings;

#seekto 0x0300;
struct {
u8 grp_up; // functions: 0x0 through 0x2 = Aux A through C
u8 grp_down; // 0x3 through 0x7 = Ch 1 through 5 direct,
u8 monitor; // 0x8 = Ch down, 0x9 = Ch up, 0xA = ch name,
u8 scan; // 0xB = Ch recall, 0xC = Del/Add, 0xD = dimmer,
u8 PF1; // 0xE = Emergency Call, 0xF = Grp down,
u8 PF2; // 0x10 = Grp up, 0x11 = HC1 (fixed),
u8 PF3; // 0x12 = HC2 (toggle), 0x13 = Horn Alert
u8 PF4; // 0x16 = Monitor, 0x17 = Operator Sel tone
u8 PF5; // 0x19 = Public Address, 0x1A = Scan,
u8 PF6; // 0x1C = Speaker int/ext, 0x1D = Squelch,
u8 PF7; // 0x1E= Talk Around, 0xFF = no function
u8 PF8;
u8 PF9;
u8 unknown1; // 0x10 when full head used
u8 unknown2; // 0x0F when full head used
u8 unknown3[12]; //all 0xFF
u8 knob_control; //00 = ch up/down, 01 = grp up/down
u8 headUnitType; //0x00 = full, 0xFF = basic
} button_assignments;

#seekto 0x0400;
struct {
u8 tot; // * 10 seconds, 30 sec increments
u8 tot_pre_alert; // seconds, off - 10, default 5
u8 tot_rekey_time; // seconds, off - 60, default off
u8 tot_reset_time; // seconds, off - 15, default off
u8 unknown[4];
} group_settings[160];

#seekto 0x1480;
struct {
u8 index; // the index in the group_belong where this group start
u8 length; // how many channels are in this group
} group_limits[160];

#seekto 0x1600;
struct {
u8 number; // CH number relative to the group, 1-160
u8 index; // index relative to the memory space memory[index]
} group_belong[160];

#seekto 0x1800;
struct {
lbcd rxfreq[4]; // 00-03
lbcd txfreq[4]; // 04-07
ul16 rxtone;
ul16 txtone;
u8 unknown0:1,
power:1, // power 0 = high, 1 = low
beatshift:1, // beat shift, 1 = on
bcl:1, // busy channel lockout, 1 = on
pttid:1, // ptt id, 1 = on
optionSignalling:3; //off=0, 1=DTMF, 2,3,4 = "2-Tone 1,2,3"
u8 unknown1:4,
add:1, // scan add, 1 = add
unknown2:1,
wide:1, // Wide = 1, narrow = 0
unknown4:1;
u8 unknown5;
u8 nose;
} memory[160];

#seekto 0x3DF0;
char poweron_msg[14];

#seekto 0x3E80;
struct {
char line1[32];
char line2[32];
} embeddedMessage;

#seekto 0x3ED0;
struct {
u8 unknown10[10];
char soft[6];
u8 rid[10];
u8 unknown11[6];
u8 unknown12[11];
char soft_ver[5];
} properties;

#seekto 0x4090;
struct {
char name[16];
} grp_names[160];

#seekto 0x4AA0;
struct {
char name[16];
} chs_names[160];

"""

MEM_SIZE = 0x6090 # 24720 bytes, 24,720 KiB
DAT_FILE_SIZE = 0x60E0
ACK_CMD = "\x06"
RX_BLOCK_SIZE_L = 128
MEM_LR = range(0x0380, 0x0400)
RX_BLOCK_SIZE_M = 16
MEM_MR = range(1, 10)
RX_BLOCK_SIZE_H = 32
MEM_HR = range(0, 0x2000, RX_BLOCK_SIZE_H)
EMPTY_L = "\xFF" * RX_BLOCK_SIZE_L
EMPTY_H = "\xFF" * RX_BLOCK_SIZE_H
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=45),
chirp_common.PowerLevel("Low", watts=5)]
MODES = ["NFM", "FM"] # 12.5 / 25 Khz
VALID_CHARS = chirp_common.CHARSET_UPPER_NUMERIC + "()/\*@-+,.#_"
NAME_CHARS = 8
SKIP_VALUES = ["S", ""]
TONES = chirp_common.TONES
DTCS_CODES = chirp_common.DTCS_CODES

BUTTON_FUNCTION_LIST = [('Aux A', 0), ('Aux B', 1), ('Aux C', 2),
('Ch 1 direct', 3), ('Ch 2 direct', 4), ('Ch 3 direct', 5), ('Ch 4 direct', 6),
('Ch 5 direct', 7), ('Ch down', 8), ('Ch up', 9), ('Ch name', 10),
('Ch recall', 11), ('Del/Add', 12), ('Dimmer', 13), ('Emergency Call', 14),
('Grp down', 15), ('Grp up', 16), ('HC1 (fixed)', 17), ('HC2 (toggle)', 18),
('Horn Alert', 19), ('Monitor', 22), ('Operator Sel tone', 23),
('Public Address', 25), ('Scan', 26), ('Speaker int/ext', 28), ('Squelch', 29),
('Talk Around', 30), ('no function', 255)]

ASSIGNABLE_BUTTONS = ["grp_up", "grp_down", "monitor", "scan", "PF1", "PF2", "PF3", "PF4", "PF5", "PF6", "PF7", "PF8", "PF9"]
FULL_HEAD_ONLY_BUTTONS = ["monitor", "scan", "PF5", "PF6", "PF7", "PF8", "PF9"]


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

# DEBUG
LOG.debug("<== (%d) bytes: %s" % (len(data), util.hexprint(data)))
return data


def _raw_send(radio, data):
"""Raw send to the radio device"""
try:
radio.pipe.flush()
radio.pipe.write(data)
# DEBUG
LOG.debug("==> (%d) bytes: %s" % (len(data), util.hexprint(data)))
except:
raise errors.RadioError("Error sending data to radio")


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


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_framel(cmd, addr):
"""Pack the info in the format it likes"""
# x52 x0F (x0380-x0400)
return struct.pack(">BBH", ord(cmd), 0x0F, addr)


def _make_framem(cmd, addr):
"""Pack the info in the format it likes"""
# x54 x0F (x00-x0A)
return struct.pack(">BBB", ord(cmd), 0x0F, addr)


def _make_frameh(cmd, addr):
"""Pack the info in the format it likes"""
# x53 x8F (x0000-x2000) x20
return struct.pack(">BBHB", ord(cmd), 0x8F, addr, RX_BLOCK_SIZE_H)


def _handshake(radio, msg="", full=True):
"""Make a full handshake"""
if full is True:
# 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, got ack: '0x%02x': " % ord(ack)
mesg += msg
# DEBUG
LOG.debug(mesg)
raise errors.RadioError(mesg)


def _recvl(radio):
"""Receive low data block from the radio, 130 or 2 bytes"""
rxdata = _raw_recv(radio, 2)
if rxdata == "\x5A\xFF":
# when the RX block has 2 bytes and the paylod+CS is \x5A\xFF
# then the block is all \xFF
_handshake(radio, "short block")
return False
rxdata += _raw_recv(radio, RX_BLOCK_SIZE_L)
if len(rxdata) == RX_BLOCK_SIZE_L + 2 and rxdata[0] == "W":
# Data block is W + Data(128) + CS
rcs = ord(rxdata[-1])
data = rxdata[1:-1]
ccs = _checksum(data)

if rcs != ccs:
msg = "Block Checksum Error! real %02x, calculated %02x" % \
(rcs, ccs)
LOG.error(msg)
_handshake(radio)
_close_radio(radio)
raise errors.RadioError(msg)

_handshake(radio, "After checksum in Low Mem")
return data
else:
raise errors.RadioError("Radio send an answer we don't understand")


def _recvh(radio):
"""Receive high data from the radio, 35 or 4 bytes"""
rxdata = _raw_recv(radio, 4)
# There are two valid options, the first byte is the content
if len(rxdata) == 4 and rxdata[0] == "\x5B" and rxdata[3] == "\xFF":
# 4 bytes, x5B = empty; payload = xFF (block is all xFF)
_handshake(radio, "Short block in High Mem")
return False
rxdata += _raw_recv(radio, RX_BLOCK_SIZE_H - 1)
if len(rxdata) == RX_BLOCK_SIZE_H + 3 and rxdata[0] == "\x58":
# 35 bytes, x58 + address(2) + data(32), no checksum
data = rxdata[3:]
_handshake(radio, "After data in High Mem")
return data
else:
_close_radio(radio)
raise errors.RadioError("Radio send a answer we don't understand")


def _open_radio(radio):
"""Open the radio into program mode and check if it's the correct model"""
radio.pipe.baudrate = 9600
radio.pipe.timeout = 1.0

LOG.debug("Starting program mode.")

_raw_send(radio, "PROGRAM")
ack = _raw_recv(radio, 1024)
while ack:
if ack[0] == ACK_CMD:
break
ack = ack[ack.index('\xff')+1:]
else:
radio.pipe.write("E")
raise errors.RadioError("Radio didn't acknowledge program mode.")

# DEBUG
LOG.debug("Received correct ACK to the MAGIC, send ID query.")
LOG.info("Radio entered Program mode.")

_raw_send(radio, "\x02\x0F")
rid = _raw_recv(radio, 10)

if not rid.startswith(radio.TYPE):
# 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.info("Positive ID on radio.")
LOG.debug("Full ident string is:\n%s" % util.hexprint(rid))

_handshake(radio)


def do_download(radio):
""" The download function """
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = MEM_SIZE
status.msg = ""
radio.status_fn(status)

# open the radio
_open_radio(radio)

# initialize variables
data = ""
bar = 0

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

# speed up the reading for this stage
# radio.pipe.timeout = (0.08) # never below 0.08 or you will get errors

for addr in MEM_LR:
_send(radio, _make_framel("R", addr))
d = _recvl(radio)
# if empty block, return false = full of xFF
if d == False:
d = EMPTY_L

# aggregate the data
data += d

# UI update
bar += RX_BLOCK_SIZE_L
status.cur = bar
status.msg = "Cloning from Main MCU (Low mem)..."
radio.status_fn(status)

# DEBUG
LOG.debug("Main MCU (low) mem received")

# speed up the reading for this stage
# radio.pipe.timeout = (0.04) # never below 0.04 or you will get errors

for addr in MEM_MR:
_send(radio, _make_framem("T", addr))
d = _raw_recv(radio, 17)

if len(d) != 17 :
raise errors.RadioError(
"Problem receiving short block %d on mid mem" % addr)

# Aggregate data ans hansdhake
data += d[1:]
_handshake(radio, "Middle mem ack error")

# UI update
bar += RX_BLOCK_SIZE_M
status.cur = bar
status.msg = "Cloning from 'unknown' (mid mem)..."
radio.status_fn(status)

# DEBUG
LOG.debug("Middle mem received.")

# speed up the reading for this stage
# radio.pipe.timeout = (0.08) # never below 0.08 or you will get errors
for addr in MEM_HR:
_send(radio, _make_frameh("S", addr))
# FIXME: empty blocks are short and always expire timeout
d = _recvh(radio)
# if empty block, return false = full of xFF
if d == False:
d = EMPTY_H

# aggregate the data
data += d

# UI update
bar += RX_BLOCK_SIZE_H
status.cur = bar
status.msg = "Cloning from Head (High mem)..."
radio.status_fn(status)

# DEBUG
LOG.debug("Head (high) mem received")
LOG.info("Full Memory received ok.")

_close_radio(radio)
return memmap.MemoryMapBytes(data)


def do_upload(radio):
""" The upload function """
# UI progress
status = chirp_common.Status()
status.cur = 0
status.max = MEM_SIZE
status.msg = "Getting the radio into program mode."
radio.status_fn(status)
# open the radio
_open_radio(radio)

# initialize variables
bar = 0
img = radio.get_mmap()

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

for addr in MEM_LR:
# this is the data to write
data = img[bar:bar + RX_BLOCK_SIZE_L]
# this is the full packet to send
sdata = ""

# flag
short = False

# building the data to send
if data == EMPTY_L:
# empty block
sdata = _make_framel("Z", addr) + "\xFF"
short = True
else:
# normal
cs = _checksum(data)
sdata = _make_framel("W", addr) + data + chr(cs)

# send the data
_send(radio, sdata)

# DEBUG
LOG.debug("Sended memmap pos 0x%04x" % bar)

# slow MCU
# time.sleep(0.15)

# check ack
msg = "Bad ACK on low block %04x" % addr
_handshake(radio, msg, False)

# UI Update
bar += RX_BLOCK_SIZE_L
status.cur = bar
status.msg = "Cloning to Main MCU (Low mem)..."
radio.status_fn(status)

# DEBUG
LOG.debug("Main MCU (low) mem received")

# speed up the reading for this stage
# radio.pipe.timeout = (0.04) # never below 0.04 or you will get errors
for addr in MEM_MR:
# this is the data to write
data = img[bar:bar + RX_BLOCK_SIZE_M]
sdata = _make_framem("Y", addr) + "\x00" + data

# send it
_send(radio, sdata)

# DEBUG
LOG.debug("Sent memmap pos 0x%04x" % bar)

# slow MCU
# time.sleep(0.2)

# check ack
msg = "Bad ACK on mid block %04x" % addr
_handshake(radio, msg, not short)

# UI Update
bar += RX_BLOCK_SIZE_M
status.cur = bar
status.msg = "Cloning from middle mem..."
radio.status_fn(status)

# DEBUG
LOG.debug("Middle mem received")

for addr in MEM_HR:
# this is the data to write
data = img[bar:bar + RX_BLOCK_SIZE_H]
# this is the full packet to send
sdata = ""

# building the data to send
if data == EMPTY_H:
# empty block
sdata = _make_frameh("[", addr) + "\xFF"
else:
# normal
sdata = _make_frameh("X", addr) + data

# send the data
_send(radio, sdata)

# DEBUG
LOG.debug("Sended memmap pos 0x%04x" % bar)

# slow MCU
# time.sleep(0.15)

# check ack
msg = "Bad ACK on low block %04x" % addr
_handshake(radio, msg, False)

# UI Update
bar += RX_BLOCK_SIZE_H
status.cur = bar
status.msg = "Cloning to Head MCU (high mem)..."
radio.status_fn(status)

# DEBUG
LOG.debug("Head (high) mem received")
# DEBUG
LOG.info("Upload finished")


_close_radio(radio)


def model_match(cls, data, rid_index=0x03EE0):
"""Match the opened/downloaded image to the correct version"""
rid = _get_rid(data, rid_index)

# DEBUG
print("Radio ID is:")
print(util.hexprint(rid))
print(cls.VARIANTS)
if (rid in cls.VARIANTS):
print("File/Data match for ID.")
return True
else:
print("BAD File/Data match.")
return False


def _get_rid(data, index=0x03EE0):
"""Get the radio ID string from a mem string"""
return data[index:index+6]


class Kenwoodx90BankModel(chirp_common.BankModel):
"""Testing the bank model on kenwood"""
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):
# display group number
bindex = i + 1
# display name of the channel
gname = "%03i" % bindex
# assign the channel
bank = self._radio._bclass(self, i, gname )
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)
if index is None:
return []
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 Kenwoodx90(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio):
"""Kenwood TK-790 radio base class"""
VENDOR = "Kenwood"
BAUD_RATE = 9600
VARIANT = ""
MODEL = ""
NAME_LENGTH = 6
# others
_memsize = MEM_SIZE
_range = [136000000, 162000000]
_upper = 160
_banks = dict()
_num_banks = 160
_bclass = memBank
_kind = ""
FORMATS = [directory.register_format('Kenwood KPG-44D', '*.dat')]


@classmethod
def get_prompts(cls):
rp = chirp_common.RadioPrompts()
rp.experimental = \
('========== PLEASE READ THIS NOTICE FULLY. ===============\n'
'\n'
'This driver is experimental and for personal use only. '
'Please do a backup with the KPG44 software BEFORE using it '
'to have a way of restoring the radio\'s features if something '
'goes wrong.\n'
'\n'
'By now just channel management and basic banks support is '
'included, the driver still in development, so any success '
'or failure report are welcomed. (keep reading)\n'
'\n'
'If you uses GROUP names this driver will ignore that and can '
'fail, that feature is in development\n'
'\n'
'It\'s known that there are some special firmware version radios '
'specifically for California, this driver may not work with that '
'radios, if so please report to the developer at: '
'co7wt@frcuba.co.cu or via the chirp web interface\n'

)
rp.pre_download = _(dedent("""\
Follow this instructions to read from your radio 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

The structure of the radio data is different from normal radios
so the download will take up to 2+ minutes.

"""))
rp.pre_upload = _(dedent("""\
Follow this instructions to write to your radio 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

The structure of the radio data is different from normal radios
so the upload will take up to 2+ minutes.

"""))
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 = NAME_CHARS
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 x1600 area with banks and channel
info; other in the x1000 with banks and channel counts
and a last one in x7000 with flog 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, and other runtime data"""
rid = _get_rid(self.get_mmap())

# 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]

# 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"

# DEBUG
LOG.info("Radio variant: %s" % self._VARIANT)

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.")

# the channel name length is a variable in the radio settings
NAME_CHARS = int(self._memobj.settings.ch_name_length)

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"""
try:
do_upload(self)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)

def _get_bank_struct(self):
"""Parse the bank data in the mem into the self.bank variable"""
# Variables
gl = self._memobj.group_limits
gb = self._memobj.group_belong
bank_count = 0

for bg in gl:
# check for empty banks
if bg.index == 255 and bg.length == 255:
self._banks[bank_count] = list()
# increment the bank count
bank_count += 1
continue

for i in range(0, bg.length):
# bank inside this channel
position = bg.index + i
index = int(gb[position].index)

try:
self._banks[bank_count].append(index)
except KeyError:
self._banks[bank_count] = list()
self._banks[bank_count].append(index)

# increment the bank count
bank_count += 1

def process_mmap(self):
"""Process the memory object"""
# load the memobj
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)

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

# load the bank data
self._get_bank_struct()

def load_mmap(self, filename):
print('loading'+filename) #LOG.info
if filename.lower().endswith('.dat'):
with open(filename, "rb") as f:
self._datHeaderMmap = memmap.MemoryMapBytes(f.read(0x40))
#f.seek(0x40)
self._mmap = memmap.MemoryMapBytes(f.read())
print('loaded KPG-44D dat file at offset 0x40') #LOG.info
self.process_mmap()
else:
self._datHeaderMmap = None
chirp_common.CloneModeRadio.load_mmap(self, filename)

def save_mmap(self, filename):
print("saving as"+filename)
if filename.lower().endswith('.dat'):
with open(filename, 'wb') as f:
datHeader = self._prep_dat_header()
f.write(datHeader.get_packed())
f.write(self._mmap.get_packed())
LOG.info("Wrote KPG-44D dat file")
else:
chirp_common.CloneModeRadio.save_mmap(self, filename)

def _prep_dat_header(self):
if self._datHeaderMmap is not None: #if dat header imported with file
return self._datHeaderMmap
#otherwise build our own header
datHeaderMap = memmap.MemoryMapBytes(bytes([255]*0x40))
softwareName = self._mmap.get(0x3EDA, 6)
softwareVer = self._mmap.get(0x3EFB, 5)
rid = self._mmap.get(0x3EE0, 10)
datHeaderMap.set(0x00, softwareName)
datHeaderMap.set(0x0A, softwareVer)
datHeaderMap.set(0x0F, rid)
print(datHeaderMap.printable())
return datHeaderMap

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(b"\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_memory(self, number):
"""Get the mem representation from the radio image"""
_mem = self._memobj.memory[number - 1]

_chs_names = self._memobj.chs_names[number-1]

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

# Memory number
mem.number = number

if _mem.get_raw()[0] == 0xFF:
mem.empty = True
return mem

# Freq and offset
mem.freq = int(_mem.rxfreq) * 10
# tx freq can be blank
if _mem.get_raw()[4] == 0xFF:
# 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(_chs_names.name).rstrip(" ")[:NAME_CHARS + 1]

# power (0 = high, 1 = low)
mem.power = POWER_LEVELS[int(_mem.power)]

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

# skip
mem.skip = SKIP_VALUES[int(_mem.add)]

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

# Extra
mem.extra = RadioSettingGroup("extra", "Extra")

bcl = RadioSetting("bcl", "Busy channel lockout",
RadioSettingValueBoolean(bool(_mem.bcl)))
mem.extra.append(bcl)

pttid = RadioSetting("pttid", "PTT ID",
RadioSettingValueBoolean(bool(_mem.pttid)))
mem.extra.append(pttid)

beat = RadioSetting("beatshift", "Beat Shift",
RadioSettingValueBoolean(bool(_mem.beatshift)))
mem.extra.append(beat)

return mem

def set_memory(self, mem):
"""Set the memory data in the eeprom img from the UI"""
# get the eprom representation of this channel
_mem = self._memobj.memory[mem.number - 1]
_ch_name = self._memobj.chs_names[mem.number - 1]

# if empty memory
if mem.empty:
# the channel it self
_mem.set_raw("\xFF" * 16)

# the name tag
for byte in _ch_name.name:
byte.set_raw("\xFF")

# delete it from the banks
self._del_channel_from_bank(mem.number)

return

# frequency
_mem.rxfreq = mem.freq / 10

# 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.txtone, txmode, txtone, txpol)
self._encode_tone(_mem.rxtone, rxmode, rxtone, rxpol)

# name TAG of the channel
_ch_name.name = str(mem.name).ljust(16, " ")

# power, # default power is low (0 = high, 1 = low)
_mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)

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

# scan add property
_mem.add = SKIP_VALUES.index(mem.skip)

# reseting unknowns, this have to be set !?!?!?!?
_mem.nose.set_raw("\xFE")

# extra settings
if len(mem.extra) > 0:
# there are setting, parse
for setting in mem.extra:
setattr(_mem, setting.get_name(), bool(setting.value))
else:
msg = "Channel #%d has no extra data, loading defaults" % \
int(mem.number - 1)
LOG.info(msg)
# there is no extra settings, load defaults
_mem.bcl = 0
_mem.pttid = 0
_mem.beatshift = 0
# unknowns
_mem.unknown0 = 0
_mem.unknown1 = 0

# it's new and we need to update it's bank state?
b = self._get_bank(mem.number)
if b == None:
self._set_bank(mem.number, 1)

return mem

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

if filename.lower().endswith('.dat'):
if (len(filedata) == DAT_FILE_SIZE):
match_size = True
#.dat metadata specifies model near start of file
match_model = model_match(cls, filedata, 0x000F)
# testing the file data size
#print(len(filedata))
#print(MEM_SIZE)
#TODO figure out the cause of this size discrepancy
if (len(filedata) == MEM_SIZE) or (len(filedata) == MEM_SIZE + 16):
match_size = True
LOG.info("File match for size")

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

if match_size and match_model:
return True
else:
return False

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

def _get_bank(self, loc):
"""Get the bank data for a specific channel"""
for k in self._banks:
if (loc - 1) in self._banks[k]:
# DEBUG
LOG.info("Channel %d is in bank %d" % (loc, k))
return k

return None

def _set_bank(self, loc, bank=0):
"""Set the bank data for a specific channel"""
# it's on the same bank?
b = self._get_bank(loc)
if b == bank:
return

# it's already asigned, delete from there
if b != None:
self._del_channel_from_bank(loc, b)

# adding it
# DEBUG
LOG.debug("Loc %d is not in bank %d, adding it" % (loc, bank))
self._banks[bank].append(loc - 1)

# if the update was successful update in the memmap
self._update_bank_memmap()

def _del_channel_from_bank(self, loc, bank=None):
"""Remove a channel from a bank, if no bank is specified we search
from where it is"""
LOG.debug("removing "+str(loc-1)+"from bank "+str(bank))

# some times we need to just erase it not knowing where it's
# if so,
if bank == None:
bank = self._get_bank(loc)

# remove it
self._banks[bank].pop(self._banks[bank].index(loc - 1))

## check if the banks got empty to erase it
#if len(self._banks[bank]) == 0:
#self._banks.pop(bank)

# if the delete was successful update in the memmap
self._update_bank_memmap()

def _update_bank_memmap(self):
"""This function is called whatever a change is made to a channel
or a bank, to update the memmap with the changes that was made"""

bl = b""
bb = b""

# group_belong index
gbi = 0
for bank in self._banks:
# check for empty banks
if len(self._banks[bank]) == 0:
bl += b"\xff\xff"
continue

# channel index inside the bank, starting at 1
# aka channel in group index
cgi = 1
for channel in range(0, len(self._banks[bank])):
# update bb
bb += bytes([cgi, self._banks[bank][channel]])
# set the group limitst for this group
if cgi == 1:
bl += bytes([gbi,len(self._banks[bank])])

# increments
gbi += 1
cgi += 1

# fill the gaps before write it
bb += b"\xff" * 2 * int(self._num_banks - len(bb) / 2)
bl += b"\xff" * 2 * int(self._num_banks - len(bl) / 2)

# update the memmap
self._fill(0x1480, bl)
self._fill(0x1600, bb)

def get_settings(self):
"""Translate the MEM_FORMAT structs into the UI"""
_button_settings = self._memobj.button_assignments
button_assignments = RadioSettingGroup("Button Functions", "Configurable Button Functions")
group = RadioSettings(button_assignments)
for buttonName in ASSIGNABLE_BUTTONS:
_fullHeadWarning = ""
if buttonName in FULL_HEAD_ONLY_BUTTONS:
_fullHeadWarning = " (If Equipped)"
rs = RadioSetting(buttonName, "Configured function for "+buttonName+" button"+_fullHeadWarning,
RadioSettingValueMap(BUTTON_FUNCTION_LIST, self._memobj.button_assignments[buttonName]))
button_assignments.append(rs)
return group
#TODO implement set_settings


@directory.register
class TK790_Radios(Kenwoodx90):
"""Kenwood TK-790 K/K2"""
MODEL = "TK-790"
TYPE = "M0790"
VARIANTS = {
b"M0790\x04": (160, 144, 174, "K"), # se note below
b"M0790\x05": (160, 136, 156, "K2")
}

@directory.register
class TK690_Radios(Kenwoodx90):
"""Kenwood TK-690 """
MODEL = "TK-690"
TYPE = "M0690"
VARIANTS = {
b"M0690\x01": (160, 28, 37, "K"), # see note below
b"M0690\x02": (160, 35, 43, "K2"),
b"M0690\x03": (160, 136, 156, "K3")
}

# Note:
# These radios originaly are constrained to some band segments but the
# original software doesn't care much about it, so in order to match a
# feature many will miss from the factory software and to help
# the use of this radios in the ham bands I'm expanding the range
# of the "K" version of the TK-790 from 148 to 144, as well as the
# range of the TK-690 "F1" (from 29.7 to 28) and "F3" (from 50 to 54)
# versions (note that F3 also needs physical modifications for use with
# Ham bands)


(6-6/8)