[chirp_devel] [PATCH] [termn8r] Add Anytone TERMN-8R Support #2437
Jim Unroe
Thu Apr 2 19:58:28 PDT 2015
# HG changeset patch
# User Jim Unroe <rock.unroe at gmail.com>
# Date 1428029723 14400
# Node ID 276bcdf9e7adfea093fb32847bfeb24472ff96ce
# Parent ee81cedd447b16eacca70ff6e8bff3552e4a5227
[termn8r] Add Anytone TERMN-8R Support #2437
Add basic spreadsheet editor support
Add a few "Function Setup" settings to get started
- Welcome Message (line 1 & line 2)
- Display Mode
- Automatic Power-off
Add an "extra" setting to get started
- Busy Channel Lockout
Bonus: Add equivalent Anytone OBLTR-8R support
#2437
diff -r ee81cedd447b -r 276bcdf9e7ad chirp/drivers/anytone_ht.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/anytone_ht.py Thu Apr 02 22:55:23 2015 -0400
@@ -0,0 +1,587 @@
+# Copyright 2013 Dan Smith <dsmith at danplanet.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 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
+
+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
+
+LOG = logging.getLogger(__name__)
+
+mem_format = """
+struct memory {
+ bbcd freq[4];
+ bbcd offset[4];
+ u8 unknown1:4,
+ tune_step:4;
+ u8 unknown2:2,
+ txdcsextra:1,
+ txinv:1,
+ channel_width:2,
+ unknown3:1,
+ tx_off:1;
+ u8 unknown4:2,
+ rxdcsextra:1,
+ rxinv:1,
+ power:2,
+ duplex:2;
+ u8 unknown5:4,
+ rxtmode:2,
+ txtmode:2;
+ u8 unknown6:2,
+ txtone:6;
+ u8 unknown7:2,
+ rxtone:6;
+ u8 txcode;
+ u8 rxcode;
+ u8 unknown8[3];
+ char name[6];
+ u8 squelch:4,
+ unknown9:2,
+ bcl:2;
+ u8 unknownA;
+ u8 unknownB:7,
+ sqlmode:1;
+ u8 unknownC[4];
+};
+
+#seekto 0x0010;
+struct {
+ u8 unknown1;
+ u8 unknown2:5,
+ bands1:3;
+ char model[7];
+ u8 unknown3:5,
+ bands2:3;
+ u8 unknown4[6];
+ u8 unknown5[16];
+ char date[9];
+ u8 unknown6[7];
+ u8 unknown7[16];
+ u8 unknown8[16];
+ char dealer[16];
+ char stockdate[9];
+ u8 unknown9[7];
+ char selldate[9];
+ u8 unknownA[7];
+ char seller[16];
+} oem_info;
+
+#seekto 0x0100;
+u8 used_flags[50];
+
+#seekto 0x0120;
+u8 skip_flags[50];
+
+#seekto 0x0220;
+struct {
+ u8 unknown1:6,
+ display:2;
+ u8 unknown2[19];
+ u8 unknown3:3,
+ apo:5;
+} settings;
+
+#seekto 0x03E0;
+struct {
+ char line1[6];
+ char line2[6];
+} welcome_msg;
+
+#seekto 0x2000;
+struct memory memory[200];
+"""
+
+
+def _echo_write(radio, data):
+ try:
+ radio.pipe.write(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 = ['TERMN8R', 'OBLTR8R']
+
+
+def _ident(radio):
+ radio.pipe.setTimeout(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("Radio did not respond. Check connection.")
+ _echo_write(radio, "\x02")
+ response = radio.pipe.read(16)
+ LOG.debug(util.hexprint(response))
+ if radio._file_ident not in response:
+ LOG.debug("Response was:\n%s" % util.hexprint(response))
+ raise errors.RadioError("Unsupported model")
+
+
+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):
+ 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)
+
+ _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 addr < 0x0100:
+ 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)
+
+
+APO = ['Off', '30 Min', '1 Hour', '2 Hours']
+BCLO = ['Off', 'Repeater', 'Busy']
+CHARSET = chirp_common.CHARSET_ASCII
+DISPLAY = ['Frequency', 'Channel', 'Name']
+DUPLEXES = ['', 'N/A', '-', '+', 'split', 'off']
+MODES = ["FM", "NFM"]
+POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5),
+ chirp_common.PowerLevel("Mid", watts=2),
+ chirp_common.PowerLevel("Low", watts=1)]
+TMODES = ['', 'Tone', 'DTCS', '']
+TONES = [62.5] + list(chirp_common.TONES)
+
+
+ at directory.register
+class AnyToneTERMN8RRadio(chirp_common.CloneModeRadio,
+ chirp_common.ExperimentalRadio):
+ """AnyTone TERMN-8R"""
+ VENDOR = "AnyTone"
+ MODEL = "TERMN-8R"
+ BAUD_RATE = 9600
+ _file_ident = "TERMN8R"
+
+ # May try to mirror the OEM behavior later
+ _ranges = [
+ (0x0000, 0x8000),
+ ]
+
+ @classmethod
+ def get_prompts(cls):
+ rp = chirp_common.RadioPrompts()
+ rp.experimental = ("The Anytone driver is currently experimental. "
+ "There are no known issues with it, but you should "
+ "proceed with caution.")
+ return rp
+
+ def get_features(self):
+ rf = chirp_common.RadioFeatures()
+ rf.has_settings = True
+ rf.has_bank = False
+ rf.has_cross = True
+ rf.has_tuning_step = False
+ rf.has_rx_dtcs = True
+ rf.valid_skips = ["", "S"]
+ rf.valid_modes = MODES
+ rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
+ rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
+ "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
+ rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES
+ rf.valid_bands = [(136000000, 174000000),
+ (400000000, 520000000)]
+ rf.valid_characters = CHARSET
+ rf.valid_name_length = 6
+ rf.valid_power_levels = POWER_LEVELS
+ rf.valid_duplexes = DUPLEXES
+ rf.can_odd_split = True
+ rf.memory_bounds = (0, 199)
+ 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_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_memory(self, number):
+ bitpos = (1 << (number % 8))
+ bytepos = (number / 8)
+
+ _mem = self._memobj.memory[number]
+ _skp = self._memobj.skip_flags[bytepos]
+ _usd = self._memobj.used_flags[bytepos]
+
+ mem = chirp_common.Memory()
+ mem.number = number
+
+ if _usd & bitpos:
+ mem.empty = True
+ return mem
+
+ mem.freq = int(_mem.freq) * 100
+ mem.offset = int(_mem.offset) * 100
+ mem.name = self.filter_name(str(_mem.name).rstrip())
+ mem.duplex = DUPLEXES[_mem.duplex]
+ mem.mode = MODES[_mem.channel_width]
+
+ if _mem.tx_off == True:
+ mem.duplex = "off"
+ mem.offset = 0
+
+ rxtone = txtone = None
+ rxmode = TMODES[_mem.rxtmode]
+ txmode = TMODES[_mem.txtmode]
+ if txmode == "Tone":
+ txtone = TONES[_mem.txtone]
+ elif txmode == "DTCS":
+ txtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
+ 'tx')]
+ if rxmode == "Tone":
+ rxtone = TONES[_mem.rxtone]
+ elif rxmode == "DTCS":
+ rxtone = chirp_common.ALL_DTCS_CODES[self._get_dcs_index(_mem,
+ 'rx')]
+
+ rxpol = _mem.rxinv and "R" or "N"
+ txpol = _mem.txinv and "R" or "N"
+
+ chirp_common.split_tone_decode(mem,
+ (txmode, txtone, txpol),
+ (rxmode, rxtone, rxpol))
+
+ if _skp & bitpos:
+ mem.skip = "S"
+
+ mem.power = POWER_LEVELS[_mem.power]
+
+ mem.extra = RadioSettingGroup("Extra", "extra")
+
+ rs = RadioSetting("bcl", "Busy Channel Lockout",
+ RadioSettingValueList(BCLO,
+ BCLO[_mem.bcl]))
+ mem.extra.append(rs)
+
+ return mem
+
+ def set_memory(self, mem):
+ bitpos = (1 << (mem.number % 8))
+ bytepos = (mem.number / 8)
+
+ _mem = self._memobj.memory[mem.number]
+ _skp = self._memobj.skip_flags[bytepos]
+ _usd = self._memobj.used_flags[bytepos]
+
+ if mem.empty:
+ _usd |= bitpos
+ _skp |= bitpos
+ _mem.set_raw("\xFF" * 32)
+ return
+ _usd &= ~bitpos
+
+ if _mem.get_raw() == ("\xFF" * 32):
+ LOG.debug("Initializing empty memory")
+ _mem.set_raw("\x00" * 32)
+ _mem.squelch = 3
+
+ _mem.freq = mem.freq / 100
+
+ if mem.duplex == "off":
+ _mem.duplex = DUPLEXES.index("")
+ _mem.offset = 0
+ _mem.tx_off = True
+ elif mem.duplex == "split":
+ diff = mem.offset - mem.freq
+ _mem.duplex = DUPLEXES.index("-") if diff < 0 \
+ else DUPLEXES.index("+")
+ _mem.offset = abs(diff) / 100
+ else:
+ _mem.offset = mem.offset / 100
+ _mem.duplex = DUPLEXES.index(mem.duplex)
+
+ _mem.name = mem.name.ljust(6)
+
+ 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 = TMODES.index(txmode)
+ _mem.rxtmode = TMODES.index(rxmode)
+ 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.sqlmode = 1
+ _mem.rxtone = TONES.index(rxtone)
+ elif rxmode == "DTCS":
+ _mem.sqlmode = 1
+ self._set_dcs_index(_mem, 'rx',
+ chirp_common.ALL_DTCS_CODES.index(rxtone))
+ else:
+ _mem.sqlmode = 0
+
+ _mem.txinv = txpol == "R"
+ _mem.rxinv = rxpol == "R"
+
+ if mem.skip:
+ _skp |= bitpos
+ else:
+ _skp &= ~bitpos
+
+ if mem.power:
+ _mem.power = POWER_LEVELS.index(mem.power)
+ else:
+ _mem.power = 0
+
+ for setting in mem.extra:
+ setattr(_mem, setting.get_name(), setting.value)
+
+ def get_settings(self):
+ _msg = self._memobj.welcome_msg
+ _oem = self._memobj.oem_info
+ _settings = self._memobj.settings
+ cfg_grp = RadioSettingGroup("cfg_grp", "Function Setup")
+ oem_grp = RadioSettingGroup("oem_grp", "OEM Info")
+
+ group = RadioSettings(cfg_grp,
+ oem_grp)
+
+ def _filter(name):
+ filtered = ""
+ for char in str(name):
+ if char in chirp_common.CHARSET_ASCII:
+ filtered += char
+ else:
+ filtered += " "
+ return filtered
+
+ #
+ # Function Setup
+ #
+
+ rs = RadioSetting("welcome_msg.line1", "Welcome Message 1",
+ RadioSettingValueString(
+ 0, 6, _filter(_msg.line1)))
+ cfg_grp.append(rs)
+
+ rs = RadioSetting("welcome_msg.line2", "Welcome Message 2",
+ RadioSettingValueString(
+ 0, 6, _filter(_msg.line2)))
+ cfg_grp.append(rs)
+
+ rs = RadioSetting("display", "Display Mode",
+ RadioSettingValueList(DISPLAY,
+ DISPLAY[_settings.display]))
+ cfg_grp.append(rs)
+
+ rs = RadioSetting("apo", "Automatic Power Off",
+ RadioSettingValueList(APO,
+ APO[_settings.apo]))
+ cfg_grp.append(rs)
+
+ #
+ # OEM info
+ #
+
+ val = RadioSettingValueString(0, 7, _filter(_oem.model))
+ val.set_mutable(False)
+ rs = RadioSetting("oem_info.model", "Model", val)
+ oem_grp.append(rs)
+
+ val = RadioSettingValueString(0, 9, _filter(_oem.date))
+ val.set_mutable(False)
+ rs = RadioSetting("oem_info.date", "Date", val)
+ oem_grp.append(rs)
+
+ val = RadioSettingValueString(0, 16, _filter(_oem.dealer))
+ val.set_mutable(False)
+ rs = RadioSetting("oem_info.dealer", "Dealer Code", val)
+ oem_grp.append(rs)
+
+ val = RadioSettingValueString(0, 9, _filter(_oem.stockdate))
+ val.set_mutable(False)
+ rs = RadioSetting("oem_info.stockdate", "Stock Date", val)
+ oem_grp.append(rs)
+
+ val = RadioSettingValueString(0, 9, _filter(_oem.selldate))
+ val.set_mutable(False)
+ rs = RadioSetting("oem_info.selldate", "Sell Date", val)
+ oem_grp.append(rs)
+
+ return group
+
+ def set_settings(self, settings):
+ _settings = self._memobj.settings
+ for element in settings:
+ if not isinstance(element, RadioSetting):
+ self.set_settings(element)
+ continue
+ else:
+ try:
+ name = element.get_name()
+ if "." in name:
+ bits = name.split(".")
+ obj = self._memobj
+ for bit in bits[:-1]:
+ if "/" in bit:
+ bit, index = bit.split("/", 1)
+ index = int(index)
+ obj = getattr(obj, bit)[index]
+ else:
+ obj = getattr(obj, bit)
+ setting = bits[-1]
+ else:
+ obj = _settings
+ setting = element.get_name()
+
+ if element.has_apply_callback():
+ LOG.debug("Using apply callback")
+ element.run_apply_callback()
+ else:
+ LOG.debug("Setting %s = %s" % (setting, element.value))
+ setattr(obj, setting, element.value)
+ except Exception, e:
+ LOG.debug(element.get_name())
+ raise
+
+ @classmethod
+ def match_model(cls, filedata, filename):
+ return cls._file_ident in filedata[0x10:0x20]
+
+ at directory.register
+class AnyToneOBLTR8RRadio(AnyToneTERMN8RRadio):
+ """AnyTone OBLTR-8R"""
+ VENDOR = "AnyTone"
+ MODEL = "OBLTR-8R"
+ _file_ident = "OBLTR8R"
More information about the chirp_devel
mailing list