# Copyright 2020 Brad Schuler K0BAS <brad@schuler.ws>
#
# 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 3 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 os
import struct
import time
import logging
import string

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

from chirp.ui.memdetail import MemoryDetailEditor


class ATBankModel(chirp_common.BankModel):
    """Anytone Banks A-J, Each chan in zero or one bank"""

    def __init__(self, radio, name='Banks'):
        super(ATBankModel, self).__init__(radio, name)
        self._banks = []
        self._memBounds = range(0, 750)
        for i in range(0, 10):
            self._banks.append(chirp_common.Bank(self, i, string.uppercase[i]))

    def get_num_mappings(self):
        return len(self._banks)

    def get_mappings(self):
        return self._banks

    def add_memory_to_mapping(self, memory, bank):
        self._radio.set_bank(memory.number, bank.get_index())

    def remove_memory_from_mapping(self, memory, mapping):
        # I would argue that removing a memory from a mapping in which it does
        # not exist should not throw an error.  The end result is the
        # requested result.  The memory is not in the mapping.
        index = self._radio.get_bank(memory.number)
        if (index is None or index != mapping.get_index()):
            raise Exception("Memory %d is not in bank %s" %
                            (memory.number, bank.get_name()))
        self._radio.clr_bank(memory.number)

    def get_mapping_memories(self, bank):
        memories = []
        for i in self._memBounds:
            bankIdx = self._radio.get_bank(i)
            if (bankIdx is not None and bankIdx == bank.get_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 []
        else:
            return [self._banks[index]]


LOG = logging.getLogger(__name__)

# TODO: Reverse engineer scramble codes
#       Examples: 1=x0300 1111=xDA0B 3300=x3323 3400=x4524
# TODO: Reverse engineer derived-value for 2Tone frequencies
#       Below are some samples:
#       freq    tone    derived
#               0000    0000
#       288.0   0b40    0823    208.3
#       321.7   0c91    0749    186.5
#       526.8   1494    2397    911.1
#       526.9   1495    2395    910.9
#       712.3   1bd3    1a52    673.8
#       928.1   2441    0286     64.6
#       1527.3  3ba9    0c46    314.2
#       1934.7  4b93    09b1    248.1
#       3116.0  79b8    000c      1.2


mem_format_thruflags = """
struct flag {                   // Flags about a channel
  u8 leftSideOnly:1,    // Channel is 1.25m or AM (meaning left side only)
     unused:1,          // Unused flag
     scan:2,            // chirp_common.SKIP_VALUES
     group:4;           // [A-J, Off]
};

#seekto 0x0020;                 // Read Only
struct {
  u8 unknown1[16];
  u8 unknown2[16];
  char modellike[14];
  u8 unknown3[2];
  char serial[16];
  u8 zeros16[16];
  char date[16];
  u8 zeros128[128];
} oem_info;

#seekto 0x0100;
struct flag chanFlags[750]; // Channel flags
struct flag limitFlags[10]; // Limit channel flags
//u8 effs8[8];
"""

mem_format = mem_format_thruflags + """
struct memory {
    bbcd freq[5];           // rx freq
    bbcd offset[3];         // tx offset from rx / 100
    u8 unknown1:4,
       tune_step:4;         // [2.5k, 5k, 6.25k, 8.33k, 10k,
                            //  12.5k, 15k, 20k, 25k, 30k]
    u8 rxdcsextra:1,        // use with rxcode for index of rx DCS to use
       txdcsextra:1,        // use with txcode for index of tx DCS to use
       rxdcsinv:1,          // inverse DCS rx polarity
       txdcsinv:1,          // inverse DCS tx polarity
       channel_width:2,     // [25kHz, 20kHz, 12.5kHz]
       rev:1,               // reverse the tx and rx frequencies & tones
       txoff:1;             // prevent tx
    u8 talkaround:1,        // Use the rx freq & tone when transmitting
       compander:1,
       unknown2:1,
       is_am:1,             // modulation is AM
       power:2,             // [High, Mid1, Mid2, Low]
       duplex:2;            // ['', -, +, off]
    u8 dtmfSlotNum:4,       // zero-based DTMF slot to use for optional
                            // signalling (when optsig = DTMF)
       rxtmode:2,           // ['', Tone, DTCS]
       txtmode:2;           // ['', Tone, DTCS]
    u8 unknown3:2,
       txtone:6;            // TONES[0..50] & 51=custom
    u8 unknown4:2,
       rxtone:6;            // TONES[0..50] & 51=custom
    u8 txcode;              // use with txdcsextra for index of tx DCS to use
    u8 rxcode;              // use with rxdcsextra for index of rx DCS to use
    u8 unknown5:2,
       pttid:2,             // [Off, Begin, End, Begin&End]
       unknown6:2,
       bclo:2;              // [Off, Repeater, Busy]
    u8 unknown7:6
       band:2;              // [2m, 1-1/4m, 350+ MHz, 70cm]
    u8 unknown8:5,
       sqlMode:3;           // [Carrier, CTCSS/DCS Tones, Opt Sig Only,
                            //  Tones & Opt Sig, Tones or Opt Sig]
                            // Implemented in GUI as a combination of rxmode
                            // and opt sig squelch list OPT_SIG_SQL
    u8 unknown9:6,
       optsig:2;            // [Off, DTMF, 2Tone, 5Tone]
    u8 unknown10:3,
       twotone:5;           // zero-based 2-tone slot to use for optional
                            // signalling (when optsig = 2Tone)
    u8 unknown11:1,
       fivetone:7;          // zero-based 5-tone slot to use for optional
                            // signalling (when optsig = 5Tone)
    u8 unknown12:4,
       scramble:4;          // [Off, 1 ... 9, Define 1, Define 2]
    char name[7];
    ul16 custtone;          // custom ctcss tone (%5.1f) * 10
};

struct DTMF12p3 {
  u8 dtmf[12];              // Each nibble is a DTMF code [0-9A-D*#]
  u8 numDigits;             // Quantity of nibbles/digits in above
  u8 unknown3[3];
};

struct DTMF7 {
  u8 dtmf[7];               // Each nibble is a DTMF code [0-9A-D*#]
  u8 numDigits;             // Quantity of nibbles/digits in above
};

#seekto 0x0400;                 // DTMF slots
struct DTMF12p3 dtmfSlots[16];

//#seekto 0x0500;
//u8 unknown[64];

#seekto 0x0540;                 // Hyper channels (12 x 2)
struct memory hyperChannels[24];

#seekto 0x0840;                 // Settings
struct {
  u8 unknown1:6,
     display:2;             // Display Mode: [freq, chan, name]
  u8 unknown2:3,
     sqlLvlLeft:5;          // Left Squelch Level: 1..20
  u8 unknown3:3,
     sqlLvlRight:5;         // Right Squelch Level: 1..20
  u8 unknown4;
  u8 unknown5:6,
     scanTime:2;            // Scan Pause Time: [TO, CO, SE]
  u8 unknown6:7,
     scanMode:1;            // Scan Mode: [MEM, MSM(Pri Scan)]
  u8 colorRed;              // Background color red intensity
                            // 0-x1f displayed as 1-32d
  u8 colorGreen;            // Background color green intensity
  u8 colorBlue;             // Background color blue intensity
  u8 unknown7:5,
     tbstFreq:3;            // TBST Freq [Off, 1750, 2100, 1000, 1450] Hz
  u8 unknown8:3,
     talkTimeout:5;         // Talk Timeout Timer: [Off, 1..30] minutes
  u8 unknown9:6,
     pttKeyLock:2;          // PTT Key Lock: [Off, Right, Left, Both]
  u8 unknown10:3,
     apo:5;                 // Auto Power Off: [Off, 1..18] * 30 minutes
  u8 unknown11:3,
     keyLightLvl:5;         // Key Light Level: [00-1e] := [1-32]
  u8 longKeyTime;           // Long Key Time: # seconds * 100,
                            // so 100 := 1.0 sec, 250 := 2.5 sec
  u8 unknown12:6,
     deputyMute:2;          // Mute the Deputy channel (sub band) when
                            // primary is doing this: [Off, Tx, Rx, Tx & Rx]
  //#seekto 0x0850;
  u8 dispChanLock:1,        // Display Channel Lock when display=1(Chan)
     unknown13:1,
     keypadLock:1,          // Lock the keypad
     beepHi:1,              // 0=low 1=high volume
     noInit:1,              // Prohibit initialization: 0=no 1=yes
     unknown14:3;
  u8 unknown15:2,
     clkShift:1,            // CLK Shift: 0=off 1=on
     extSpkEnable:1,        // 0=off 1=on
     altKeyMode:1,          // Use Alt Keypad Mode: 0=off 1=on
     beepEnable:1           // 0=off 1=on
     noCodeElimTail:1,      // Elim squelch tail when no CTCSS/DCS signaling
     sqlKeyFunc:1;          // SQL Key Function: [Momentary, Toggle]
  u8 unknown16:5,
     dtmfEncodePretime:3;   // DTMF encode preload time:
                            // [100, 300, 500, 800, 1000] ms
  u8 unknown17:5,
     dtmfSpeed:3;           // DTMF transmit time/speed:
                            // [30, 50, 80, 100, 150, 200, 250] ms
  u8 unknown19:6,
     tailElimType:2;        // Tail Eliminator Type: [Off, 120, 180] degrees
  u8 unknown20:3,
     keyAFunc:5;            // Programmable key A function [0-19]:
                            // [Off, DUP, ... Bandwidth, Talk Around]
  u8 unknown21:3,
     keyBFunc:5;            // Programmable key B function
  u8 unknown22:3,
     keyCFunc:5;            // Programmable key C function
  u8 unknown23:3,
     keyDFunc:5;            // Programmable key D function
  u8 unknown24:7,
     useBootPwd:1;          // Use boot password (verify password is set!)
  u8 unknown25:5,
     sqlRfLvlLeft:3;        // Left RF Squelch Level:
                            // [Off, S-2, S-5, S-9, FULL]
  u8 unknown26:5,
     sqlRfLvlRight:3;       // Right RF Squelch Level
  u8 scramble1[2];          // Scramble code #1 (unknown encoding)
  u8 scramble2[2];          // Scramble code #2 (unknown encoding)

  //#seekto 0x0860 & 0x0880
  struct {
    u8 unknown1;
    u8 unknown2:6,
        leftMode:2;        // [VFO, Channel, Home]
    u8 unknown3:6,
        rightMode:2;       // [VFO, Channel, Home]
    u8 unknown4:6,
        subDisplay:2;      // [Freq, DC IN, Off]
    ul16 leftChannel;      // Zero-based chan number
    ul16 rightChannel;     // Zero-based chan number
    u8 unknown5;
    u8 unknown6:6,
        speakers:2;        // Hand/Main [Off/On, On/On, On/Off]
    u8 unknown7:6,
        leftVfo:2;         // [108, 220, 350, 400] MHz
    u8 unknown8:7,
        rightVfo:1;        // [136, 400] MHz
    u8 unknown9;
    u8 unknown10:1,
        vfoTracked:1,
        autoHyperSave:1,
        autoRptSplit:1,
        autoAm:1,
        main:1,            // [Left, Right]
        vfoBandEdge:1,
        unknown11:1;
    u8 unknown12;
    u8 unknown13;
    //#seekto 0x0870 & 0x0890
    u8 unknown14:4,
        leftWorkBank:4;    // [A - J, Off]
    u8 unknown15:7,
        leftBankMode:1;    // [CH, Bank]
    u8 unknown16:7,
        leftBankSwitch:1;  // [Off, On]
    u8 unknown17:4,
        rightWorkBank:4;   // [A - J, Off]
    u8 unknown18:7,
        rightBankMode:1;   // [CH, Bank]
    u8 unknown19:7,
        rightBankSwitch:1; // [Off, On]
    // This would be better than below:
    //   struct { u8 unknown:7, linked:1; } linkedBanks[10];
    // They would need to be in a sub-group of settings in the extras.
    // A-J (one byte each) 00 or 01
    u8 unknown20:7,
        linkBankA:1;
    u8 unknown21:7,
        linkBankB:1;
    u8 unknown22:7,
        linkBankC:1;
    u8 unknown23:7,
        linkBankD:1;
    u8 unknown24:7,
        linkBankE:1;
    u8 unknown25:7,
        linkBankF:1;
    u8 unknown26:7,
        linkBankG:1;
    u8 unknown27:7,
        linkBankH:1;
    u8 unknown28:7,
        linkBankI:1;
    u8 unknown29:7,
        linkBankJ:1;
  } hyperSettings[2];

  //#seekto 0x08a0;
  char bootPassword[7];     // Boot password
  u8 unknown30[9];

  //#seekto 0x08b0;
  u8 unknown31[16];

  //#seekto 0x08c0;
  u8 unknown32:4,
     dtmfIntervalChar:4;    // DTMF interval character
  u8 dtmfGroupCode;         // DTMF Group Code: 0a-0f,ff displayed as A-#,Off
  u8 unknown33:6,
     dtmfDecodeResp:2;      // DTMF Decoding Response:
                            // [None, Beep, Beep & Respond]
  u8 unknown34;
  u8 dtmfFirstDigTime;      // DTMF First digit time: 0..250 * 10 ms
  u8 dtmfAutoResetTime;     // DTMF auto reset time: 0..250
                            // displayed as 0.0-25.0 in 0.1 increments sec
  u8 unknown35;
  u8 dtmfSelfId[3];         // DTMF Self ID: low nibble of each byte is a
                            // DTMF char, exactly 3 chars
  u8 unknown36;
  u8 unknown37;
  u8 unknown38;
  u8 unknown39:7,
     dtmfSideToneOn:1;      // DTMF side tone enabled
  u8 dtmfEncodeDelay;       // DTMF encode delay: 1..250 * 10 ms
  u8 dtmfPttIdDelay;        // DTMF PTT ID delay: 0,5-75
                            // shown as [Off,5-75] sec

  //#seekto 0x08d0;
  struct DTMF7 dtmfKill;    // DTMF to remotely kill
  struct DTMF7 dtmfStun;    // DTMF to remotely stun
} settings;

//#seekto 0x08e0;                   // 2 Tone settings - for future work
//struct {
//  ul16 callTone1;           // Call format 1st tone freq * 10
//  ul16 callTone2;           // Call format 2nd tone freq * 10
//                            // (or 00 00 for long tone version of 1st tone)
//  ul16 callTone1derived;    // some derived freq? calculated from 1st tone
//  ul16 callTone2derived;    // some derived freq? calculated from 2nd tone
//  u8 unknown1:6,
//     decodeResponse:2;      // Decode response [None, Beep, Beep & Respond]
//  u8 tone1Dur;              // 1st tone duration * 10
//                            // shown as range(0.5, 10, 0.1) seconds
//  u8 tone2Dur;              // 2nd tone duration * 10
//                            // shown as range(0.5, 10, 0.1) seconds
//  u8 longToneDur;           // Long tone duration * 10
//                            // shown as range(0.5, 10, 0.1) seconds
//  u8 gapTime;               // Gap time / 100
//                            // shown as range(0, 2000, 100) msec
//  u8 autoResetTime;         // Auto reset time * 10
//                            // shown as range(0, 25, 0.1) sec
//  u8 unkonwn:7,
//     encodeSideTone;        // Encode side-tone 0=off 1=on
//  u8 unknown2:4,
//     callFormat:4;          // Call format [A-B, A-C, A-D, B-A, B-C, B-D,
//                            //              C-A, C-B, C-D, D-A, D-B, D-C,
//                            //              Long A, Long B, Long C]
//                            // Be sure to change callTone1/2 and
//                            // callTone1/2derived when callFormat is changed
//  //#seekto 0x08f0;
//  u16 aTone;                // A tone freq * 10
//  u16 bTone;                // B tone freq * 10
//  u16 cTone;                // C tone freq * 10
//  u16 dTone;                // D tone freq * 10
//  u8 zeros[8];
//} twoToneSettings;

//#seekto 0x0900;                 // Unknown all 0x00
//u8 zeros[512];

//#seekto 0x0b00;                 // 2 Tone memories (24 slots of 16 bytes)
//struct {
//  ul16 tone1derived;        // some derived freq? calculated from 1st tone
//  ul16 tone2derived;        // some derived freq? calculated from 2nd tone
//  ul16 tone1;               // 1st tone freq * 10
//  ul16 tone2;               // 2nd tone freq * 10
//  char name[7];             // Tone name (padded)
//  u8 zero;
//} twoToneSlots[24];

//#seekto 0x0c80;                 // Unknown all 0x24
//u8 twentyfours[128];

//#seekto 0x0d00;                 // 5 Tone configs (100 slots of 32 bytes)
//u8 fivetones[3200];

#seekto 0x1980;                 // Communication  notes
struct {
    char callId[5];         // Call ID (numeric, max 5 digits (x30-x39)
    u8 zeros[3];            // 0x00 * 3
} noteCalls[100];
struct {
    char name[7];           // Names (ALPHAnumeric max 7 chars)
    char space;             // 0x20
} noteNames[100];

//#seekto 0x1fc0;                 // Unknown all 0x00
//u8 zeros[64];

#seekto 0x2000;
struct memory channels[750];        // Normal channels (750 * 32 bytes)

//#seekto 0x7dc0;
struct memory limitChannels[10];    // Limit channels
                                    // (10 * 32 bytes: Five pair of lo/hi)

//#seekto 0x7f00;                 // Unknown
//u8 unknown[32];

#seekto 0x7f20;
struct DTMF12p3 dtmfPttIdBot;   // DTMF PTT ID Start (BOT)
struct DTMF12p3 dtmfPttIdEot;   // DTMF PTT ID End (EOT)

//#seekto 0x7f40;
//u8 unknown[64]

#seekto 0x7f80;                 // Emergency Information
struct {
  u8 unknown:6,
     mode:2;                // [Alarm, Transpond+Background,
                            //  Transpond+Alarm, Both]
  u8 unknown:6,
     eniType:2;             // [None, DTMF, 5Tone]
  u8 id;                    // When DTMF: [M1-M16], When 5Tone: 0-99
  u8 alarmTime;             // [1-255] seconds
  u8 txDuration;            // [0-255] seconds
  u8 rxDuration;            // [0-255] seconds
  u8 unknown:7,
     chanSelect:1;          // [Assigned, Selected]
  ul16 channel;             // [0-749]
  u8 cycle;                 // [Continuous, 1-255]
} emergency;
u8 unknown9[6];

//#seekto 0x7f90;                 // Unknown
//u8 unknown[48]

#seekto 0x7fc0;                 // Welcome message
char welcome[7];
//u8 zeros[9];

//#seekto 0x7fd0;
//u8 unknown[48];
"""


def _is_chan_used(memobj, num):
    return not memobj.chanFlags[num].unused


def _is_limit_chan_used(memobj, num):
    return not memobj.limitFlags[num].unused


def _should_send_addr(memobj, addr):
    # Skip read-only & out of bounds
    if addr < 0x0100 or addr >= 0x8000:
        return False
    # Skip regions of unknown or uncontrolled by this software
    if ((addr >= 0x0500 and addr < 0x0540) or
            (addr >= 0x08e0 and addr < 0x1980) or
            (addr >= 0x1fc0 and addr < 0x2000) or
            (addr >= 0x7f00 and addr < 0x7f20) or
            (addr >= 0x7f40 and addr < 0x7f80) or
            (addr >= 0x7f90 and addr < 0x7fc0) or
            addr >= 0x7fd0):
        return False
    # Skip unused memories
    if addr >= 0x2000 and addr < 0x7dc0:
        return _is_chan_used(memobj, int((addr - 0x2000) / 0x20))
    if addr >= 0x7dc0 and addr < 0x7f00:
        return _is_limit_chan_used(memobj, int((addr - 0x7dc0) / 0x20))
    return True


def _echo_write(radio, data):
    try:
        radio.pipe.write(data)
        radio.pipe.read(len(data))
    except Exception, e:
        LOG.error("Error writing to radio: %s" % e)
        raise errors.RadioError("Unable to write to radio")


def _read(radio, length):
    try:
        data = radio.pipe.read(length)
    except Exception, e:
        LOG.error("Error reading from radio: %s" % e)
        raise errors.RadioError("Unable to read from radio")

    if len(data) != length:
        LOG.error("Short read from radio (%i, expected %i)" %
                  (len(data), length))
        LOG.debug(util.hexprint(data))
        raise errors.RadioError("Short read from radio")
    return data

valid_model = ['I588UVP']


def _ident(radio):
    radio.pipe.timeout = 1
    _echo_write(radio, "PROGRAM")
    response = radio.pipe.read(3)
    if response != "QX\x06":
        LOG.debug("Response was:\n%s" % util.hexprint(response))
        raise errors.RadioError("Unsupported model")
    _echo_write(radio, "\x02")
    response = radio.pipe.read(16)
    LOG.debug(util.hexprint(response))
    if response[15] != "\x06":
        LOG.debug("Response was:\n%s" % util.hexprint(response))
        raise errors.RadioError("Missing ack")
    if response[0:7] not in valid_model:
        LOG.debug("Response was:\n%s" % util.hexprint(response))
        raise errors.RadioError("Unsupported model")
    # Manufacturer software does this also: _send(radio, 'R', 0x0080, 0x10)


def _finish(radio):
    endframe = "\x45\x4E\x44"
    _echo_write(radio, endframe)
    result = radio.pipe.read(1)
    if result != "\x06":
        LOG.debug("Got:\n%s" % util.hexprint(result))
        raise errors.RadioError("Radio did not finish cleanly")


def _checksum(data):
    cs = 0
    for byte in data:
        cs += ord(byte)
    return cs % 256


def _send(radio, cmd, addr, length, data=None):
    frame = struct.pack(">cHb", cmd, addr, length)
    if data:
        frame += data
        frame += chr(_checksum(frame[1:]))
        frame += "\x06"
    _echo_write(radio, frame)
    LOG.debug("Sent:\n%s" % util.hexprint(frame))
    if data:
        result = radio.pipe.read(1)
        if result != "\x06":
            LOG.debug("Ack was: %s" % repr(result))
            raise errors.RadioError(
                "Radio did not accept block at %04x" % addr)
        return
    result = _read(radio, length + 6)
    LOG.debug("Got:\n%s" % util.hexprint(result))
    header = result[0:4]
    data = result[4:-2]
    ack = result[-1]
    if ack != "\x06":
        LOG.debug("Ack was: %s" % repr(ack))
        raise errors.RadioError("Radio NAK'd block at %04x" % addr)
    _cmd, _addr, _length = struct.unpack(">cHb", header)
    if _addr != addr or _length != _length:
        LOG.debug("Expected/Received:")
        LOG.debug(" Length: %02x/%02x" % (length, _length))
        LOG.debug(" Addr: %04x/%04x" % (addr, _addr))
        raise errors.RadioError("Radio send unexpected block")
    cs = _checksum(result[1:-2])
    if cs != ord(result[-2]):
        LOG.debug("Calculated: %02x" % cs)
        LOG.debug("Actual:     %02x" % ord(result[-2]))
        raise errors.RadioError("Block at 0x%04x failed checksum" % addr)
    return data


def _download(radio):
    _ident(radio)

    memobj = None

    data = ""
    for start, end in radio._ranges:
        for addr in range(start, end, 0x10):
            if memobj is not None and not _should_send_addr(memobj, addr):
                block = "\x00" * 0x10
            else:
                block = _send(radio, 'R', addr, 0x10)
            data += block

            status = chirp_common.Status()
            status.cur = len(data)
            status.max = end
            status.msg = "Cloning from radio"
            radio.status_fn(status)

            if addr == 0x0400 - 0x10:
                memobj = bitwise.parse(mem_format_thruflags, data)

    _finish(radio)

    return memmap.MemoryMap(data)


def _upload(radio):
    _ident(radio)

    for start, end in radio._ranges:
        for addr in range(start, end, 0x10):
            if not _should_send_addr(radio._memobj, addr):
                continue
            block = radio._mmap[addr:addr + 0x10]
            _send(radio, 'W', addr, len(block), block)

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

    _finish(radio)


def _filter(s, charset):
    s_ = ""
    for i in range(0, len(s)):
        c = str(s[i])
        s_ += (c if c in charset else "")
    return s_


SEVEN_SPACES = "       "
APO = ['Off'] + ['%.1f hour(s)' % (0.5 * x) for x in range(1, 25)]
ONEANDQTRBAND = (220000000, 260000000)
LEFT_BANDS = [
    (108000000, 180000000),  # L-108
    ONEANDQTRBAND,           # L-220
    (350000000, 399995000),  # L-350
    (400000000, 523000000)   # L/R-400
]
RIGHT_BANDS = [
    (136000000, 174000000),  # R-136
    (400000000, 523000000)   # L/R-400
]
BCLO = ['Off', 'Repeater', 'Busy']
BEEP_VOL = ['Low', 'High']
CROSS_MODES = list(chirp_common.CROSS_MODES)
CROSS_MODES.remove("Tone->")
DISPLAY = ['Freq', 'Chan#', 'Name']
DTMF_SLOTS = ['M%d' % x for x in range(1, 17)]
DUPLEXES = ['', '-', '+', 'off']
KEY_FUNCS = ['Off', 'DUP', 'PRI On', 'Power', 'Set DCS/CTCSS Code', 'MHz',
             'Reverse', 'HM Chan', 'Main L/R Switch', 'VFO/MR', 'Scan',
             'Squelch Off', 'Call TBST', 'Call', 'Tone Compande',
             'Scramble', 'Add Opt Signal', 'Bandwidth', 'Talk Around']
MODES = ["WFM", "FM", "NFM"]
OPT_SIGS = ['Off', 'DTMF', '2Tone', '5Tone']
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50),
                chirp_common.PowerLevel("Mid1", watts=25),
                chirp_common.PowerLevel("Mid2", watts=10),
                chirp_common.PowerLevel("Low", watts=5)]
