# HG changeset patch
# User Brad Schuler <brad@schuler.ws>
# Date 1605936471 25200
#      Fri Nov 20 22:27:51 2020 -0700
# Node ID 3f46ece7ba12a91774b44e76d897146cf4a7a5b5
# Parent  fb0e42bb94ed2d8e95dac61ae38a86bb267d1019
[anytone_iii] Add the new radio AnyTone 5888UV-III

Related to issue #3941

Add support for the tri-band AnyTone 5888UV-III.
Limited inclusion of 2-tone, 5-tone, and scramble settings.

diff -r fb0e42bb94ed -r 3f46ece7ba12 chirp/drivers/anytone_iii.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/anytone_iii.py	Fri Nov 20 22:27:51 2020 -0700
@@ -0,0 +1,1335 @@
+# 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.
+        # However, to pass the CHIRP TestCaseBanks, I must throw an error.  This causes more code to execute than necessary.
+        # I could just call _radio.clr_bank, but now I must get_bank and compare.
+        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 1=x0300 1111=xDA0B 3300=x3323 3400=x4524
+# TODO: Emergency shows duration of tx/rx as 0/0 when QPS shows 1/1 - may have to do with mode selection
+# 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,        // combine with rxcode for index of which rx DCS to use
+	   txdcsextra:1,        // combine with txcode for index of which 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[x00-x32] & x33=custom
+	u8 unknown4:2,
+	   rxtone:6;            // TONES[x00-x32] & x33=custom
+	u8 txcode;              // combine with txdcsextra for index of which tx DCS to use
+	u8 rxcode;              // combine with rxdcsextra for index of which rx DCS to use
+	u8 unknown5:2,
+	   pttid:2,             // [Off, Begin, End, Begin&End]
+	   unknown6:2,
+	   bclo:2;              // [Off, Repeater, Busy]
+	u8 unknown7;
+	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: 0=freq 1=chan 2=name
+  u8 unknown2:3,
+     sqlLvlLeft:5;			// Left Squelch Level: d1-d20(x14)
+  u8 unknown3:3,
+     sqlLvlRight:5;			// Right Squelch Level: d1-d20(x14)
+  u8 unknown4;
+  u8 unknown5:6,
+     scanTime:2;			// Scan Pause Time: 0=TO 1=CO 2=SE
+  u8 unknown6:7,
+     scanMode:1;			// Scan Mode: 0=MEM 1=MSM(Pri Scan)
+  u8 colorRed;              // Background color red intensity 0-x1f displayed as 1-32d
+  u8 colorGreen;            // Background color green intensity 0-x1f displayed as 1-32d
+  u8 colorBlue;             // Background color blue intensity 0-x1f displayed as 1-32d
+  u8 unknown7:5,
+     tbstFreq:3;            // TBST Freq [0-4] := [Off, 1750, 2100, 1000, 1450] Hz
+  u8 unknown8:3,
+     talkTimeout:5;			// Talk Timeout Timer: 0x00=Off 01=1min 1d=30min (1 for each min)
+  u8 unknown9:6,
+     pttKeyLock:2;          // PTT Key Lock: [0-3] := [Off, Right, Left, Both]
+  u8 unknown10:3,
+     apo:5;					// Auto Power Off: 0x00=off 01=30min 18=12hr (1 for each 30min)
+  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;          // Deputy channel (sub band) mute when primary is doing this: [0-3] := [Off, Tx, Rx, Tx & Rx]
+  //#seekto 0x0850;
+  u8 dispChanLock:1,		// Display Channel Lock when display=1(Chan): 0=not locked 1=locked
+     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,      // Eliminate Squelch Tail When no CTCSS/DCS Signaling: 0=no 1=yes
+     sqlKeyFunc:1;          // SQL Key Function: 0=Momentary 1=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: [0-2] := [Off, 120, 180] Degrees
+  u8 unknown20:3,
+     keyAFunc:5;			// Programmable key A function [0-19(x12)] := [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: 0=no 1=yes (verify password is set!)
+  u8 unknown25:5,
+     sqlRfLvlLeft:3;		// Left RF Squelch Level: 0=Off 1=S-2 2=S-5 3=S-9 4=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, but parser cannot do that.
+    // 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 displayed as [0-2500] 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 displayed as [10-2500] 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)
+//  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 & 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                             UNCONTROLLED
+//u8 zeros[512];
+
+//#seekto 0x0b00;				  // 2 Tone memories (24 slots of 16 bytes) - for future work
+//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                             UNCONTROLLED
+//u8 twentyfours[128];
+
+//#seekto 0x0d00;				  // 5 Tone configs (100 slots of 32 bytes)       UNCONTROLLED
+//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                             UNCONTROLLED
+//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                                      UNCONTROLLED
+//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                                      UNCONTROLLED
+//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)
+#         L-108                   R-136                  L-220           L-350                   L/R-400
+BANDS = [(108000000, 180000000), (136000000, 174000000), ONEANDQTRBAND, (350000000, 399995000), (400000000, 512000000)]
+BCLO = ['Off', 'Repeater', 'Busy']
+BEEP_VOL = ['Low', 'High']
+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'] + ['%d' % x for x in range(1, 10)] + ['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 has no way to present the TONES list instead of the 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 = ["Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS", "DTCS->Tone", "->Tone", "DTCS->DTCS"]
+        rf.valid_duplexes = DUPLEXES
+        rf.valid_tuning_steps = TUNING_STEPS
+        rf.valid_bands = 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
+        rf.memory_bounds = (1, len(self._memobj.channels))
+        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 it
+        # 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
+        _mem.set_raw("\x00" * 32)
+        _flg.unused = 0
+
+        _mem.freq = mem.freq
+        if (mem.freq in ONEANDQTRBAND or mem.mode == "AM") and not _isHyperChannel:
+            _flg.leftSideOnly = 1
+
+        _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", 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(0, 255, _emergency.txDuration)))
+        emer.append(RadioSetting("rxDuration", "RX Duration (sec)", RadioSettingValueInteger(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 == 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 == 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 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]
