[chirp_devel] [PATCH] [FTM-3200D] Add support for Yaesu FTM-3200D

Wade Simmons
Wed May 24 08:25:47 PDT 2017


# HG changeset patch
# User Wade Simmons <wade at wades.im>
# Date 1495638955 14400
#      Wed May 24 11:15:55 2017 -0400
# Node ID 8745d861ed0d38ecf45017a6ff1ba2f57ac5590d
# Parent  93e58a8a1a2ac3e50b255cca410821f756bac4a9
[FTM-3200D] Add support for Yaesu FTM-3200D

This patch adds basic CHIRP support for programming the FTM-3200D. This radio
shares most of its memory layout with the FT1D, so it has been created as a
subclass of that radio.

#4279

diff -r 93e58a8a1a2a -r 8745d861ed0d chirp/drivers/ft1d.py
--- a/chirp/drivers/ft1d.py	Wed May 17 16:54:45 2017 -0400
+++ b/chirp/drivers/ft1d.py	Wed May 24 11:15:55 2017 -0400
@@ -27,7 +27,7 @@
 
 LOG = logging.getLogger(__name__)
 
-MEM_FORMAT = """
+MEM_SETTINGS_FORMAT = """
 #seekto 0x049a;
 struct {
   u8 vfo_a;
@@ -141,17 +141,21 @@
   u8 unknown[2];
   u8 name[16];
 } bank_info[24];
+"""
 
+MEM_FORMAT = """
 #seekto 0x2D4A;
 struct {
-  u8 unknown1;
+  u8 unknown0:2,
+     mode_alt:1,  // mode for FTM-3200D
+     unknown1:5;
   u8 mode:2,
      duplex:2,
      tune_step:4;
   bbcd freq[3];
   u8 power:2,
-     unknown2:4,
-     tone_mode:2;
+     unknown2:2,
+     tone_mode:4;
   u8 charsetbits[2];
   char label[16];
   bbcd offset[3];
@@ -160,7 +164,7 @@
   u8 unknown6:1,
      dcs:7;
   u8 unknown7[3];
-} memory[900];
+} memory[%d];
 
 #seekto 0x280A;
 struct {
@@ -170,8 +174,10 @@
      skip:1,
      used:1,
      valid:1;
-} flag[900];
+} flag[%d];
+"""
 
+MEM_APRS_FORMAT = """
 #seekto 0xbeca;
 struct {
   u8 rx_baud;
@@ -334,7 +340,9 @@
   char path_and_body[66];
   u8 unknown[70];
 } aprs_message_pkt[60];