PTT_IDS = ['Off', 'Begin', 'End', 'Begin & End']
RF_SQUELCHES = ['Off', 'S-2', 'S-5', 'S-9', 'FULL']
SCAN_PAUSES = ['TO', 'CO', 'SE']
SCAN_MODES = ['MEM', 'MSM (Pri Scan)']

SCRAMBLE_CODES = ['Off']
for x in range(1, 10):
    SCRAMBLE_CODES.append('%d' % x)
SCRAMBLE_CODES += ['Define 1', 'Define 2']

T_MODES = ['', 'Tone', 'DTCS', '']
TONE2_SLOTS = ['%d' % x for x in range(0, 24)]
TONE5_SLOTS = ['%d' % x for x in range(0, 100)]
# Future: chirp_common should present TONES instead of hard-coded list
TONES = [62.5] + chirp_common.TONES
TOT = ['Off'] + ['%s Minutes' % x for x in range(1, 31)]
TUNING_STEPS = [2.5, 5, 6.25, 8.33, 10, 12.5, 15, 20, 25, 30]
PTT_KEY_LOCKS = ["Off", "Right", "Left", "Both"]
TBST_FREQS = ["Off", "1750 Hz", "2100 Hz", "1000 Hz", "1450 Hz"]
LONG_KEY_TIMES = ["1 second", "1.5 seconds", "2 seconds", "2.5 seconds"]
DEPUTY_CHAN_MUTES = ["Off", "Tx", "Rx", "Both"]
SQL_BTN_MODES = ["Momentary", "Toggle"]
SQL_MODES = ["Carrier", "CTCSS/DCS", "Opt Sig Only", "Tones AND Sig",
             "Tones OR Sig"]
