[chirp_devel] [PATCH] [New Model] Add Radtel Model T18

Jim Unroe
Wed Jul 5 18:29:45 PDT 2017


# HG changeset patch
# User Jim Unroe <rock.unroe at gmail.com>
# Date 1499304522 14400
# Node ID a6eea51cc96a13c63737c5ef83bb48a66b6736ea
# Parent  0b2aaa54f3082a8fe735b4e32091466c50b9900b
[New Model] Add Radtel Model T18

This patch adds support for programming the channels and global settings
of the Radtel model T18 hand held radio.

#4969

diff -r 0b2aaa54f308 -r a6eea51cc96a chirp/drivers/radtel_t18.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/radtel_t18.py	Wed Jul 05 21:28:42 2017 -0400
@@ -0,0 +1,501 @@
+# Copyright 2017 Jim Unroe <rock.unroe at gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import time
+import os
+import struct
+import unittest
+import logging
+
+from chirp import chirp_common, directory, memmap
+from chirp import bitwise, errors, util
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+    RadioSettingValueInteger, RadioSettingValueList, \
+    RadioSettingValueBoolean, RadioSettings
+
+LOG = logging.getLogger(__name__)
+
+MEM_FORMAT = """
+#seekto 0x0010;
+struct {
+    lbcd rxfreq[4];
+    lbcd txfreq[4];
+    lbcd rxtone[2];
+    lbcd txtone[2];
+    u8 unknown1:1,
+       compander:1,
+       scramble:1,
+       skip:1,
+       highpower:1,
+       narrow:1,
+       unknown2:1,
+       bcl:1;
+    u8 unknown3[3];
+} memory[16];
+#seekto 0x03C0;
+struct {
+    u8 unknown1:1,
+       scanmode:1,
+       unknown2:2,
+       voiceprompt:2,
+       batterysaver:1,
+       beep:1;
+    u8 squelchlevel;
+    u8 unused2;
+    u8 timeouttimer;
+    u8 voxlevel;
+    u8 unknown3;
+    u8 unused;
+    u8 voxdelay;
+} settings;
+"""
+
+CMD_ACK = "\x06"
+BLOCK_SIZE = 0x08
+
+VOICE_LIST = ["Off", "Chinese", "English"]
+TIMEOUTTIMER_LIST = ["Off", "30 seconds", "60 seconds", "90 seconds",
+                     "120 seconds", "150 seconds", "180 seconds",
+                     "210 seconds", "240 seconds", "270 seconds",
+                     "300 seconds"]
+SCANMODE_LIST = ["Carrier", "Time"]
+VOXLEVEL_LIST = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
+VOXDELAY_LIST = ["0.5 seconds", "1.0 seconds", "1.5 seconds",
+                 "2.0 seconds", "2.5 seconds", "3.0 seconds"]
+
+SETTING_LISTS = {
+    "voice": VOICE_LIST,
+    "timeouttimer": TIMEOUTTIMER_LIST,
+    "scanmode": SCANMODE_LIST,
+    "voxlevel": VOXLEVEL_LIST,
+    "voxdelay": VOXDELAY_LIST
+}
+
+
+def _t18_enter_programming_mode(radio):
+    serial = radio.pipe
+
+    try:
+        serial.write("\x02")
+        time.sleep(0.1)
+        serial.write("1ROGRAM")
+        ack = serial.read(1)
+    except:
+        raise errors.RadioError("Error communicating with radio")
+
+    if not ack:
+        raise errors.RadioError("No response from radio")
+    elif ack != CMD_ACK:
+        raise errors.RadioError("Radio refused to enter programming mode")
+
+    try:
+        serial.write("\x02")
+        ident = serial.read(8)
+    except:
+        raise errors.RadioError("Error communicating with radio")
+
+    if not ident.startswith("SMP558"):
+        LOG.debug(util.hexprint(ident))
+        raise errors.RadioError("Radio returned unknown identification string")
+
+    try:
+        serial.write(CMD_ACK)
+        ack = serial.read(1)
+    except:
+        raise errors.RadioError("Error communicating with radio")
+
+    if ack != CMD_ACK:
+        raise errors.RadioError("Radio refused to enter programming mode")
+
+    try:
+        serial.write("\x05")
+        response = serial.read(6)
+    except:
+        raise errors.RadioError("Error communicating with radio")
+
+    if not response == ("\xFF" * 6):
+        LOG.debug(util.hexprint(response))
+        raise errors.RadioError("Radio returned unexpected response")
+
+    try:
+        serial.write(CMD_ACK)
+        ack = serial.read(1)
+    except:
+        raise errors.RadioError("Error communicating with radio")
+
+    if ack != CMD_ACK:
+        raise errors.RadioError("Radio refused to enter programming mode")
+
+
+def _t18_exit_programming_mode(radio):
+    serial = radio.pipe
+    try:
+        serial.write("b")
+    except:
+        raise errors.RadioError("Radio refused to exit programming mode")
+
+
+def _t18_read_block(radio, block_addr, block_size):
+    serial = radio.pipe
+
+    cmd = struct.pack(">cHb", 'R', block_addr, BLOCK_SIZE)
+    expectedresponse = "W" + cmd[1:]
+    LOG.debug("Reading block %04x..." % (block_addr))
+
+    try:
+        serial.write(cmd)
+        response = serial.read(4 + BLOCK_SIZE)
+        if response[:4] != expectedresponse:
+            raise Exception("Error reading block %04x." % (block_addr))
+
+        block_data = response[4:]
+
+        serial.write(CMD_ACK)
+        ack = serial.read(1)
+    except:
+        raise errors.RadioError("Failed to read block at %04x" % block_addr)
+
+    if ack != CMD_ACK:
+        raise Exception("No ACK reading block %04x." % (block_addr))
+
+    return block_data
+
+
+def _t18_write_block(radio, block_addr, block_size):
+    serial = radio.pipe
+
+    cmd = struct.pack(">cHb", 'W', block_addr, BLOCK_SIZE)
+    data = radio.get_mmap()[block_addr:block_addr + 8]
+
+    LOG.debug("Writing Data:")
+    LOG.debug(util.hexprint(cmd + data))
+
+    try:
+        serial.write(cmd + data)
+        if serial.read(1) != CMD_ACK:
+            raise Exception("No ACK")
+    except:
+        raise errors.RadioError("Failed to send block "
+                                "to radio at %04x" % block_addr)
+
+
+def do_download(radio):
+    LOG.debug("download")
+    _t18_enter_programming_mode(radio)
+
+    data = ""
+
+    status = chirp_common.Status()
+    status.msg = "Cloning from radio"
+
+    status.cur = 0
+    status.max = radio._memsize
+
+    for addr in range(0, radio._memsize, BLOCK_SIZE):
+        status.cur = addr + BLOCK_SIZE
+        radio.status_fn(status)
+
+        block = _t18_read_block(radio, addr, BLOCK_SIZE)
+        data += block
+
+        LOG.debug("Address: %04x" % addr)
+        LOG.debug(util.hexprint(block))
+
+    _t18_exit_programming_mode(radio)
+
+    return memmap.MemoryMap(data)
+
+
+def do_upload(radio):
+    status = chirp_common.Status()
+    status.msg = "Uploading to radio"
+
+    _t18_enter_programming_mode(radio)
+
+    status.cur = 0
+    status.max = radio._memsize
+
+    for start_addr, end_addr in radio._ranges:
+        for addr in range(start_addr, end_addr, BLOCK_SIZE):
+            status.cur = addr + BLOCK_SIZE
+            radio.status_fn(status)
+            _t18_write_block(radio, addr, BLOCK_SIZE)
+
+    _t18_exit_programming_mode(radio)
+
+
+def model_match(cls, data):
+    """Match the opened/downloaded image to the correct version"""
+
+    if len(data) == cls._memsize:
+        rid = data[0x03D0:0x03D8]
+        return "P558" in rid
+    else:
+        return False
+
+
+ at directory.register
+class T18Radio(chirp_common.CloneModeRadio):
+    """radtel T18"""
+    VENDOR = "Radtel"
+    MODEL = "T18"
+    BAUD_RATE = 9600
+
+    _ranges = [
+        (0x0000, 0x03F0),
+    ]
+    _memsize = 0x03F0
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_settings = True
+        rf.valid_modes = ["NFM", "FM"]  # 12.5 KHz, 25 kHz.
+        rf.valid_skips = ["", "S"]
+        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
+        rf.valid_duplexes = ["", "-", "+", "split", "off"]
+        rf.can_odd_split = True
+        rf.has_rx_dtcs = True
+        rf.has_ctone = True
+        rf.has_cross = True
+        rf.valid_cross_modes = [
+            "Tone->Tone",
+            "DTCS->",
+            "->DTCS",
+            "Tone->DTCS",
+            "DTCS->Tone",
+            "->Tone",
+            "DTCS->DTCS"]
+        rf.has_tuning_step = False
+        rf.has_bank = False
+        rf.has_name = False
+        rf.memory_bounds = (1, 16)
+        rf.valid_bands = [(400000000, 470000000)]
+
+        return rf
+
+    def process_mmap(self):
+        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
+
+    def sync_in(self):
+        self._mmap = do_download(self)
+        self.process_mmap()
+
+    def sync_out(self):
+        do_upload(self)
+
+    def get_raw_memory(self, number):
+        return repr(self._memobj.memory[number - 1])
+
+    def _decode_tone(self, val):
+        val = int(val)
+        if val == 16665:
+            return '', None, None
+        elif val >= 12000:
+            return 'DTCS', val - 12000, 'R'
+        elif val >= 8000:
+            return 'DTCS', val - 8000, 'N'
+        else:
+            return 'Tone', val / 10.0, None
+
+    def _encode_tone(self, memval, mode, value, pol):
+        if mode == '':
+            memval[0].set_raw(0xFF)
+            memval[1].set_raw(0xFF)
+        elif mode == 'Tone':
+            memval.set_value(int(value * 10))
+        elif mode == 'DTCS':
+            flag = 0x80 if pol == 'N' else 0xC0
+            memval.set_value(value)
+            memval[1].set_bits(flag)
+        else:
+            raise Exception("Internal error: invalid mode `%s'" % mode)
+
+    def get_memory(self, number):
+        _mem = self._memobj.memory[number - 1]
+
+        mem = chirp_common.Memory()
+
+        mem.number = number
+        mem.freq = int(_mem.rxfreq) * 10
+
+        # We'll consider any blank (i.e. 0MHz frequency) to be empty
+        if mem.freq == 0:
+            mem.empty = True
+            return mem
+
+        if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF":
+            mem.freq = 0
+            mem.empty = True
+            return mem
+
+        if _mem.txfreq.get_raw() == "\xFF\xFF\xFF\xFF":
+            mem.duplex = "off"
+            mem.offset = 0
+        elif int(_mem.rxfreq) == int(_mem.txfreq):
+            mem.duplex = ""
+            mem.offset = 0
+        else:
+            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) and "-" or "+"
+            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
+
+        mem.mode = not _mem.narrow and "FM" or "NFM"
+
+        mem.skip = _mem.skip and "S" or ""
+
+        txtone = self._decode_tone(_mem.txtone)
+        rxtone = self._decode_tone(_mem.rxtone)
+        chirp_common.split_tone_decode(mem, txtone, rxtone)
+
+        mem.extra = RadioSettingGroup("Extra", "extra")
+        rs = RadioSetting("bcl", "Busy Channel Lockout",
+                          RadioSettingValueBoolean(not _mem.bcl))
+        mem.extra.append(rs)
+        rs = RadioSetting("scramble", "Scramble",
+                          RadioSettingValueBoolean(not _mem.scramble))
+        mem.extra.append(rs)
+        rs = RadioSetting("compander", "Compander",
+                          RadioSettingValueBoolean(not _mem.compander))
+        mem.extra.append(rs)
+
+        return mem
+
+    def set_memory(self, mem):
+        # Get a low-level memory object mapped to the image
+        _mem = self._memobj.memory[mem.number - 1]
+
+        if mem.empty:
+            _mem.set_raw("\xFF" * (_mem.size() / 8))
+            return
+
+        _mem.rxfreq = mem.freq / 10
+
+        if mem.duplex == "off":
+            for i in range(0, 4):
+                _mem.txfreq[i].set_raw("\xFF")
+        elif mem.duplex == "split":
+            _mem.txfreq = mem.offset / 10
+        elif mem.duplex == "+":
+            _mem.txfreq = (mem.freq + mem.offset) / 10
+        elif mem.duplex == "-":
+            _mem.txfreq = (mem.freq - mem.offset) / 10
+        else:
+            _mem.txfreq = mem.freq / 10
+
+        txtone, rxtone = chirp_common.split_tone_encode(mem)
+        self._encode_tone(_mem.txtone, *txtone)
+        self._encode_tone(_mem.rxtone, *rxtone)
+
+        _mem.narrow = 'N' in mem.mode
+        _mem.skip = mem.skip == "S"
+
+        for setting in mem.extra:
+            # NOTE: Only three settings right now, all are inverted
+            setattr(_mem, setting.get_name(), not int(setting.value))
+
+    def get_settings(self):
+        _settings = self._memobj.settings
+        basic = RadioSettingGroup("basic", "Basic Settings")
+        top = RadioSettings(basic)
+
+        rs = RadioSetting("squelchlevel", "Squelch level",
+                          RadioSettingValueInteger(
+                              0, 9, _settings.squelchlevel))
+        basic.append(rs)
+
+        rs = RadioSetting("timeouttimer", "Timeout timer",
+                          RadioSettingValueList(
+                              TIMEOUTTIMER_LIST,
+                              TIMEOUTTIMER_LIST[
+                                  _settings.timeouttimer]))
+        basic.append(rs)
+
+        rs = RadioSetting("scanmode", "Scan mode",
+                          RadioSettingValueList(
+                              SCANMODE_LIST,
+                              SCANMODE_LIST[_settings.scanmode]))
+        basic.append(rs)
+
+        rs = RadioSetting("voiceprompt", "Voice prompt",
+                          RadioSettingValueList(
+                              VOICE_LIST,
+                              VOICE_LIST[_settings.voiceprompt]))
+        basic.append(rs)
+
+        rs = RadioSetting("voxlevel", "Vox level",
+                          RadioSettingValueList(
+                              VOXLEVEL_LIST,
+                              VOXLEVEL_LIST[_settings.voxlevel]))
+        basic.append(rs)
+
+        rs = RadioSetting("voxdelay", "VOX delay",
+                          RadioSettingValueList(
+                              VOXDELAY_LIST,
+                              VOXDELAY_LIST[_settings.voxdelay]))
+        basic.append(rs)
+
+        rs = RadioSetting("batterysaver", "Battery saver",
+                          RadioSettingValueBoolean(_settings.batterysaver))
+        basic.append(rs)
+
+        rs = RadioSetting("beep", "Beep",
+                          RadioSettingValueBoolean(_settings.beep))
+        basic.append(rs)
+
+        return top
+
+    def set_settings(self, settings):
+        for element in settings:
+            if not isinstance(element, RadioSetting):
+                self.set_settings(element)
+                continue
+            else:
+                try:
+                    if "." in element.get_name():
+                        bits = element.get_name().split(".")
+                        obj = self._memobj
+                        for bit in bits[:-1]:
+                            obj = getattr(obj, bit)
+                        setting = bits[-1]
+                    else:
+                        obj = self._memobj.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):
+        match_size = False
+        match_model = False
+
+        # testing the file data size
+        if len(filedata) == cls._memsize:
+            match_size = True
+
+        # testing the model fingerprint
+        match_model = model_match(cls, filedata)
+
+        if match_size and match_model:
+            return True
+        else:
+            return False



More information about the chirp_devel mailing list