[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