OPT_SIG_SQL = ["Off"] + SQL_MODES[2:]
TAIL_ELIM_TYPES = ["Off", "120 degrees", "180 degrees"]
LIMIT_NAMES = ["Limit %s %d" % (c, i + 1)
               for i in range(0, 5) for c in ('Lo', 'Hi')]
HYPER_NAMES = ["Hyper-%s %s%s" % (h, x, y)
               for h in ('1', '2')
               for x in ('', 'H')
               for y in ('L-108', 'L-220', 'L-350', 'L-400', 'R-136', 'R-400')]
HYPER_MODES = ['VFO', 'Channel', 'Home']
HYPER_SUB_DISPLAYS = ['Freq', 'DC IN', 'Off']
HYPER_SPKR_MODES = ['Mic off', 'Mic on', 'Main off']
HYPER_L_VFOS = ['108 MHz', '220 MHz', '350 MHz', '400 MHz']
HYPER_R_VFOS = ['136 MHz', '400 MHz']
HYPER_MAINS = ['Left', 'Right']
HYPER_BANK_MODES = ['CH', 'Bank']
EMER_MODES = ['Alarm', 'Transpond+Background', 'Transpond+Alarm', 'Both']
EMER_ENI_TYPES = ['None', 'DTMF', '5Tone']
EMER_CHAN_SEL = ['Assigned', 'Selected']
EMER_CYCLES = ['Continuous'] + ["%d seconds" % x for x in range(1, 256)]
BANK_CHOICES = ["%s" % chr(x) for x in range(ord("A"), ord("J") + 1)] + ['Off']
DTMF_CHARS = "0123456789ABCD*#"
DTMF_INTCHARS = "ABCD*#"
DTMF_PRELOADS = ['100 ms', '300 ms', '500 ms', '800 ms', '1000 ms']
DTMF_SPEEDS = ['30 ms', '50 ms', '80 ms', '100 ms',
               '150 ms', '200 ms', '250 ms']
