# 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

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

LOG = logging.getLogger(__name__)

# #### IMPORTANT DATA ##########################################
# These 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 unknown
  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 possible: range(0,32); 0 = off
  u8 tone_vol;              // minimum tone volume possible:
                                // xff = continuous, 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 "\xFF" * 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;               // Portable > Triangle to Left
  u8 kGROUP_DOWN;       // Portable > Triangle to Right
  u8 kGROUP_UP;         // Portable > Side 1
  u8 kSCN;              // Portable > Open Circle
  u8 kMON;              // Portable > 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;
"""

MEM_SIZE = 0x8000  # 32,768 bytes
BLOCK_SIZE = 256
BLOCKS = MEM_SIZE // BLOCK_SIZE
MEM_BLOCKS = list(range(0, BLOCKS))

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

ACK_CMD = b"\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 + r"_-*()/\-+=)"
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] = "Continuous"
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",                   # Possible portable only, check 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 momentary",
    0x41: "Monitor B: Open Toggle",
    0x42: "Monitor C: Carrier momentary",
    0x43: "Monitor D: Carrier toggle",
    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
    }
KEY_MAP = [(v, k) for k, v in KEYS.items()]


def _raw_recv(radio, amount):
    """Raw read from the radio device"""
    try:
        data = radio.pipe.read(amount)
    except Exception as e:
        LOG.exception('Failed read: %s', e)
        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 Exception as e:
        LOG.exception('Failed write: %s', e)
        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, b"E")


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


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)
        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 == b"\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 a single block from the radio"""

    cmd = _raw_recv(radio, 1)
    # when the RX block has two bytes and the first is \x5A
    # then the block is all \xFF
    if not cmd:
        raise errors.RadioError('No response from radio')
    elif cmd == b'Z':
        # Empty "zero" block
        _raw_recv(radio, 1)
        _handshake(radio, "after zero block")
        return b'\xff' * 256
    elif cmd != b'W':
        LOG.error('Got non-W command:')
        rxdata = _raw_recv(radio, BLOCK_SIZE + 1)
        LOG.error(util.hexprint(rxdata))
        raise errors.RadioError('Received unexpected response from radio')
    else:
        rxdata = _raw_recv(radio, BLOCK_SIZE + 1)
        rcs = rxdata[-1]
        data = rxdata[:-1]
        ccs = _checksum(data)

        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"""
    # linux min is 0.13, win min is 0.25; set to bigger to be safe
    radio.pipe.timeout = 1
    radio.pipe.parity = "E"

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

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

    _raw_send(radio, b"PROGRAM")
    ack = _raw_recv(radio, 1)
    if ack != ACK_CMD:
        _close_radio(radio)
        LOG.debug("Radio did not accept PROGRAM command")
        raise errors.RadioError("The radio did not start program mode")

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

    _raw_send(radio, b"\x02")
    rid = _raw_recv(radio, 8)

    # See issue #12227 and associated ones. Some strange (potentially
    # pre-release) models seem to identify with a lowercase form factor
    # character. Allow these and let the variant check sort out the
    # differences).
    if not (radio.TYPE in rid.upper()):
        # 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(b"\xff"), radio.TYPE))

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

    status.msg = "Radio ident success!"
    radio.status_fn(status)
    # a pause
    time.sleep(1)


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

    # open the radio
    _open_radio(radio, status)

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

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

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

        time.sleep(0.1)

        # now we get the data
        data += _recv(radio)

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

        count += 1

    _close_radio(radio)
    return memmap.MemoryMapBytes(data)


def do_upload(radio):
    """ The upload function """
    # UI progress
    status = chirp_common.Status()
    data = b""
    count = 0

    # open the radio
    _open_radio(radio, status)

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

    # 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 written only if not empty
        if addr in radio.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(b"Z", addr) + b"\xFF"
        else:
            cs = _checksum(data)
            frame = _make_frame(b"W", addr) + data + bytes([cs])

        _raw_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 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):
            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 doesn'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_Series_60G(chirp_common.CloneModeRadio,
                         chirp_common.ExperimentalRadio):
    """Kenwood Series 60G Radios base class"""
    VENDOR = "Kenwood"
    BAUD_RATE = 9600
    _memsize = MEM_SIZE
    NAME_LENGTH = 8
    _range = [136000000, 162000000]
    _upper = 128
    _chs_progs = 0
    _num_banks = 128
    _bclass = memBank
    _kind = ""
    VARIANT = ""
    MODEL = ""
    RO_BLOCKS = list(range(0x10, 0x1F)) + list(range(0x59, 0x5f))

    @classmethod
    def get_prompts(cls):
        rp = chirp_common.RadioPrompts()
        rp.experimental = \
            'This driver is experimental: Use at your own risk! '
        rp.pre_download = _(
            "Follow these instructions to download your radio:\n"
            "1 - Turn off your radio\n"
            "2 - Connect your interface cable\n"
            "3 - Turn on your radio (unlock it if password protected)\n"
            "4 - Click OK to start\n")
        rp.pre_upload = _(
            "Follow these instructions to upload your radio:\n"
            "1 - Turn off your radio\n"
            "2 - Connect your interface cable\n"
            "3 - Turn on your radio (unlock it if password protected)\n"
            "4 - Click OK to start\n")
        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)
        rf.valid_tuning_steps = [1.0, 2.5, 5.0, 6.25, 12.5]
        return rf

    def _fill(self, offset, data):
        """Fill an specified area of the memmap with the passed data"""
        self._mmap[offset] = data

    def _prep_data(self):
        """Prepare the areas in the memmap to do a consistent 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 Exception:
                    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 = b""

        for k, v in data.items():
            # possible bad data
            if k == 0:
                k = 1
                raise errors.InvalidValueError(
                    "Invalid bank value '%s', bad data in the image? "
                    "Trying to fix this, review your bank data!" % k)
            c = 1
            for i in v:
                fdata += bytes([k, c, k - 1, i])
                c = c + 1

        # fill to match a full 256 bytes block
        fdata += (len(fdata) % 256) * b"\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 = b""
        for i in range(1, len(data) + 1):
            line = bytes([i, len(data[i])])
            line += b"\xff" * 14
            bdata += line

        # fill to match a full 256 bytes block
        bdata += (256 - (len(bdata)) % 256) * b"\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 = b"\x00\xf0\xff\xff\xff" * self._chs_progs + \
            b"\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 according
        to the correct variant of the radio"""
        rid = self._mmap[0xA7:0xAE]

        # identify the radio variant and set the environment to its values
        try:
            self._upper, low, high, self._kind = self.VARIANTS[rid]

            # Frequency ranges: some model/variants are able to work the near
            # ham bands, even if they are outside the OEM ranges.
            # By experimentation we found that 4% at the edges is in most
            # cases safe and will cover the near ham bands in full
            self._range = [int(low * 1000000 * 0.96),
                           int(high * 1000000 * 1.04)]

            # setting the bank data in the features, 8 & 16 CH don't 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]: "
            # In the OEM string we show the real OEM ranges
            self._VARIANT += self._kind + ", %d - %d MHz" % (low, high)

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

    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 = b"Chirp"
        self._fill(0xbb, sign)

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

    def process_mmap(self):
        """Process the memory object"""
        # how many channels are programmed
        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 - 1])

    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

        # A "blank" rxfreq is the indication of an empty memory
        if _mem.rxfreq[0].get_raw(asbytes=False) == '\xFF':
            mem.empty = True
            return mem

        # Freq and offset
        mem.freq = int(_mem.rxfreq) * 10
        # tx freq can be blank
        if _mem.get_raw(asbytes=False)[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(' \xff')

        # 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",
                            RadioSettingValueInteger(1, 128, b))
        mem.extra.append(bank)

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

        bnumb = RadioSetting("bnumb", "Bank index",
                             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 memory
        if mem.empty:
            _mem.set_raw("\xFF" * 48)
            return

        # frequency
        _mem.rxfreq = mem.freq / 10

        # this are a mystery yet, but so far there is no impact
        # with 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

        # 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 / PLANNED
        # adjust feqs
        # freqs = RadioSettingGroup("freqs", "Adjust Frequencies")

        top = RadioSettings(basic, dealer, fkeys)

        # Basic
        tot = RadioSetting("settings.tot", "Time Out Timer (TOT)",
                           RadioSettingValueList(
                               TOT, current_index=TOT.index('%i' % sett.tot)))
        basic.append(tot)

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

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

        totreset = RadioSetting("settings.tot_reset", "TOT reset time",
                                RadioSettingValueList(
                                    TOT_RESET, current_index=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, current_index=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, current_index=tv))
            basic.append(tvol)

        sql = RadioSetting("settings.sql_level", "SQL Ref Level",
                           RadioSettingValueList(
                               SQL, current_index=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)

        offhook = RadioSetting('settings.off_hook_decode', 'Off-hook decode',
                               RadioSettingValueBoolean(
                                   not sett.off_hook_decode))
        offhook.set_doc('Squelch mode active when mic is off-hook')
        basic.append(offhook)

        # dealer
        valid_chars = ",-/:[]" + chirp_common.CHARSET_ALPHANUMERIC
        mstr = "".join([c for c in self._VARIANT if c in valid_chars])

        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 wrapped here
        if self.TYPE[0] == "M":
            vu = RadioSetting("keys.kVOL_UP", "VOL UP",
                              RadioSettingValueMap(KEY_MAP,
                                                   int(keys.kVOL_UP)))
            fkeys.append(vu)

            vd = RadioSetting("keys.kVOL_DOWN", "VOL DOWN",
                              RadioSettingValueMap(KEY_MAP,
                                                   int(keys.kVOL_DOWN)))
            fkeys.append(vd)

            chu = RadioSetting("keys.kCH_UP", "CH UP",
                               RadioSettingValueMap(KEY_MAP,
                                                    int(keys.kCH_UP)))
            fkeys.append(chu)

            chd = RadioSetting("keys.kCH_DOWN", "CH DOWN",
                               RadioSettingValueMap(KEY_MAP,
                                                    int(keys.kCH_DOWN)))
            fkeys.append(chd)

            foot = RadioSetting("keys.kFOOT", "Foot switch",
                                RadioSettingValueMap(KEY_MAP,
                                                     int(keys.kFOOT)))
            fkeys.append(foot)

        # this is the common buttons for all

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

            scn = RadioSetting("keys.kSCN", scn_name,
                               RadioSettingValueMap(KEY_MAP,
                                                    int(keys.kSCN)))
            fkeys.append(scn)

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

            a = RadioSetting("keys.kA", a_name,
                             RadioSettingValueMap(KEY_MAP,
                                                  int(keys.kA)))
            fkeys.append(a)

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

            da = RadioSetting("keys.kDA", da_name,
                              RadioSettingValueMap(KEY_MAP,
                                                   int(keys.kDA)))
            fkeys.append(da)

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

            gu = RadioSetting("keys.kGROUP_UP", gu_name,
                              RadioSettingValueMap(KEY_MAP,
                                                   int(keys.kGROUP_UP)))
            fkeys.append(gu)

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

        gd = RadioSetting("keys.kGROUP_DOWN", gd_name,
                          RadioSettingValueMap(KEY_MAP,
                                               int(keys.kGROUP_DOWN)))
        fkeys.append(gd)

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

        mon = RadioSetting("keys.kMON", mon_name,
                           RadioSettingValueMap(KEY_MAP,
                                                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 ValueError:
                        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", "off_hook_decode"]:
                    value = bool(value)

                    # this cases are inverted
                    if setting in ["c2t", "off_hook_decode"]:
                        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 = list(KEYS.keys())[list(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 Exception as e:
            LOG.warning('Failed to set bank: %s', e)
            msg = "You can't have a channel without a bank, click another bank"
            raise errors.InvalidDataError(msg)


# This kenwwood family is known as "60-G Series"
# all this radios ending in G are compatible:
#
# Portables VHF TK-260G/270G/272G/278G
# Portables UHF TK-360G/370G/372G/378G/388G
#
# Mobiles VHF TK-760G/762G/768G
# Mobiles VHF TK-860G/862G/868G
#
# WARNING !!!! Radios With Password in the data section ###############
#
# When a radio has a data password (aka to program it) the last byte (#8)
# in the id code change from \xf1 to \xb1; so we remove this last byte
# from the identification procedures and variants.
#
# This effectively render the data password USELESS even if set.
# Translation: Chirps will read and write password protected radios
# with no problem.


@directory.register
class TK868G_Radios(Kenwood_Series_60G):
    """Kenwood TK-868G Radio M/C"""
    MODEL = "TK-868G"
    TYPE = b"M8680"
    VARIANTS = {
        b"M8680\x18\xff":    (8, 400, 490, "M"),
        b"M8680;\xff":       (128, 350, 390, "C1"),
        b"M86808\xff":       (128, 400, 430, "C2"),
        b"M86806\xff":       (128, 450, 490, "C3"),
        }


@directory.register
class TK862G_Radios(Kenwood_Series_60G):
    """Kenwood TK-862G Radio K/E/(N)E"""
    MODEL = "TK-862G"
    TYPE = b"M8620"
    VARIANTS = {
        b"M8620\x06\xff":    (8, 450, 490, "K"),
        b"M8620\x07\xff":    (8, 485, 512, "K2"),
        b"M8620&\xff":       (8, 440, 470, "E"),
        b"M8620V\xff":       (8, 440, 470, "(N)E"),
        }


@directory.register
class TK860G_Radios(Kenwood_Series_60G):
    """Kenwood TK-860G Radio K"""
    MODEL = "TK-860G"
    TYPE = b"M8600"
    VARIANTS = {
        b"M8600\x08\xff":    (128, 400, 430, "K"),
        b"M8600\x06\xff":    (128, 450, 490, "K1"),
        b"M8600\x07\xff":    (128, 485, 512, "K2"),
        b"M8600\x18\xff":    (128, 400, 430, "M"),
        b"M8600\x16\xff":    (128, 450, 490, "M1"),
        b"M8600\x17\xff":    (128, 485, 520, "M2"),
        }


@directory.register
class TK768G_Radios(Kenwood_Series_60G):
    """Kenwood TK-768G Radios [M/C]"""
    MODEL = "TK-768G"
    TYPE = b"M7680"
    # Note that 8 CH don't have banks
    VARIANTS = {
        b"M7680\x15\xff": (8, 136, 162, "M2"),
        b"M7680\x14\xff": (8, 148, 174, "M"),
        b"M76805\xff":    (128, 136, 162, "C2"),
        b"M76804\xff":    (128, 148, 174, "C"),
        }


@directory.register
class TK762G_Radios(Kenwood_Series_60G):
    """Kenwood TK-762G Radios [K/E/NE]"""
    MODEL = "TK-762G"
    TYPE = b"M7620"
    # Note that 8 CH don't have banks
    VARIANTS = {
        b"M7620\x05\xff": (8, 136, 162, "K2"),
        b"M7620\x04\xff": (8, 148, 172, "K"),
        b"M7620$\xff":    (8, 148, 172, "E"),
        b"M7620T\xff":    (8, 148, 172, "NE"),
        }


@directory.register
class TK760G_Radios(Kenwood_Series_60G):
    """Kenwood TK-760G Radios [K/M/(N)E]"""
    MODEL = "TK-760G"
    TYPE = b"M7600"
    VARIANTS = {
        b"M7600\x05\xff": (128, 136, 162, "K2"),
        b"M7600\x04\xff": (128, 148, 174, "K"),
        b"M7600\x14\xff": (128, 148, 174, "M"),
        b"M7600T\xff":    (128, 148, 174, "NE")
        }


@directory.register
class TK388G_Radios(Kenwood_Series_60G):
    """Kenwood TK-388 Radio [K/E/M/NE]"""
    MODEL = "TK-388G"
    TYPE = b"P3880"
    VARIANTS = {
        b"P3880\x1b\xff": (128, 350, 370, "M")
        }


@directory.register
class TK378G_Radios(Kenwood_Series_60G):
    """Kenwood TK-378 Radio [K/E/M/NE]"""
    MODEL = "TK-378G"
    TYPE = b"P3780"
    VARIANTS = {
        b"P3780\x16\xff": (16, 450, 470, "M"),
        b"P3780\x17\xff": (16, 400, 420, "M1"),
        b"P3780\x36\xff": (128, 490, 512, "C"),
        b"P3780\x39\xff": (128, 403, 430, "C1")
        }


@directory.register
class TK372G_Radios(Kenwood_Series_60G):
    """Kenwood TK-372 Radio [K/E/M/NE]"""
    MODEL = "TK-372G"
    TYPE = b"P3720"
    VARIANTS = {
        b"P3720\x06\xff": (32, 450, 470, "K"),
        b"P3720\x07\xff": (32, 470, 490, "K1"),
        b"P3720\x08\xff": (32, 490, 512, "K2"),
        b"P3720\x09\xff": (32, 403, 430, "K3")
        }


@directory.register
class TK370G_Radios(Kenwood_Series_60G):
    """Kenwood TK-370 Radio [K/E/M/NE]"""
    MODEL = "TK-370G"
    TYPE = b"P3700"
    VARIANTS = {
        b"P3700\x06\xff": (128, 450, 470, "K"),
        b"P3700\x07\xff": (128, 470, 490, "K1"),
        b"P3700\x08\xff": (128, 490, 512, "K2"),
        b"P3700\x09\xff": (128, 403, 430, "K3"),
        b"P3700\x16\xff": (128, 450, 470, "M"),
        b"P3700\x17\xff": (128, 470, 490, "M1"),
        b"P3700\x18\xff": (128, 490, 520, "M2"),
        b"P3700\x19\xff": (128, 403, 430, "M3"),
        b"P3700&\xff": (128, 440, 470, "E"),
        b"P3700V\xff": (128, 440, 470, "NE")
        }


@directory.register
class TK360G_Radios(Kenwood_Series_60G):
    """Kenwood TK-360 Radio [K/E/M/NE]"""
    MODEL = "TK-360G"
    TYPE = b"P3600"
    VARIANTS = {
        b"P3600\x06\xff": (8, 450, 470, "K"),
        b"P3600\x07\xff": (8, 470, 490, "K1"),
        b"P3600\x08\xff": (8, 490, 512, "K2"),
        b"P3600\x09\xff": (8, 403, 430, "K3"),
        b"P3600&\xff": (8, 440, 470, "E"),
        b"P3600)\xff": (8, 406, 430, "E1"),
        b"P3600\x16\xff": (8, 450, 470, "M"),
        b"P3600\x17\xff": (8, 470, 490, "M1"),
        b"P3600\x19\xff": (8, 403, 430, "M2"),
        b"P3600V\xff": (8, 440, 470, "NE"),
        b"P3600Y\xff": (8, 403, 430, "NE1")
        }


@directory.register
class TK278G_Radios(Kenwood_Series_60G):
    """Kenwood TK-278G Radio C/C1/M/M1"""
    MODEL = "TK-278G"
    TYPE = b"P2780"
    # Note that 16 CH don't have banks
    VARIANTS = {
        b"P27805\xff":    (128, 136, 150, "C1"),
        b"P27804\xff":    (128, 150, 174, "C"),
        b"P2780\x15\xff": (16,  136, 150, "M1"),
        b"P2780\x14\xff": (16,  150, 174, "M")
        }


@directory.register
class TK272G_Radios(Kenwood_Series_60G):
    """Kenwood TK-272G Radio K/K1"""
    MODEL = "TK-272G"
    TYPE = b"P2720"
    VARIANTS = {
        # NOTE: This is technically 136-150 MHz, but the radio supports
        # the full range for RX at least
        b"P2720\x05\xfb": (32, 136, 174, "K1"),
        b"P2720\x04\xfb": (32, 150, 174, "K")
        }


@directory.register
class TK270G_Radios(Kenwood_Series_60G):
    """Kenwood TK-270G Radio K/K1/M/E/NE/NT"""
    MODEL = "TK-270G"
    TYPE = b"P2700"
    VARIANTS = {
        b"P2700T\xff":    (128, 146, 174, "NE/NT"),
        b"P2700$\xff":    (128, 146, 174, "E"),
        b"P2700\x14\xff": (128, 150, 174, "M"),
        b"P2700\x05\xff": (128, 136, 150, "K1"),
        b"P2700\x04\xff": (128, 150, 174, "K")
        }


@directory.register
class TK260G_Radios(Kenwood_Series_60G):
    """Kenwood TK-260G Radio K/K1/M/E/NE/NT"""
    MODEL = "TK-260G"
    _hasbanks = False
    TYPE = b"P2600"
    RO_BLOCKS = list(range(0x10, 0x1F)) + list(range(0x52, 0x5f))
    VARIANTS = {
        b"P2600U\xff":    (8, 136, 150, "N1"),
        b"P2600T\xff":    (8, 146, 174, "N"),
        b"P2600$\xff":    (8, 150, 174, "E"),
        b"P2600\x14\xff": (8, 150, 174, "M"),
        b"P2600\x05\xff": (8, 136, 150, "K1"),
        b"P2600\x04\xff": (8, 150, 174, "K"),
        # See issue #12227 for discussion of this odd variant
        b"p2600\x24\xfb": (8, 150, 174, "E?"),
        }
