# HG changeset patch # User Brad Schuler # Date 1604358756 25200 # Mon Nov 02 16:12:36 2020 -0700 # Node ID af9c114dd8c5d13c0a1031103900d7db5262bc62 # Parent fb0e42bb94ed2d8e95dac61ae38a86bb267d1019 [anytone_iii] Add the new radio AnyTone 5888UV-III (Issue #3941) Limited inclusion of 2-tone, 5-tone, and scramble settings. diff -r fb0e42bb94ed -r af9c114dd8c5 chirp/drivers/anytone_iii.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/anytone_iii.py Mon Nov 02 16:12:36 2020 -0700 @@ -0,0 +1,1325 @@ +# Copyright 2020 Brad Schuler K0BAS +# +# 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 . + +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 = (0, 760) + 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): + 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 { // A channel definition + bbcd freq[5]; + bbcd offset[3]; + u8 unknown1:4, + tune_step:4; + u8 rxdcsextra:1, + txdcsextra:1, + rxdcsinv:1, + txdcsinv:1, + channel_width:2, + rev:1, + txoff:1; + u8 talkaround:1, + compander:1, + unknown2:1, + is_am:1, + power:2, + duplex:2; + u8 dtmfSlotNum:4, + rxtmode:2, + txtmode:2; + u8 unknown3:2, + txtone:6; + u8 unknown4:2, + rxtone:6; + u8 txcode; + u8 rxcode; + u8 unknown5:2, + pttid:2, + unknown6:2, + bclo:2; + 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; + u8 unknown10:3, + twotone:5; + u8 unknown11:1, + fivetone:7; + u8 unknown12:4, + scramble:4; + char name[7]; + ul16 custtone; +}; + +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, + 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 + // 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 +//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) +//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 = "QX588UVP" + + _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_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 + 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[0x20:0x40]