DTMF_GROUPS = list("ABCDEF") + ['Off']
DTMF_RESPONSES = ['None', 'Beep', 'Beep & Respond']
DTMF_PTTDELAYS = ['Off'] + ["%d seconds" % x for x in range(5, 76)]


def _DtmfStrToBytes(s, numBytes):
    _bytes = []
    if len(s) != 0:
        _byte = 0x00
        for i in range(1, len(s)+1):
            if i % 2 == 0:
                _byte |= DTMF_CHARS.index(s[i-1])
                _bytes.append(_byte)
                _byte = 0x00
            else:
                _byte = DTMF_CHARS.index(s[i-1]) << 4
        if len(s) % 2 == 1:
            _bytes.append(_byte)
    while (len(_bytes) < numBytes):
        _bytes.append(0)
    return _bytes


def _BytesToDtmfStr(_bytes, numDigits):
    _s = ""
    x = 1
    while (x <= numDigits):
        _byte = _bytes[(x-1)/2]
        _s += DTMF_CHARS[_byte >> 4]
        x += 1
        if (x <= numDigits):
            _s += DTMF_CHARS[_byte & 0x0F]
            x += 1
    return _s


class _RadioSettingValueOffsetInt(RadioSettingValueInteger):
    """An integer setting whose real value is offset from displayed value"""

    def __init__(self, minval, maxval, current, step=1, offset=0):
        RadioSettingValueInteger.__init__(self, minval, maxval, current, step)
        self.offset = offset