+"""
 
+MEM_CHECKSUM_FORMAT = """
 #seekto 0x1FDC9;
 u8 checksum;
 """
@@ -500,11 +508,7 @@
         return banks
 
 
-def _wipe_memory(mem):
-    mem.set_raw("\x00" * (mem.size() / 8))
-    mem.unknown1 = 0x05
-
-
+# Note: other radios like FTM3200Radio subclass this radio
 @directory.register
 class FT1Radio(yaesu_clone.YaesuCloneModeRadio):
     """Yaesu FT1DR"""
@@ -517,7 +521,9 @@
     _memsize = 130507
     _block_lengths = [10, 130497]
     _block_size = 32
-    _mem_params = (0xFECA,         # APRS beacon metadata address.
+    _mem_params = (900,            # size of memories array
+                   900,            # size of flags array
+                   0xFECA,         # APRS beacon metadata address.
                    60,             # Number of beacons stored.
                    0x1064A,        # APRS beacon content address.
                    134,            # Length of beacon data stored.
@@ -610,7 +616,9 @@
         return rp
 
     def process_mmap(self):
-        self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
+        mem_format = MEM_SETTINGS_FORMAT + MEM_FORMAT + MEM_APRS_FORMAT + \
+                MEM_CHECKSUM_FORMAT
+        self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
 
     def get_features(self):
         rf = chirp_common.RadioFeatures()
@@ -632,7 +640,8 @@
         return rf
 
     def get_raw_memory(self, number):
-        return repr(self._memobj.memory[number])
+        return "\n".join([repr(self._memobj.memory[number - 1]),
+                          repr(self._memobj.flag[number - 1])])
 
     def _checksums(self):
         return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
@@ -666,21 +675,53 @@
         mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
         mem.offset = int(_mem.offset) * 1000
         mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
-        mem.tmode = TMODES[_mem.tone_mode]
+        self._get_tmode(mem, _mem)
         mem.duplex = DUPLEX[_mem.duplex]
         if mem.duplex == "split":
             mem.offset = chirp_common.fix_rounded_step(mem.offset)
-        mem.mode = MODES[_mem.mode]
+        mem.mode = self._decode_mode(_mem)
         mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
         mem.tuning_step = STEPS[_mem.tune_step]
-        mem.power = POWER_LEVELS[3 - _mem.power]
+        mem.power = self._decode_power_level(_mem)
         mem.skip = flag.pskip and "P" or flag.skip and "S" or ""
 
-        charset = ''.join(CHARSET).ljust(256, '.')
-        mem.name = str(_mem.label).rstrip("\xFF").translate(charset)
+        mem.name = self._decode_label(_mem)
 
         return mem
 
+    def _decode_label(self, mem):
+        charset = ''.join(CHARSET).ljust(256, '.')
+        return str(mem.label).rstrip("\xFF").translate(charset)
+
+    def _encode_label(self, mem):
+        label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()])
+        return self._add_ff_pad(label, 16)
+
+    def _encode_charsetbits(self, mem):
+        # We only speak english here in chirpville
+        return [0x00, 0x00]
+
+    def _decode_power_level(self, mem):
+        return POWER_LEVELS[3 - mem.power]
+
+    def _encode_power_level(self, mem):
+        return 3 - POWER_LEVELS.index(mem.power)
+
+    def _decode_mode(self, mem):
+        return MODES[mem.mode]
+
+    def _encode_mode(self, mem):
+        return MODES.index(mem.mode)
+
+    def _get_tmode(self, mem, _mem):
+        mem.tmode = TMODES[_mem.tone_mode]
+
+    def _set_tmode(self, _mem, mem):
+        _mem.tone_mode = TMODES.index(mem.tmode)
+
+    def _set_mode(self, _mem, mem):
+        _mem.mode = self._encode_mode(mem)
+
     def _debank(self, mem):
         bm = self.get_bank_model()
         for bank in bm.get_memory_mappings(mem):
@@ -693,7 +734,7 @@
         self._debank(mem)
 
         if not mem.empty and not flag.valid:
-            _wipe_memory(_mem)
+            self._wipe_memory(_mem)
 
         if mem.empty and flag.valid and not flag.used:
             flag.valid = False
@@ -714,25 +755,28 @@
         _mem.freq = int(mem.freq / 1000)
         _mem.offset = int(mem.offset / 1000)
         _mem.tone = chirp_common.TONES.index(mem.rtone)
-        _mem.tone_mode = TMODES.index(mem.tmode)
+        self._set_tmode(_mem, mem)
         _mem.duplex = DUPLEX.index(mem.duplex)
-        _mem.mode = MODES.index(mem.mode)
+        self._set_mode(_mem, mem)
         _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
         _mem.tune_step = STEPS.index(mem.tuning_step)
         if mem.power:
-            _mem.power = 3 - POWER_LEVELS.index(mem.power)
+            _mem.power = self._encode_power_level(mem)
         else:
             _mem.power = 0
 
-        label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()])
-        _mem.label = self._add_ff_pad(label, 16)
-        # We only speak english here in chirpville
-        _mem.charsetbits[0] = 0x00
-        _mem.charsetbits[1] = 0x00
+        _mem.label = self._encode_label(mem)
+        charsetbits = self._encode_charsetbits(mem)
+        _mem.charsetbits[0], _mem.charsetbits[1] = charsetbits
 
         flag.skip = mem.skip == "S"
         flag.pskip = mem.skip == "P"
 
+    @classmethod
+    def _wipe_memory(cls, mem):
+        mem.set_raw("\x00" * (mem.size() / 8))
+        mem.unknown1 = 0x05
+
     def get_bank_model(self):
         return FT1BankModel(self)
 
diff -r 93e58a8a1a2a -r 8745d861ed0d chirp/drivers/ftm3200d.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/drivers/ftm3200d.py	Wed May 24 11:15:55 2017 -0400
@@ -0,0 +1,201 @@
+# Copyright 2010 Dan Smith <dsmith at danplanet.com>
+# Copyright 2017 Wade Simmons <wade at wades.im>
+#
+# 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 logging
+from textwrap import dedent
+
+from chirp.drivers import yaesu_clone, ft1d
+from chirp import chirp_common, directory, bitwise
+from chirp.settings import RadioSettings
+
+LOG = logging.getLogger(__name__)
+
+POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=5),
+                chirp_common.PowerLevel("Mid", watts=30),
+                chirp_common.PowerLevel("Hi", watts=65)]
+
+TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", None, None, "Pager", "Cross"]
+CROSS_MODES = [None, "DTCS->", "Tone->DTCS", "DTCS->Tone"]
+
+MODES = ["FM", "NFM"]
+STEPS = [0, 5, 6.25, 10, 12.5, 15, 20, 25, 50, 100]  # 0 = auto
+RFSQUELCH = ["OFF", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"]
+
+# Charset is subset of ASCII + some unknown chars \x80-\x86
+VALID_CHARS = ["%i" % int(x) for x in range(0, 10)] + \
+    list(":>=<?@") + \
+    [chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
+    list("[\\]_") + \
+    [chr(x) for x in range(ord("a"), ord("z") + 1)] + \
+    list("%*+,-/=$ ")
+
+MEM_FORMAT = """
+#seekto 0xceca;
+struct {
+  u8 unknown5;
+  u8 unknown3;
+  u8 unknown4:6,
+     dsqtype:2;
+  u8 dsqcode;
+  u8 unknown1[2];
+  char mycall[10];
+  u8 unknown2[368];
+} settings;
+
+#seekto 0xfec9;
+u8 checksum;
+"""
+
+
+ at directory.register
+class FTM3200Radio(ft1d.FT1Radio):
+    """Yaesu FTM-3200D"""
+    BAUD_RATE = 38400
+    VENDOR = "Yaesu"
+    MODEL = "FTM-3200D"
+    VARIANT = "R"
+
+    _model = "AH52N"
+    _memsize = 65227
+    _block_lengths = [10, 65217]
+    _has_vibrate = False
+    _has_af_dual = False
+
+    _mem_params = (199,            # size of memories array
+                   199)            # size of flags array
+
+    @classmethod
+    def get_prompts(cls):
+        rp = chirp_common.RadioPrompts()
+        rp.pre_download = _(dedent("""\
+            1. Turn radio off.
+            2. Connect cable to DATA terminal.
+            3. Press and hold in the [MHz(SETUP)] key while turning the radio
+                 on ("CLONE" will appear on the display).
+            4. <b>After clicking OK</b>, press the [REV(DW)] key
+                 to send image."""))
+        rp.pre_upload = _(dedent("""\
+            1. Turn radio off.
+            2. Connect cable to DATA terminal.
+            3. Press and hold in the [MHz(SETUP)] key while turning the radio
+                 on ("CLONE" will appear on the display).
+            4. Press the [MHz(SETUP)] key
+                 ("-WAIT-" will appear on the LCD)."""))
+        return rp
+
+    def process_mmap(self):
+        mem_format = ft1d.MEM_FORMAT + MEM_FORMAT
+        self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
+
+    def get_features(self):
+        rf = chirp_common.RadioFeatures()
+        rf.has_dtcs_polarity = False
+        rf.valid_modes = list(MODES)
+        rf.valid_tmodes = [x for x in TMODES if x is not None]
+        rf.valid_cross_modes = [x for x in CROSS_MODES if x is not None]
+        rf.valid_duplexes = list(ft1d.DUPLEX)
+        rf.valid_tuning_steps = list(STEPS)
+        rf.valid_bands = [(136000000, 174000000)]
+        # rf.valid_skips = SKIPS
+        rf.valid_power_levels = POWER_LEVELS
+        rf.valid_characters = "".join(VALID_CHARS)
+        rf.valid_name_length = 8
+        rf.memory_bounds = (1, 199)
+        rf.can_odd_split = True
+        rf.has_ctone = False
+        rf.has_bank = False
+        rf.has_bank_names = False
+        # disable until implemented
+        rf.has_settings = False
+        return rf
+
+    def _decode_label(self, mem):
+        # TODO preserve the unknown \x80-x86 chars?
+        return str(mem.label).rstrip("\xFF").decode('ascii', 'replace')
+
+    def _encode_label(self, mem):
+        label = mem.name.rstrip().encode('ascii', 'ignore')
+        return self._add_ff_pad(label, 16)
+
+    def _encode_charsetbits(self, mem):
+        # TODO this is a setting to decide if the memory should be displayed
+        # as a name or frequency. Should we expose this setting to the user
+        # instead of autoselecting it (and losing their preference)?
+        if mem.name.rstrip() == '':
+            return [0x00, 0x00]
+        return [0x00, 0x80]
+
+    def _decode_power_level(self, mem):
+        return POWER_LEVELS[mem.power - 1]
+
+    def _encode_power_level(self, mem):
+        return POWER_LEVELS.index(mem.power) + 1
+
+    def _decode_mode(self, mem):
+        return MODES[mem.mode_alt]
+
+    def _encode_mode(self, mem):
+        return MODES.index(mem.mode)
+
+    def _get_tmode(self, mem, _mem):
+        if _mem.tone_mode > 8:
+            tmode = "Cross"
+            mem.cross_mode = CROSS_MODES[_mem.tone_mode - 8]
+        else:
+            tmode = TMODES[_mem.tone_mode]
+
+        if tmode == "Pager":
+            # TODO chirp_common does not allow 'Pager'
+            #   Expose as a different setting?
+            mem.tmode = ""
+        else:
+            mem.tmode = tmode
+
+    def _set_tmode(self, _mem, mem):
+        if mem.tmode == "Cross":
+            _mem.tone_mode = 8 + CROSS_MODES.index(mem.cross_mode)
+        else:
+            _mem.tone_mode = TMODES.index(mem.tmode)
+
+    def _set_mode(self, _mem, mem):
+        _mem.mode_alt = self._encode_mode(mem)
+
+    def get_bank_model(self):
+        return None
+
+    def _debank(self, mem):
+        return
+
+    def _checksums(self):
+        return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
+                yaesu_clone.YaesuChecksum(0x06CA, 0x0748),
+                yaesu_clone.YaesuChecksum(0x074A, 0x07C8),
+                yaesu_clone.YaesuChecksum(0x07CA, 0x0848),
+                yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)]
+
+    def _get_settings(self):
+        # TODO
+        top = RadioSettings()
+        return top
+
+    @classmethod
+    def _wipe_memory(cls, mem):
+        mem.set_raw("\x00" * (mem.size() / 8))
+
+    def sync_out(self):
+        # Need to give enough time for the radio to ACK after writes
+        self.pipe.timeout = 1
+        return super(FTM3200Radio, self).sync_out()



More information about the chirp_devel mailing list