@directory.register
class AnyTone5888UVIIIRadio(chirp_common.CloneModeRadio,
                            chirp_common.ExperimentalRadio):
    """AnyTone 5888UVIII"""
    VENDOR = "AnyTone"
    MODEL = "5888UVIII"
    BAUD_RATE = 9600
    _file_ident = "588UVP"

    _ranges = [
            (0x0000, 0x8000)
        ]

    def get_bank_model(self):
        return ATBankModel(self)

    @classmethod
    def get_prompts(cls):
        rp = chirp_common.RadioPrompts()
        rp.experimental = ("The Anytone 5888UVIII driver is currently "
                           "experimental. There are no known issues with it, "
                           "but you should proceed with caution. 2-tone, "
                           "5-tone, and scramble settings are limited. ")
        return rp

    def get_features(self):
        rf = chirp_common.RadioFeatures()
        rf.has_rx_dtcs = True
        rf.has_bank = True
        rf.has_cross = True
        rf.has_settings = True
        rf.valid_modes = MODES + ['AM']
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
        rf.valid_cross_modes = CROSS_MODES
        rf.valid_duplexes = DUPLEXES
        rf.valid_tuning_steps = TUNING_STEPS
        rf.valid_bands = LEFT_BANDS + RIGHT_BANDS
        rf.valid_skips = chirp_common.SKIP_VALUES
        rf.valid_power_levels = POWER_LEVELS
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC + "-"
        rf.valid_name_length = 7
        # Future: rf.valid_tones = TONES
        # Future: update memdetail.py to use rf.valid_dtcs_codes
        # Future: update memdetail.py to use rf.dtcs_polarity
        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
        try:
            rf.memory_bounds = (1, len(self._memobj.channels))
        except AttributeError:
            # This really doesn't need to be dynamic, AFAICT,
            # and doesn't work when we don't have a memory object
            # loaded. Default to the number defined above.
            rf.memory_bounds = (1, 750)
        rf.valid_special_chans = LIMIT_NAMES + HYPER_NAMES
        return rf

    def sync_in(self):
        self._mmap = _download(self)
        self.process_mmap()

    def sync_out(self):
        _upload(self)

    def process_mmap(self):
        self._memobj = bitwise.parse(mem_format, self._mmap)

    def _get_memobjs(self, number):
        if number <= len(self._memobj.channels):
            _mem = self._memobj.channels[number - 1]
            _flg = self._memobj.chanFlags[number - 1]
        elif (number <= len(self._memobj.channels) +
              len(self._memobj.limitChannels)):
            _mem = self._memobj.limitChannels[number -
                                              len(self._memobj.channels) -
                                              1]
            _flg = self._memobj.limitFlags[number -
                                           len(self._memobj.channels) -
                                           1]
        else:
            _mem = self._memobj.hyperChannels[number -
                                              len(self._memobj.channels) -
                                              len(self._memobj.limitChannels) -
                                              1]
            _flg = None
        return _mem, _flg

    def _get_dcs_index(self, _mem, which):
        base = getattr(_mem, '%scode' % which)
        extra = getattr(_mem, '%sdcsextra' % which)
        return (int(extra) << 8) | int(base)

    def _set_dcs_index(self, _mem, which, index):
        base = getattr(_mem, '%scode' % which)
        extra = getattr(_mem, '%sdcsextra' % which)
        base.set_value(index & 0xFF)
        extra.set_value(index >> 8)

    def get_raw_memory(self, number):
        _mem, _flg = self._get_memobjs(number)
        return repr(_mem) + repr(_flg)

    def set_bank(self, number, bankIdx):
        _mem, _flg = self._get_memobjs(number)
        _flg.group = bankIdx

    def clr_bank(self, number):
        _mem, _flg = self._get_memobjs(number)
        _flg.group = BANK_CHOICES.index('Off')

    def get_bank(self, number):
        _mem, _flg = self._get_memobjs(number)
        if (_flg.group < 0xa):
            return _flg.group
        return None

    def get_memory(self, number):
        mem = chirp_common.Memory()
        _isLimitChannel = _isHyperChannel = False
        if isinstance(number, str):
            if number in LIMIT_NAMES:
                _isLimitChannel = True
                mem.number = (len(self._memobj.channels) +
                              LIMIT_NAMES.index(number) +
                              1)
            elif number in HYPER_NAMES:
                _isHyperChannel = True
                mem.number = (len(self._memobj.channels) +
                              len(LIMIT_NAMES) +
                              HYPER_NAMES.index(number) +
                              1)
            mem.extd_number = number
        elif number > (len(self._memobj.channels) +
                       len(self._memobj.limitChannels)):
            _isHyperChannel = True
            mem.number = number
            mem.extd_number = HYPER_NAMES[number -
                                          len(self._memobj.channels) -
                                          len(self._memobj.limitChannels) -
                                          1]
        elif number > len(self._memobj.channels):
            _isLimitChannel = True
            mem.number = number
            mem.extd_number = LIMIT_NAMES[number -
                                          len(self._memobj.channels) -
                                          1]
        else:
            mem.number = number

        _mem, _flg = self._get_memobjs(mem.number)

        if not _isHyperChannel and _flg.unused:
            mem.empty = True
            _mem.set_raw("\x00" * 32)
            _mem.name = SEVEN_SPACES
            _flg.scan = 0

        mem.freq = int(_mem.freq)

        mem.offset = int(_mem.offset) * 100
        mem.name = (str(_mem.name).rstrip()
                    if not _isLimitChannel and not _isHyperChannel
                    else SEVEN_SPACES)
        mem.duplex = DUPLEXES[_mem.duplex]
        mem.mode = _mem.is_am and "AM" or MODES[_mem.channel_width]
        mem.tuning_step = TUNING_STEPS[_mem.tune_step]

        if _mem.txoff:
            mem.duplex = DUPLEXES[3]

        rxtone = txtone = None
        rxmode = T_MODES[_mem.rxtmode]
        if _mem.sqlMode == 0 or _mem.sqlMode == 2:
            rxmode = T_MODES.index('')
        txmode = T_MODES[_mem.txtmode]
        if txmode == "Tone":
            # If custom tone is being used, show as 88.5 and set checkbox in
            # extras.
            # Future: Improve chirp_common, so I can add "CUSTOM" into TONES
            if _mem.txtone == len(TONES):
                txtone = 88.5
            else:
                txtone = TONES[_mem.txtone]
        elif txmode == "DTCS":
            txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
                                                                     'tx')]
        if rxmode == "Tone":
            # If custom tone is being used, show as 88.5 and set checkbox in
            # extras.
            # Future: Improve chirp_common, so I can add "CUSTOM" into TONES
            if _mem.rxtone == len(TONES):
                rxtone = 88.5
            else:
                rxtone = TONES[_mem.rxtone]
        elif rxmode == "DTCS":
            rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
                                                                     'rx')]

        rxpol = _mem.rxdcsinv and "R" or "N"
        txpol = _mem.txdcsinv and "R" or "N"

        chirp_common.split_tone_decode(mem,
                                       (txmode, txtone, txpol),
                                       (rxmode, rxtone, rxpol))

        mem.skip = chirp_common.SKIP_VALUES[_flg.scan
                                            if not _isHyperChannel
                                            else 0]
        mem.power = POWER_LEVELS[_mem.power]

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

        rs = RadioSetting("rev", "Reverse",
                          RadioSettingValueBoolean(_mem.rev))
        mem.extra.append(rs)

        rs = RadioSetting("compander", "Compander",
                          RadioSettingValueBoolean(_mem.compander))
        mem.extra.append(rs)

        rs = RadioSetting("talkaround", "Talkaround",
                          RadioSettingValueBoolean(_mem.talkaround))
        mem.extra.append(rs)

        rs = RadioSetting("pttid", "PTT ID",
                          RadioSettingValueList(PTT_IDS,
                                                PTT_IDS[_mem.pttid]))
        mem.extra.append(rs)

        rs = RadioSetting("bclo", "Busy Channel Lockout",
                          RadioSettingValueList(BCLO,
                                                BCLO[_mem.bclo]))
        mem.extra.append(rs)

        rs = RadioSetting("optsig", "Optional Signaling",
                          RadioSettingValueList(OPT_SIGS,
                                                OPT_SIGS[_mem.optsig]))
        mem.extra.append(rs)

        rs = RadioSetting("OPTSIGSQL", "Squelch w/Opt Signaling",
                          RadioSettingValueList(
                              OPT_SIG_SQL,
                              SQL_MODES[_mem.sqlMode]
                              if SQL_MODES[_mem.sqlMode] in OPT_SIG_SQL
                              else "Off"))
        mem.extra.append(rs)

        rs = RadioSetting("dtmfSlotNum", "DTMF",
                          RadioSettingValueList(DTMF_SLOTS,
                                                DTMF_SLOTS[_mem.dtmfSlotNum]))
        mem.extra.append(rs)

        rs = RadioSetting("twotone", "2-Tone",
                          RadioSettingValueList(TONE2_SLOTS,
                                                TONE2_SLOTS[_mem.twotone]))
        mem.extra.append(rs)

        rs = RadioSetting("fivetone", "5-Tone",
                          RadioSettingValueList(TONE5_SLOTS,
                                                TONE5_SLOTS[_mem.fivetone]))
        mem.extra.append(rs)

        rs = RadioSetting("scramble", "Scrambler Switch",
                          RadioSettingValueList(SCRAMBLE_CODES,
                                                SCRAMBLE_CODES[_mem.scramble]))
        mem.extra.append(rs)

        # Memory properties dialog is only capable of Boolean and List
        # RadioSettingValue classes, so cannot configure custtone.
        # rs = RadioSetting("custtone", "Custom CTCSS",
        #                   RadioSettingValueFloat(
        #                       min(TONES),
        #                       max(TONES),
        #                       _mem.custtone and _mem.custtone / 10 or 151.1,
        #                       0.1,
        #                       1))
        # mem.extra.append(rs)
        custToneStr = chirp_common.format_freq(_mem.custtone)

        rs = RadioSetting("CUSTTONETX",
                          "Use Custom CTCSS (%s) for Tx" % custToneStr,
                          RadioSettingValueBoolean(_mem.txtone == len(TONES)))
        mem.extra.append(rs)

        rs = RadioSetting("CUSTTONERX",
                          "Use Custom CTCSS (%s) for Rx" % custToneStr,
                          RadioSettingValueBoolean(_mem.rxtone == len(TONES)))
        mem.extra.append(rs)

        return mem

    def set_memory(self, mem):
        _isLimitChannel = _isHyperChannel = False
        if mem.number > (len(self._memobj.channels) +
                         len(self._memobj.limitChannels)):
            _isHyperChannel = True
        elif mem.number > len(self._memobj.channels):
            _isLimitChannel = True

        _mem, _flg = self._get_memobjs(mem.number)

        if mem.empty:
            if _isHyperChannel:
                raise errors.InvalidValueError("Hyper memories may not be "
                                               "empty.")
            _mem.set_raw("\x00" * 32)
            _flg.unused = 1
            _flg.group = BANK_CHOICES.index('Off')
            return

        if mem.mode == "AM" and (mem.freq < LEFT_BANDS[0][0] or
                                 mem.freq > LEFT_BANDS[0][1]):
            raise errors.InvalidValueError("AM modulation only allowed in 2m "
                                           "band.")

        _mem.set_raw("\x00" * 32)
        _flg.unused = 0

        _mem.freq = mem.freq

        for _bandIdx in range(0, len(LEFT_BANDS)):
            if (mem.freq >= LEFT_BANDS[_bandIdx][0] and
                    mem.freq <= LEFT_BANDS[_bandIdx][1]):
                _mem.band = _bandIdx
                break

        _flg.leftSideOnly = 0
        if (not _isHyperChannel):
            if mem.mode == "AM":
                _flg.leftSideOnly = 1
            else:
                _flg.leftSideOnly = 1
                for _lo, _hi in RIGHT_BANDS:
                    if mem.freq >= _lo and mem.freq <= _hi:
                        _flg.leftSideOnly = 0
                        break

        _mem.offset = mem.offset / 100

        if _isHyperChannel:
            mem.name = SEVEN_SPACES
        else:
            _mem.name = mem.name.ljust(7)

        if mem.duplex == "off":
            _mem.duplex = DUPLEXES.index("")
            _mem.txoff = 1
        else:
            _mem.duplex = DUPLEXES.index(mem.duplex)
            _mem.txoff = 0

        _mem.is_am = mem.mode == "AM"

        _mem.tune_step = TUNING_STEPS.index(mem.tuning_step)

        try:
            _mem.channel_width = MODES.index(mem.mode)
        except ValueError:
            _mem.channel_width = 0

        ((txmode, txtone, txpol),
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)

        _mem.txtmode = T_MODES.index(txmode)
        _mem.rxtmode = T_MODES.index(rxmode)
        if rxmode != '':
            _mem.sqlMode = SQL_MODES.index("CTCSS/DCS")
        else:
            _mem.sqlMode = SQL_MODES.index("Carrier")
        if txmode == "Tone":
            _mem.txtone = TONES.index(txtone)
        elif txmode == "DTCS":
            self._set_dcs_index(_mem, 'tx',
                                chirp_common.ALL_DTCS_CODES.index(txtone))
        if rxmode == "Tone":
            _mem.rxtone = TONES.index(rxtone)
        elif rxmode == "DTCS":
            self._set_dcs_index(_mem, 'rx',
                                chirp_common.ALL_DTCS_CODES.index(rxtone))

        _mem.txdcsinv = txpol == "R"
        _mem.rxdcsinv = rxpol == "R"

        if not _isHyperChannel:
            _flg.scan = chirp_common.SKIP_VALUES.index(mem.skip)

        if mem.power:
            _mem.power = POWER_LEVELS.index(mem.power)
        else:
            _mem.power = 0

        for setting in mem.extra:
            if setting.get_name() == "ignore":
                LOG.debug("*** ignore: %s" % str(setting.value))
            # Future: elif setting.get_name() == "custtone":
            # Future: setattr(_mem, "custtone", setting.value.get_value() * 10)
            elif setting.get_name() == "OPTSIGSQL":
                if str(setting.value) != "Off":
                    _mem.sqlMode = SQL_MODES.index(str(setting.value))
            elif setting.get_name() == "CUSTTONETX":
                if setting.value:
                    _mem.txtone = len(TONES)
            elif setting.get_name() == "CUSTTONERX":
                if setting.value:
                    _mem.rxtone = len(TONES)
            else:
                setattr(_mem, setting.get_name(), setting.value)

        return mem

    def get_settings(self):
        allGroups = RadioSettings()

        _settings = self._memobj.settings

        basic = RadioSettingGroup("basic", "Basic")
        basic.append(
            RadioSetting(
                "welcome",
                "Welcome Message",
                RadioSettingValueString(
                    0,
                    7,
                    _filter(self._memobj.welcome,
                            chirp_common.CHARSET_ASCII))))
        basic.append(
            RadioSetting(
                "display",
                "Display",
                RadioSettingValueList(DISPLAY, DISPLAY[_settings.display])))
        basic.append(
            RadioSetting(
                "dispChanLock",
                "Lock Chan# Disp",
                RadioSettingValueBoolean(_settings.dispChanLock)))
        basic.append(
            RadioSetting(
                "scanTime",
                "Scan Pause Time",
                RadioSettingValueList(
                    SCAN_PAUSES,
                    SCAN_PAUSES[_settings.scanTime])))
        basic.append(
            RadioSetting(
                "scanMode",
                "Scan Mode",
                RadioSettingValueList(
                    SCAN_MODES,
                    SCAN_MODES[_settings.scanMode])))
        basic.append(
            RadioSetting(
                "talkTimeout",
                "Talk Timeout",
                RadioSettingValueList(TOT, TOT[_settings.talkTimeout])))
        basic.append(
            RadioSetting(
                "apo",
                "Auto Power Off",
                RadioSettingValueList(APO, APO[_settings.apo])))
        basic.append(
            RadioSetting(
                "beepEnable",
                "Beep",
                RadioSettingValueBoolean(_settings.beepEnable)))
        basic.append(
            RadioSetting(
                "beepHi",
                "Beep Volume",
                RadioSettingValueList(BEEP_VOL, BEEP_VOL[_settings.beepHi])))
        basic.append(
            RadioSetting(
                "extSpkEnable",
                "External Speaker",
                RadioSettingValueBoolean(_settings.extSpkEnable)))
        basic.append(
            RadioSetting(
                "deputyMute",
                "Deputy Chan Mute",
                RadioSettingValueList(
                    DEPUTY_CHAN_MUTES,
                    DEPUTY_CHAN_MUTES[_settings.deputyMute])))
        basic.append(
            RadioSetting(
                "tailElimType",
                "Tail Eliminator",
                RadioSettingValueList(
                    TAIL_ELIM_TYPES,
                    TAIL_ELIM_TYPES[_settings.tailElimType])))
        basic.append(
            RadioSetting(
                "noCodeElimTail",
                "Eliminate SQL Tail When\nNo Tone Signaling",
                RadioSettingValueBoolean(_settings.noCodeElimTail)))
        allGroups.append(basic)

        sqls = RadioSettingGroup("sql", "Squelch Levels")
        sqls.append(
            RadioSetting(
                "sqlLvlLeft",
                "Left Squelch",
                RadioSettingValueInteger(0, 20, _settings.sqlLvlLeft)))
        sqls.append(
            RadioSetting(
                "sqlLvlRight",
                "Right Squelch",
                RadioSettingValueInteger(0, 20, _settings.sqlLvlRight)))
        sqls.append(
            RadioSetting(
                "sqlRfLvlLeft",
                "Left RF Squelch",
                RadioSettingValueList(
                    RF_SQUELCHES,
                    RF_SQUELCHES[_settings.sqlRfLvlLeft])))
        sqls.append(
            RadioSetting(
                "sqlRfLvlRight",
                "Right RF Squelch",
                RadioSettingValueList(
                    RF_SQUELCHES,
                    RF_SQUELCHES[_settings.sqlRfLvlRight])))
        allGroups.append(sqls)

        keys = RadioSettingGroup("keys", "Keys / Buttons")
        keys.append(
            RadioSetting(
                "sqlKeyFunc",
                "SQL Key Mode",
                RadioSettingValueList(
                    SQL_BTN_MODES,
                    SQL_BTN_MODES[_settings.sqlKeyFunc])))
        keys.append(
            RadioSetting(
                "keyAFunc",
                "PA Key Function",
                RadioSettingValueList(
                    KEY_FUNCS,
                    KEY_FUNCS[_settings.keyAFunc])))
        keys.append(
            RadioSetting(
                "keyBFunc",
                "PB Key Function",
                RadioSettingValueList(
                    KEY_FUNCS,
                    KEY_FUNCS[_settings.keyBFunc])))
        keys.append(
            RadioSetting(
                "keyCFunc",
                "PC Key Function",
                RadioSettingValueList(
                    KEY_FUNCS,
                    KEY_FUNCS[_settings.keyCFunc])))
        keys.append(
            RadioSetting(
                "keyDFunc",
                "PD Key Function",
                RadioSettingValueList(
                    KEY_FUNCS,
                    KEY_FUNCS[_settings.keyDFunc])))
        keys.append(
            RadioSetting(
                "keyLightLvl",
                "Key Light Level",
                _RadioSettingValueOffsetInt(
                    1,
                    32,
                    _settings.keyLightLvl + 1,
                    1,
                    -1)))
        keys.append(
            RadioSetting(
                "pttKeyLock",
                "PTT Key Lock",
                RadioSettingValueList(
                    PTT_KEY_LOCKS,
                    PTT_KEY_LOCKS[_settings.pttKeyLock])))
        keys.append(
            RadioSetting(
                "keypadLock",
                "Keypad Lockout",
                RadioSettingValueBoolean(_settings.keypadLock)))
        allGroups.append(keys)

        bgColor = RadioSettingGroup("bgColor", "Background Color")
        bgColor.append(
            RadioSetting(
                "colorRed",
                "Red",
                _RadioSettingValueOffsetInt(
                    1,
                    32,
                    _settings.colorRed + 1,
                    1,
                    -1)))
        bgColor.append(
            RadioSetting(
                "colorGreen",
                "Green",
                _RadioSettingValueOffsetInt(
                    1,
                    32,
                    _settings.colorGreen + 1,
                    1,
                    -1)))
        bgColor.append(
            RadioSetting(
                "colorBlue",
                "Blue",
                _RadioSettingValueOffsetInt(
                    1,
                    32,
                    _settings.colorBlue + 1,
                    1,
                    -1)))
        allGroups.append(bgColor)

        pwdGroup = RadioSettingGroup("pwdGroup", "Boot Password")
        pwdGroup.append(
            RadioSetting(
                "useBootPwd",
                "Enable",
                RadioSettingValueBoolean(_settings.useBootPwd)))
        pwdGroup.append(
            RadioSetting(
                "bootPassword",
                "Password (digits)",
                RadioSettingValueString(
                    0,
                    7,
                    _filter(_settings.bootPassword, string.digits),
                    False,
                    string.digits)))
        allGroups.append(pwdGroup)

        advanced = RadioSettingGroup("advanced", "Advanced")
        advanced.append(
            RadioSetting(
                "tbstFreq",
                "TBST Freq",
                RadioSettingValueList(
                    TBST_FREQS,
                    TBST_FREQS[_settings.tbstFreq])))
        advanced.append(
            RadioSetting(
                "longKeyTime",
                "Long Key Time",
                RadioSettingValueList(
                    LONG_KEY_TIMES,
                    LONG_KEY_TIMES[int((_settings.longKeyTime - 100) / 50)])))
        advanced.append(
            RadioSetting(
                "clkShift",
                "CLK Shift",
                RadioSettingValueBoolean(_settings.clkShift)))
        advanced.append(
            RadioSetting(
                "altKeyMode",
                "Keypad Alt Mode",
                RadioSettingValueBoolean(_settings.altKeyMode)))
        advanced.append(
            RadioSetting(
                "noInit",
                "Prohibit Initialization",
                RadioSettingValueBoolean(_settings.noInit)))
        allGroups.append(advanced)

        hyperGroups = [
            RadioSettingGroup("hyper1", "Hyper 1"),
            RadioSettingGroup("hyper2", "Hyper 2")
        ]
        for idx in range(0, len(hyperGroups)):
            _hyperSettings = _settings.hyperSettings[idx]
            _hyperGroup = hyperGroups[idx]
            _hyperGroup.append(
                RadioSetting(
                    "main",
                    "Main Band",
                    RadioSettingValueList(
                        HYPER_MAINS,
                        HYPER_MAINS[_hyperSettings.main])))
            _hyperGroup.append(
                RadioSetting(
                    "subDisplay",
                    "Sub Display",
                    RadioSettingValueList(
                        HYPER_SUB_DISPLAYS,
                        HYPER_SUB_DISPLAYS[_hyperSettings.subDisplay])))
            _hyperGroup.append(
                RadioSetting(
                    "speakers",
                    "Speakers",
                    RadioSettingValueList(
                        HYPER_SPKR_MODES,
                        HYPER_SPKR_MODES[_hyperSettings.speakers])))
            _hyperGroup.append(
                RadioSetting(
                    "vfoBandEdge",
                    "VFO Band Lock",
                    RadioSettingValueBoolean(_hyperSettings.vfoBandEdge)))
            _hyperGroup.append(
                RadioSetting(
                    "autoAm",
                    "Auto AM",
                    RadioSettingValueBoolean(_hyperSettings.autoAm)))
            _hyperGroup.append(
                RadioSetting(
                    "autoRptSplit",
                    "Auto Repeater Shift",
                    RadioSettingValueBoolean(_hyperSettings.autoRptSplit)))
            _hyperGroup.append(
                RadioSetting(
                    "autoHyperSave",
                    "Auto Hyper Save",
                    RadioSettingValueBoolean(_hyperSettings.autoHyperSave)))
            _hyperGroup.append(
                RadioSetting(
                    "vfoTracked",
                    "VFO Tracking",
                    RadioSettingValueBoolean(_hyperSettings.vfoTracked)))
            _hyperGroup.append(
                RadioSetting(
                    "leftMode",
                    "Left Mode",
                    RadioSettingValueList(
                        HYPER_MODES,
                        HYPER_MODES[_hyperSettings.leftMode])))
            _hyperGroup.append(
                RadioSetting(
                    "rightMode",
                    "Right Mode",
                    RadioSettingValueList(
                        HYPER_MODES,
                        HYPER_MODES[_hyperSettings.rightMode])))
            _hyperGroup.append(
                RadioSetting(
                    "leftChannel",
                    "Left Channel",
                    _RadioSettingValueOffsetInt(
                        1,
                        750,
                        _hyperSettings.leftChannel + 1,
                        1,
                        -1)))
            _hyperGroup.append(
                RadioSetting(
                    "rightChannel",
                    "Right Channel",
                    _RadioSettingValueOffsetInt(
                        1,
                        750,
                        _hyperSettings.rightChannel + 1,
                        1,
                        -1)))
            _hyperGroup.append(
                RadioSetting(
                    "leftVfo",
                    "Left VFO Band",
                    RadioSettingValueList(
                        HYPER_L_VFOS,
                        HYPER_L_VFOS[_hyperSettings.leftVfo])))
            _hyperGroup.append(
                RadioSetting(
                    "rightVfo",
                    "Right VFO Band",
                    RadioSettingValueList(
                        HYPER_R_VFOS,
                        HYPER_R_VFOS[_hyperSettings.rightVfo])))
            _hyperGroup.append(
                RadioSetting(
                    "leftWorkBank",
                    "Left Work Bank",
                    RadioSettingValueList(
                        BANK_CHOICES,
                        BANK_CHOICES[_hyperSettings.leftWorkBank])))
            _hyperGroup.append(
                RadioSetting(
                    "rightWorkBank",
                    "Right Work Bank",
                    RadioSettingValueList(
                        BANK_CHOICES,
                        BANK_CHOICES[_hyperSettings.rightWorkBank])))
            _hyperGroup.append(
                RadioSetting(
                    "leftBankMode",
                    "Left Bank Mode",
                    RadioSettingValueList(
                        HYPER_BANK_MODES,
                        HYPER_BANK_MODES[_hyperSettings.leftBankMode])))
            _hyperGroup.append(
                RadioSetting(
                    "rightBankMode",
                    "Right Bank Mode",
                    RadioSettingValueList(
                        HYPER_BANK_MODES,
                        HYPER_BANK_MODES[_hyperSettings.rightBankMode])))
            _hyperGroup.append(
                RadioSetting(
                    "leftBankSwitch",
                    "Left Bank Switch",
                    RadioSettingValueBoolean(_hyperSettings.leftBankSwitch)))
            _hyperGroup.append(
                RadioSetting(
                    "rightBankSwitch",
                    "Right Bank Switch",
                    RadioSettingValueBoolean(_hyperSettings.rightBankSwitch)))
            for bank in BANK_CHOICES[0:-1]:
                _hyperGroup.append(
                    RadioSetting(
                        "linkBank%s" % bank,
                        "Bank %s" % bank,
                        RadioSettingValueBoolean(
                            getattr(_hyperSettings, "linkBank%s" % bank))))
            allGroups.append(_hyperGroup)

        notes = RadioSettingGroup("notes", "Comm Notes")
        for idx in range(0, 100):
            notes.append(
                RadioSetting(
                    "noteCalls.%d" % idx,
                    "Call ID %d (5 digits)" % idx,
                    RadioSettingValueString(
                        0,
                        5,
                        _filter(self._memobj.noteCalls[idx].callId,
                                string.digits)[idx*8:idx*8+5],
                        False,
                        string.digits)))
            notes.append(
                RadioSetting(
                    "noteNames.%d" % idx,
                    "Name %d (7 ALPHAnumeric)" % idx,
                    RadioSettingValueString(
                        0,
                        7,
                        _filter(self._memobj.noteNames[idx].name,
                                chirp_common.CHARSET_UPPER_NUMERIC)
                        [idx*8:idx*8+7],
                        True,
                        chirp_common.CHARSET_UPPER_NUMERIC)))
        allGroups.append(notes)

        _emergency = self._memobj.emergency
        emer = RadioSettingGroup("emergency", "Emergency Info")
        emer.append(
            RadioSetting(
                "mode",
                "Alarm Mode",
                RadioSettingValueList(
                    EMER_MODES,
                    EMER_MODES[_emergency.mode])))
        emer.append(
            RadioSetting(
                "eniType",
                "ENI Type",
                RadioSettingValueList(
                    EMER_ENI_TYPES,
                    EMER_ENI_TYPES[_emergency.eniType])))
        emer.append(
            RadioSetting(
                "dtmfId",
                "DTMF ID",
                RadioSettingValueList(
                    DTMF_SLOTS,
                    DTMF_SLOTS[_emergency.id]
                    if EMER_ENI_TYPES[_emergency.eniType] == 'DTMF'
                    else DTMF_SLOTS[0])))
        emer.append(
            RadioSetting(
                "5ToneId",
                "5Tone ID",
                RadioSettingValueInteger(
                    0,
                    99,
                    _emergency.id
                    if EMER_ENI_TYPES[_emergency.eniType] == '5Tone'
                    else 0)))
        emer.append(
            RadioSetting(
                "alarmTime",
                "Alarm Time (sec)",
                RadioSettingValueInteger(1, 255, _emergency.alarmTime)))
        emer.append(
            RadioSetting(
                "txDuration",
                "TX Duration (sec)",
                RadioSettingValueInteger(1 if _emergency.txDuration > 0 else 0,
                                         255,
                                         _emergency.txDuration)))
        emer.append(
            RadioSetting(
                "rxDuration",
                "RX Duration (sec)",
                RadioSettingValueInteger(1 if _emergency.rxDuration > 0 else 0,
                                         255,
                                         _emergency.rxDuration)))
        emer.append(
            RadioSetting(
                "chanSelect",
                "ENI Channel",
                RadioSettingValueList(
                    EMER_CHAN_SEL,
                    EMER_CHAN_SEL[_emergency.chanSelect])))
        emer.append(
            RadioSetting(
                "channel",
                "Assigned Channel",
                _RadioSettingValueOffsetInt(
                    1,
                    750,
                    _emergency.channel + 1,
                    1,
                    -1)))
        emer.append(
            RadioSetting(
                "cycle",
                "Cycle",
                RadioSettingValueList(
                    EMER_CYCLES,
                    EMER_CYCLES[_emergency.cycle])))
        allGroups.append(emer)

        dtmfGroup = RadioSettingGroup("dtmf", "DTMF")
        for idx in range(0, 16):
            dtmfGroup.append(
                RadioSetting(
                    "dtmfSlots.%d" % idx,
                    "Encode Tone %s" % DTMF_SLOTS[idx],
                    RadioSettingValueString(
                        0,
                        12,
                        _BytesToDtmfStr(self._memobj.dtmfSlots[idx].dtmf,
                                        self._memobj.dtmfSlots[idx].numDigits),
                        False,
                        DTMF_CHARS)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfEncodePretime",
                "Encode Preload Time",
                RadioSettingValueList(
                    DTMF_PRELOADS,
                    DTMF_PRELOADS[_settings.dtmfEncodePretime])))
        dtmfGroup.append(
            RadioSetting(
                "dtmfSpeed",
                "Speed",
                RadioSettingValueList(
                    DTMF_SPEEDS,
                    DTMF_SPEEDS[_settings.dtmfSpeed])))
        dtmfGroup.append(
            RadioSetting(
                "dtmfIntervalChar",
                "Interval Character",
                RadioSettingValueList(
                    list(DTMF_INTCHARS),
                    DTMF_CHARS[_settings.dtmfIntervalChar])))
        dtmfGroup.append(
            RadioSetting(
                "dtmfGroupCode",
                "Group Code",
                RadioSettingValueList(
                    DTMF_GROUPS,
                    DTMF_GROUPS[_settings.dtmfGroupCode-0x0a]
                    if _settings.dtmfGroupCode < 0xff
                    else DTMF_GROUPS[6])))
        dtmfGroup.append(
            RadioSetting(
                "dtmfDecodeResp",
                "Decode Response",
                RadioSettingValueList(
                    DTMF_RESPONSES,
                    DTMF_RESPONSES[_settings.dtmfDecodeResp])))
        dtmfGroup.append(
            RadioSetting(
                "dtmfFirstDigTime",
                "1st Digit Time (ms)",
                RadioSettingValueInteger(
                    0,
                    2500,
                    _settings.dtmfFirstDigTime * 10,
                    10)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfAutoResetTime",
                "Auto Reset Time (0-25 sec by 0.1)",
                RadioSettingValueFloat(
                    0,
                    25,
                    _settings.dtmfAutoResetTime / 10,
                    0.1,
                    1)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfSelfId",
                "Self ID",
                RadioSettingValueString(
                    3,
                    3,
                    (DTMF_CHARS[_settings.dtmfSelfId[0] & 0x0F] +
                     DTMF_CHARS[_settings.dtmfSelfId[1] & 0x0F] +
                     DTMF_CHARS[_settings.dtmfSelfId[2] & 0x0F]),
                    True,
                    DTMF_CHARS)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfSideToneOn",
                "Use Side Tone",
                RadioSettingValueBoolean(_settings.dtmfSideToneOn)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfEncodeDelay",
                "Encode Delay (ms)",
                RadioSettingValueInteger(
                    10,
                    2500,
                    _settings.dtmfEncodeDelay * 10,
                    10)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfPttIdDelay",
                "PTT ID Delay",
                RadioSettingValueList(
                    DTMF_PTTDELAYS,
                    DTMF_PTTDELAYS[_settings.dtmfPttIdDelay-4]
                    if _settings.dtmfPttIdDelay >= 5
                    else DTMF_PTTDELAYS[0])))
        dtmfGroup.append(
            RadioSetting(
                "dtmfKill",
                "Remote Kill",
                RadioSettingValueString(
                    0,
                    7,
                    _BytesToDtmfStr(_settings.dtmfKill.dtmf,
                                    _settings.dtmfKill.numDigits),
                    False,
                    DTMF_CHARS)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfStun",
                "Remote Stun",
                RadioSettingValueString(
                    0,
                    7,
                    _BytesToDtmfStr(_settings.dtmfStun.dtmf,
                                    _settings.dtmfStun.numDigits),
                    False,
                    DTMF_CHARS)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfPttIdBot",
                "PTT ID Start (BOT)",
                RadioSettingValueString(
                    0,
                    12,
                    _BytesToDtmfStr(self._memobj.dtmfPttIdBot.dtmf,
                                    self._memobj.dtmfPttIdBot.numDigits),
                    False,
                    DTMF_CHARS)))
        dtmfGroup.append(
            RadioSetting(
                "dtmfPttIdEot",
                "PTT ID Start (EOT)",
                RadioSettingValueString(
                    0,
                    12,
                    _BytesToDtmfStr(self._memobj.dtmfPttIdEot.dtmf,
                                    self._memobj.dtmfPttIdEot.numDigits),
                    False,
                    DTMF_CHARS)))
        allGroups.append(dtmfGroup)

        # TODO: 2-Tone settings
        # TODO: 5-Tone settings

        return allGroups

    def _setHyperSettings(self, settings, _hyperSettings):
        for element in settings:
            name = element.get_name()
            if not isinstance(element, RadioSetting):
                self._setHyperSettings(element, _hyperSettings)
                continue
            elif isinstance(element.value, _RadioSettingValueOffsetInt):
                setattr(_hyperSettings,
                        name,
                        int(element.value) + element.value.offset)
            else:
                setattr(_hyperSettings, name, element.value)

    def _setPassword(self, settings):
        _settings = self._memobj.settings
        usePwd = pwd = None
        for element in settings:
            name = element.get_name()
            if not isinstance(element, RadioSetting):
                self._setPassword(element)
                continue
            elif name == "useBootPwd":
                usePwd = bool(element.value)
            elif name == "bootPassword":
                pwd = _filter(str(element.value), string.digits)
        if (pwd is not None and (len(pwd) or usePwd is False)):
            setattr(_settings, "useBootPwd", usePwd and 1 or 0)
            pwd = pwd + "\x00"*(7-len(pwd))
            setattr(_settings, "bootPassword", pwd)
        elif (usePwd is not None and usePwd is True):
            raise errors.InvalidValueError("Password may not be enabled "
                                           "without being set.")

    def _setCommNotes(self, settings):
        _noteCalls = self._memobj.noteCalls
        _noteNames = self._memobj.noteNames
        for element in settings:
            name = str(element.get_name())
            if not isinstance(element, RadioSetting):
                self._setCommNotes(element)
                continue
            elif name.startswith("noteCalls."):
                idx = int(name[10:])
                setattr(self._memobj.noteCalls[idx],
                        "callId",
                        str(element.value).ljust(5, "\x00"))
            elif name.startswith("noteNames."):
                idx = int(name[10:])
                setattr(self._memobj.noteNames[idx],
                        "name",
                        str(element.value).ljust(7))

    def _setEmergency(self, settings):
        _emergency = self._memobj.emergency
        dtmfId = fiveToneId = None
        for element in settings:
            name = str(element.get_name())
            if not isinstance(element, RadioSetting):
                self._setEmergency(element)
                continue
            elif name == "dtmfId":
                dtmfId = element.value
            elif name == "5ToneId":
                fiveToneId = element.value
            elif name == "channel":
                zbMemNum = int(element.value) + element.value.offset
                _mem, _flg = self._get_memobjs(zbMemNum + 1)
                if _flg.unused:
                    raise errors.InvalidValueError("Assigned emergency ENI "
                                                   "channel is empty.")
                else:
                    setattr(_emergency, name, zbMemNum)
            elif isinstance(element.value, _RadioSettingValueOffsetInt):
                setattr(_emergency,
                        name,
                        int(element.value) + element.value.offset)
            else:
                setattr(_emergency, name, element.value)
        if EMER_ENI_TYPES[_emergency.eniType] == 'DTMF':
            setattr(_emergency, "id", dtmfId)
        elif EMER_ENI_TYPES[_emergency.eniType] == '5Tone':
            setattr(_emergency, "id", fiveToneId)

    def _setDtmfSettings(self, settings):
        def _setDtmf(dtmfStruct, newVal):
            dtmfStruct.numDigits.set_value(len(newVal))
            dtmfStruct.dtmf.set_value(_DtmfStrToBytes(newVal,
                                                      len(dtmfStruct.dtmf)))

        for element in settings:
            name = str(element.get_name())
            if not isinstance(element, RadioSetting):
                self._setDtmfSettings(element)
                continue
            if name.startswith("dtmfSlots."):
                idx = int(name[10:])
                _setDtmf(self._memobj.dtmfSlots[idx], str(element.value))
            elif name == "dtmfIntervalChar":
                setattr(self._memobj.settings.dtmfIntervalChar,
                        name,
                        DTMF_CHARS.index(str(element.value)))
            elif name == "dtmfGroupCode":
                setattr(self._memobj.settings,
                        name,
                        int(element.value)+0x0a
                        if int(element.value) != 6
                        else 0xff)
            elif name == "dtmfFirstDigTime" or name == "dtmfEncodeDelay":
                setattr(self._memobj.settings, name, int(element.value) / 10)
            elif name == "dtmfAutoResetTime":
                setattr(self._memobj.settings, name, int(element.value) * 10)
            elif name == "dtmfSelfId":
                newStr = str(element.value)
                newVal = []
                for charIdx in range(0, 3):
                    newVal.append(DTMF_CHARS.index(newStr[charIdx]))
                setattr(self._memobj.settings, name, newVal)
            elif name == "dtmfPttIdDelay":
                setattr(self._memobj.settings,
                        name,
                        DTMF_PTTDELAYS.index(str(element.value))+4
                        if element.value != DTMF_PTTDELAYS[0]
                        else 0)
            elif name == "dtmfKill" or name == "dtmfStun":
                _setDtmf(getattr(self._memobj.settings, name),
                         str(element.value))
            elif name == "dtmfPttIdBot" or name == "dtmfPttIdEot":
                _setDtmf(getattr(self._memobj, name), str(element.value))
            else:
                setattr(self._memobj.settings, name, element.value)

    def set_settings(self, settings):
        _root = self._memobj
        _settings = self._memobj.settings
        for element in settings:
            name = element.get_name()
            if isinstance(element, RadioSettingGroup):
                if name == "hyper1":
                    self._setHyperSettings(element, _settings.hyperSettings[0])
                    continue
                elif name == "hyper2":
                    self._setHyperSettings(element, _settings.hyperSettings[1])
                    continue
                elif name == "pwdGroup":
                    self._setPassword(element)
                    continue
                elif name == "notes":
                    self._setCommNotes(element)
                    continue
                elif name == "emergency":
                    self._setEmergency(element)
                    continue
                elif name == "dtmf":
                    self._setDtmfSettings(element)
                    continue
            if not isinstance(element, RadioSetting):
                self.set_settings(element)
                continue
            if name == "ignore":
                LOG.debug("*** ignore: %s" % str(element.value))
            elif name == "welcome":
                setattr(_root, "welcome", element.value)
            elif name == "longKeyTime":
                setattr(_settings, name, 100 + 50 * int(element.value))
            elif isinstance(element.value, _RadioSettingValueOffsetInt):
                setattr(_settings, name, (int(element.value) +
                                          element.value.offset))
            else:
                setattr(_settings, name, element.value)

    @classmethod
    def match_model(cls, filedata, filename):
        return cls._file_ident in filedata[0x40:0x4D]
