# HG changeset patch # User Vinny Stipo v@xpctech.com # Date 1543738069 28800 # Sun Dec 02 00:07:49 2018 -0800 # Node ID 4bddc1caa07f1389711bf6ddf1b0a75f74c80c2f # Parent c6cab71d7d7d76b63367f5529a1b2ea075a8db1b [FT818] New Model Support Yaesu FT-818 #5607
diff --git a/chirp/drivers/ft818.py b/chirp/drivers/ft818.py new file mode 100755 --- /dev/null +++ b/chirp/drivers/ft818.py @@ -0,0 +1,1098 @@ +# +# Copyright 2012 Filippi Marco iz3gme.marco@gmail.com +# Copyright 2018 Vinny Stipo v@xpctech.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/. + +"""FT818 management module""" + +from chirp.drivers import yaesu_clone +from chirp import chirp_common, util, memmap, errors, directory, bitwise +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettingValueString, \ + RadioSettings +import time +import logging +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +CMD_ACK = 0x06 + +@directory.register +class FT818Radio(yaesu_clone.YaesuCloneModeRadio): + + """Yaesu FT-818""" + BAUD_RATE = 9600 + MODEL = "FT-818" + _model = "" + + DUPLEX = ["", "-", "+", "split"] + # narrow modes has to be at end + MODES = ["LSB", "USB", "CW", "CWR", "AM", "FM", "DIG", "PKT", "NCW", + "NCWR", "NFM"] + TMODES = ["", "Tone", "TSQL", "DTCS"] + STEPSFM = [5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0] + STEPSAM = [2.5, 5.0, 9.0, 10.0, 12.5, 25.0] + STEPSSSB = [1.0, 2.5, 5.0] + + # warning ranges has to be in this exact order + VALID_BANDS = [(100000, 33000000), (33000000, 56000000), + (76000000, 108000000), (108000000, 137000000), + (137000000, 154000000), (420000000, 470000000)] + + CHARSET = list(chirp_common.CHARSET_ASCII) + CHARSET.remove("\") + + + _memsize = 6573 + + # block 9 (130 Bytes long) is to be repeted 40 times + _block_lengths = [2, 40, 208, 208, 208, 208, 198, 53, 130, 118, 130] + + MEM_FORMAT = """ + struct mem_struct { + u8 tag_on_off:1, + tag_default:1, + unknown1:3, + mode:3; + u8 duplex:2, + is_duplex:1, + is_cwdig_narrow:1, + is_fm_narrow:1, + freq_range:3; + u8 skip:1, + unknown2:1, + ipo:1, + att:1, + unknown3:4; + u8 ssb_step:2, + am_step:3, + fm_step:3; + u8 unknown4:6, + tmode:2; + u8 unknown5:2, + tx_mode:3, + tx_freq_range:3; + u8 unknown6:1, + unknown_toneflag:1, + tone:6; + u8 unknown7:1, + dcs:7; + ul16 rit; + u32 freq; + u32 offset; + u8 name[8]; + }; + + #seekto 0x4; + struct { + u8 fst:1, + lock:1, + nb:1, + pbt:1, + unknownb:1, + dsp:1, + agc:2; + u8 vox:1, + vlt:1, + bk:1, + kyr:1, + unknown5:1, + cw_paddle:1, + pwr_meter_mode:2; + u8 vfob_band_select:4, + vfoa_band_select:4; + u8 unknowna; + u8 backlight:2, + color:2, + contrast:4; + u8 beep_freq:1, + beep_volume:7; + u8 arts_beep:2, + main_step:1, + cw_id:1, + scope:1, + pkt_rate:1, + resume_scan:2; + u8 op_filter:2, + lock_mode:2, + cw_pitch:4; + u8 sql_rf_gain:1, + ars_144:1, + ars_430:1, + cw_weight:5; + u8 cw_delay; + u8 unknown8:1, + sidetone:7; + u8 batt_chg:2, + cw_speed:6; + u8 disable_amfm_dial:1, + vox_gain:7; + u8 cat_rate:2, + emergency:1, + vox_delay:5; + u8 dig_mode:3, + mem_group:1, + unknown9:1, + apo_time:3; + u8 dcs_inv:2, + unknown10:1, + tot_time:5; + u8 mic_scan:1, + ssb_mic:7; + u8 mic_key:1, + am_mic:7; + u8 unknown11:1, + fm_mic:7; + u8 unknown12:1, + dig_mic:7; + u8 extended_menu:1, + pkt_mic:7; + u8 unknown14:1, + pkt9600_mic:7; + il16 dig_shift; + il16 dig_disp; + i8 r_lsb_car; + i8 r_usb_car; + i8 t_lsb_car; + i8 t_usb_car; + u8 unknown15:2, + menu_item:6; + u8 unknown16:4, + menu_sel:4; + u16 unknown17; + u8 art:1, + scn_mode:2, + dw:1, + pri:1, + unknown18:1, + tx_power:2; + u8 spl:1, + unknown:1, + uhf_antenna:1, + vhf_antenna:1, + air_antenna:1, + bc_antenna:1, + sixm_antenna:1, + hf_antenna:1; + } settings; + + #seekto 0x2A; + struct mem_struct vfoa[16]; + struct mem_struct vfob[16]; + struct mem_struct home[4]; + struct mem_struct qmb; + struct mem_struct mtqmb; + struct mem_struct mtune; + + #seekto 0x431; + u8 visible[25]; + u8 pmsvisible; + + #seekto 0x44B; + u8 filled[25]; + u8 pmsfilled; + + #seekto 0x465; + struct mem_struct memory[200]; + struct mem_struct pms[2]; + + #seekto 0x1903; + u8 callsign[7]; + + #seekto 0x19AD; + struct mem_struct sixtymeterchannels[5]; + """ + _CALLSIGN_CHARSET = [chr(x) for x in range(ord("0"), ord("9") + 1) + + range(ord("A"), ord("Z") + 1) + [ord(" ")]] + _CALLSIGN_CHARSET_REV = dict(zip(_CALLSIGN_CHARSET, + range(0, len(_CALLSIGN_CHARSET)))) + + # WARNING Index are hard wired in memory management code !!! + SPECIAL_MEMORIES = { + "VFOa-1.8M": -37, + "VFOa-3.5M": -36, + "VFOa-5M": -35, + "VFOa-7M": -34, + "VFOa-10M": -33, + "VFOa-14M": -32, + "VFOa-18M": -31, + "VFOa-21M": -30, + "VFOa-24M": -29, + "VFOa-28M": -28, + "VFOa-50M": -27, + "VFOa-FM": -26, + "VFOa-AIR": -25, + "VFOa-144": -24, + "VFOa-430": -23, + "VFOa-HF": -22, + "VFOb-1.8M": -21, + "VFOb-3.5M": -20, + "VFOb-5M": -19, + "VFOb-7M": -18, + "VFOb-10M": -17, + "VFOb-14M": -16, + "VFOb-18M": -15, + "VFOb-21M": -14, + "VFOb-24M": -13, + "VFOb-28M": -12, + "VFOb-50M": -11, + "VFOb-FM": -10, + "VFOb-AIR": -9, + "VFOb-144M": -8, + "VFOb-430M": -7, + "VFOb-HF": -6, + "HOME HF": -5, + "HOME 50M": -4, + "HOME 144M": -3, + "HOME 430M": -2, + "QMB": -1, + } + FIRST_VFOB_INDEX = -6 + LAST_VFOB_INDEX = -21 + FIRST_VFOA_INDEX = -22 + LAST_VFOA_INDEX = -37 + + SPECIAL_PMS = { + "PMS-L": -39, + "PMS-U": -38, + } + LAST_PMS_INDEX = -39 + + SPECIAL_MEMORIES.update(SPECIAL_PMS) + + SPECIAL_MEMORIES_REV = dict(zip(SPECIAL_MEMORIES.values(), + SPECIAL_MEMORIES.keys())) + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_download = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. <b>After clicking OK</b>, press the [A] key to send image.""")) + rp.pre_upload = _(dedent("""\ + 1. Turn radio off. + 2. Connect cable to ACC jack. + 3. Press and hold in the [MODE <] and [MODE >] keys while + turning the radio on ("CLONE MODE" will appear on the + display). + 4. Press the [C] key ("RX" will appear on the LCD).""")) + return rp + + def _read(self, block, blocknum, lastblock): + # be very patient at first block + if blocknum == 0: + attempts = 60 + else: + attempts = 5 + for _i in range(0, attempts): + data = self.pipe.read(block + 2) + if data: + break + time.sleep(0.5) + if len(data) == block + 2 and data[0] == chr(blocknum): + checksum = yaesu_clone.YaesuChecksum(1, block) + if checksum.get_existing(data) != \ + checksum.get_calculated(data): + raise Exception("Checksum Failed [%02X<>%02X] block %02X" % + (checksum.get_existing(data), + checksum.get_calculated(data), blocknum)) + # Chew away the block number and the checksum + data = data[1:block + 1] + else: + raise Exception("Unable to read block %02X expected %i got %i" + % (blocknum, block + 2, len(data))) + + LOG.debug("Read %i" % len(data)) + return data + + def _clone_in(self): + # Be very patient with the radio + self.pipe.timeout = 2 + + start = time.time() + + data = "" + blocks = 0 + status = chirp_common.Status() + status.msg = _("Cloning from radio") + nblocks = len(self._block_lengths) + 39 + status.max = nblocks + for block in self._block_lengths: + if blocks == 8: + # repeated read of 40 block same size (memory area) + repeat = 40 + else: + repeat = 1 + for _i in range(0, repeat): + data += self._read(block, blocks, blocks == nblocks - 1) + self.pipe.write(chr(CMD_ACK)) + blocks += 1 + status.cur = blocks + self.status_fn(status) + + status.msg = _("Clone completed, checking for spurious bytes") + self.status_fn(status) + moredata = self.pipe.read(2) + if moredata: + raise Exception( + _("Radio sent data after the last awaited block, " + "Please choose the correct model and try again.")) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + return memmap.MemoryMap(data) + + def _clone_out(self): + delay = 0.5 + start = time.time() + + blocks = 0 + pos = 0 + status = chirp_common.Status() + status.msg = _("Cloning to radio") + status.max = len(self._block_lengths) + 39 + for block in self._block_lengths: + if blocks == 8: + # repeated read of 40 block same size (memory area) + repeat = 40 + else: + repeat = 1 + for _i in range(0, repeat): + time.sleep(0.01) + checksum = yaesu_clone.YaesuChecksum(pos, pos + block - 1) + LOG.debug("Block %i - will send from %i to %i byte " % + (blocks, pos, pos + block)) + LOG.debug(util.hexprint(chr(blocks))) + LOG.debug(util.hexprint(self.get_mmap()[pos:pos + block])) + LOG.debug(util.hexprint(chr(checksum.get_calculated( + self.get_mmap())))) + self.pipe.write(chr(blocks)) + self.pipe.write(self.get_mmap()[pos:pos + block]) + self.pipe.write(chr(checksum.get_calculated(self.get_mmap()))) + buf = self.pipe.read(1) + if not buf or buf[0] != chr(CMD_ACK): + time.sleep(delay) + buf = self.pipe.read(1) + if not buf or buf[0] != chr(CMD_ACK): + LOG.debug(util.hexprint(buf)) + raise Exception(_("Radio did not ack block %i") % blocks) + pos += block + blocks += 1 + status.cur = blocks + self.status_fn(status) + + LOG.info("Clone completed in %i seconds" % (time.time() - start)) + + def sync_in(self): + try: + self._mmap = self._clone_in() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + try: + self._clone_out() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + + def process_mmap(self): + self._memobj = bitwise.parse(self.MEM_FORMAT, self._mmap) + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_bank = False + rf.has_dtcs_polarity = False + rf.has_nostep_tuning = True + rf.valid_modes = list(set(self.MODES)) + rf.valid_tmodes = list(self.TMODES) + rf.valid_duplexes = list(self.DUPLEX) + rf.valid_tuning_steps = list(self.STEPSFM) + rf.valid_bands = self.VALID_BANDS + rf.valid_skips = ["", "S"] + rf.valid_power_levels = [] + rf.valid_characters = "".join(self.CHARSET) + rf.valid_name_length = 8 + rf.valid_special_chans = sorted(self.SPECIAL_MEMORIES.keys()) + rf.memory_bounds = (1, 200) + rf.can_odd_split = True + rf.has_ctone = False + rf.has_settings = True + return rf + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_duplex(self, mem, _mem): + if _mem.is_duplex == 1: + mem.duplex = self.DUPLEX[_mem.duplex] + else: + mem.duplex = "" + + def _get_tmode(self, mem, _mem): + mem.tmode = self.TMODES[_mem.tmode] + mem.rtone = chirp_common.TONES[_mem.tone] + mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs] + + def _set_duplex(self, mem, _mem): + _mem.duplex = self.DUPLEX.index(mem.duplex) + _mem.is_duplex = mem.duplex != "" + + def _set_tmode(self, mem, _mem): + _mem.tmode = self.TMODES.index(mem.tmode) + # have to put this bit to 0 otherwise we get strange display in tone + # frequency (menu 83). See bug #88 and #163 + _mem.unknown_toneflag = 0 + _mem.tone = chirp_common.TONES.index(mem.rtone) + _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs) + + def get_memory(self, number): + if isinstance(number, str): + return self._get_special(number) + elif number < 0: + # I can't stop delete operation from loosing extd_number but + # I know how to get it back + return self._get_special(self.SPECIAL_MEMORIES_REV[number]) + else: + return self._get_normal(number) + + def set_memory(self, memory): + if memory.number < 0: + return self._set_special(memory) + else: + return self._set_normal(memory) + + def _get_special(self, number): + mem = chirp_common.Memory() + mem.number = self.SPECIAL_MEMORIES[number] + mem.extd_number = number + + if mem.number in range(self.FIRST_VFOA_INDEX, + self.LAST_VFOA_INDEX - 1, + -1): + _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number] + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number in range(self.FIRST_VFOB_INDEX, + self.LAST_VFOB_INDEX - 1, + -1): + _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number] + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number in range(-2, -6, -1): + _mem = self._memobj.home[5 + mem.number] + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number == -1: + _mem = self._memobj.qmb + immutable = ["number", "skip", "extd_number", + "name", "dtcs_polarity", "power", "comment"] + elif mem.number in self.SPECIAL_PMS.values(): + bitindex = -self.LAST_PMS_INDEX + mem.number + used = (self._memobj.pmsvisible >> bitindex) & 0x01 + valid = (self._memobj.pmsfilled >> bitindex) & 0x01 + if not used: + mem.empty = True + if not valid: + mem.empty = True + return mem + _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number] + immutable = ["number", "skip", "rtone", "ctone", "extd_number", + "dtcs", "tmode", "cross_mode", "dtcs_polarity", + "power", "duplex", "offset", "comment"] + else: + raise Exception("Sorry, special memory index %i " % mem.number + + "unknown you hit a bug!!") + + mem = self._get_memory(mem, _mem) + mem.immutable = immutable + + return mem + + def _set_special(self, mem): + if mem.empty and mem.number not in self.SPECIAL_PMS.values(): + # can't delete special memories! + raise Exception("Sorry, special memory can't be deleted") + + cur_mem = self._get_special(self.SPECIAL_MEMORIES_REV[mem.number]) + + # TODO add frequency range check for vfo and home memories + if mem.number in range(self.FIRST_VFOA_INDEX, + self.LAST_VFOA_INDEX - 1, + -1): + _mem = self._memobj.vfoa[-self.LAST_VFOA_INDEX + mem.number] + elif mem.number in range(self.FIRST_VFOB_INDEX, + self.LAST_VFOB_INDEX - 1, + -1): + _mem = self._memobj.vfob[-self.LAST_VFOB_INDEX + mem.number] + elif mem.number in range(-2, -6, -1): + _mem = self._memobj.home[5 + mem.number] + elif mem.number == -1: + _mem = self._memobj.qmb + elif mem.number in self.SPECIAL_PMS.values(): + # this case has to be last because 817 pms keys overlap with + # 857 derived class other special memories + bitindex = -self.LAST_PMS_INDEX + mem.number + wasused = (self._memobj.pmsvisible >> bitindex) & 0x01 + wasvalid = (self._memobj.pmsfilled >> bitindex) & 0x01 + if mem.empty: + if wasvalid and not wasused: + # pylint get confused by &= operator + self._memobj.pmsfilled = self._memobj.pmsfilled & \ + ~ (1 << bitindex) + # pylint get confused by &= operator + self._memobj.pmsvisible = self._memobj.pmsvisible & \ + ~ (1 << bitindex) + return + # pylint get confused by |= operator + self._memobj.pmsvisible = self._memobj.pmsvisible | 1 << bitindex + self._memobj.pmsfilled = self._memobj.pmsfilled | 1 << bitindex + _mem = self._memobj.pms[-self.LAST_PMS_INDEX + mem.number] + else: + raise Exception("Sorry, special memory index %i " % mem.number + + "unknown you hit a bug!!") + + for key in cur_mem.immutable: + if key != "extd_number": + if cur_mem.__dict__[key] != mem.__dict__[key]: + raise errors.RadioError("Editing field `%s' " % key + + "is not supported on this channel") + + self._set_memory(mem, _mem) + + def _get_normal(self, number): + _mem = self._memobj.memory[number - 1] + used = (self._memobj.visible[(number - 1) / 8] >> (number - 1) % 8) \ + & 0x01 + valid = (self._memobj.filled[(number - 1) / 8] >> (number - 1) % 8) \ + & 0x01 + + mem = chirp_common.Memory() + mem.number = number + if not used: + mem.empty = True + if not valid or _mem.freq == 0xffffffff: + return mem + + return self._get_memory(mem, _mem) + + def _set_normal(self, mem): + _mem = self._memobj.memory[mem.number - 1] + wasused = (self._memobj.visible[(mem.number - 1) / 8] >> + (mem.number - 1) % 8) & 0x01 + wasvalid = (self._memobj.filled[(mem.number - 1) / 8] >> + (mem.number - 1) % 8) & 0x01 + + if mem.empty: + if mem.number == 1: + # as Dan says "yaesus are not good about that :(" + # if you ulpoad an empty image you can brick your radio + raise Exception("Sorry, can't delete first memory") + if wasvalid and not wasused: + self._memobj.filled[(mem.number - 1) / 8] &= \ + ~(1 << (mem.number - 1) % 8) + _mem.set_raw("\xFF" * (_mem.size() / 8)) # clean up + self._memobj.visible[(mem.number - 1) / 8] &= \ + ~(1 << (mem.number - 1) % 8) + return + if not wasvalid: + _mem.set_raw("\x00" * (_mem.size() / 8)) # clean up + + self._memobj.visible[(mem.number - 1) / 8] |= 1 << (mem.number - 1) % 8 + self._memobj.filled[(mem.number - 1) / 8] |= 1 << (mem.number - 1) % 8 + self._set_memory(mem, _mem) + + def _get_memory(self, mem, _mem): + mem.freq = int(_mem.freq) * 10 + mem.offset = int(_mem.offset) * 10 + self._get_duplex(mem, _mem) + mem.mode = self.MODES[_mem.mode] + if mem.mode == "FM": + if _mem.is_fm_narrow == 1: + mem.mode = "NFM" + mem.tuning_step = self.STEPSFM[_mem.fm_step] + elif mem.mode == "AM": + mem.tuning_step = self.STEPSAM[_mem.am_step] + elif mem.mode == "CW" or mem.mode == "CWR": + if _mem.is_cwdig_narrow == 1: + mem.mode = "N" + mem.mode + mem.tuning_step = self.STEPSSSB[_mem.ssb_step] + else: + try: + mem.tuning_step = self.STEPSSSB[_mem.ssb_step] + except IndexError: + pass + mem.skip = _mem.skip and "S" or "" + self._get_tmode(mem, _mem) + + if _mem.tag_on_off == 1: + for i in _mem.name: + if i == 0xFF: + break + if chr(i) in self.CHARSET: + mem.name += chr(i) + else: + # radio have some graphical chars that are not supported + # we replace those with a * + LOG.info("Replacing char %x with *" % i) + mem.name += "*" + mem.name = mem.name.rstrip() + else: + mem.name = "" + + mem.extra = RadioSettingGroup("extra", "Extra") + ipo = RadioSetting("ipo", "IPO", + RadioSettingValueBoolean(bool(_mem.ipo))) + ipo.set_doc("Bypass preamp") + mem.extra.append(ipo) + + att = RadioSetting("att", "ATT", + RadioSettingValueBoolean(bool(_mem.att))) + att.set_doc("10dB front end attenuator") + mem.extra.append(att) + + return mem + + def _set_memory(self, mem, _mem): + if len(mem.name) > 0: # not supported in chirp + # so I make label visible if have one + _mem.tag_on_off = 1 + else: + _mem.tag_on_off = 0 + _mem.tag_default = 0 # never use default label "CH-nnn" + self._set_duplex(mem, _mem) + if mem.mode[0] == "N": # is it narrow? + _mem.mode = self.MODES.index(mem.mode[1:]) + # here I suppose it's safe to set both + _mem.is_fm_narrow = _mem.is_cwdig_narrow = 1 + else: + _mem.mode = self.MODES.index(mem.mode) + # here I suppose it's safe to set both + _mem.is_fm_narrow = _mem.is_cwdig_narrow = 0 + i = 0 + for lo, hi in self.VALID_BANDS: + if mem.freq > lo and mem.freq < hi: + break + i += 1 + _mem.freq_range = i + # all this should be safe also when not in split but ... + if mem.duplex == "split": + _mem.tx_mode = _mem.mode + i = 0 + for lo, hi in self.VALID_BANDS: + if mem.offset >= lo and mem.offset < hi: + break + i += 1 + _mem.tx_freq_range = i + _mem.skip = mem.skip == "S" + self._set_tmode(mem, _mem) + try: + _mem.ssb_step = self.STEPSSSB.index(mem.tuning_step) + except ValueError: + pass + try: + _mem.am_step = self.STEPSAM.index(mem.tuning_step) + except ValueError: + pass + try: + _mem.fm_step = self.STEPSFM.index(mem.tuning_step) + except ValueError: + pass + _mem.rit = 0 # not supported in chirp + _mem.freq = mem.freq / 10 + _mem.offset = mem.offset / 10 + # there are ft857D that have problems with short labels, see bug #937 + # some of the radio fill with 0xff and some with blanks + # the latter is safe for all ft8x7 radio + # so why should i do it only for some? + for i in range(0, 8): + _mem.name[i] = ord(mem.name.ljust(8)[i]) + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def validate_memory(self, mem): + msgs = yaesu_clone.YaesuCloneModeRadio.validate_memory(self, mem) + + lo, hi = self.VALID_BANDS[2] # this is fm broadcasting + if mem.freq >= lo and mem.freq <= hi: + if mem.mode != "FM": + msgs.append(chirp_common.ValidationError( + "Only FM is supported in this band")) + # TODO check that step is valid in current mode + return msgs + + @classmethod + def match_model(cls, filedata, filename): + return len(filedata) == cls._memsize + + def get_settings(self): + _settings = self._memobj.settings + basic = RadioSettingGroup("basic", "Basic") + cw = RadioSettingGroup("cw", "CW") + packet = RadioSettingGroup("packet", "Digital & packet") + panel = RadioSettingGroup("panel", "Panel settings") + extended = RadioSettingGroup("extended", "Extended") + antenna = RadioSettingGroup("antenna", "Antenna selection") + panelcontr = RadioSettingGroup("panelcontr", "Panel controls") + + top = RadioSettings(basic, cw, packet, + panelcontr, panel, extended, antenna) + + rs = RadioSetting("ars_144", "144 ARS", + RadioSettingValueBoolean(_settings.ars_144)) + basic.append(rs) + rs = RadioSetting("ars_430", "430 ARS", + RadioSettingValueBoolean(_settings.ars_430)) + basic.append(rs) + rs = RadioSetting("pkt9600_mic", "Paket 9600 mic level", + RadioSettingValueInteger(0, 100, + _settings.pkt9600_mic)) + packet.append(rs) + options = ["enable", "disable"] + rs = RadioSetting("disable_amfm_dial", "AM&FM Dial", + RadioSettingValueList(options, + options[ + _settings.disable_amfm_dial + ])) + panel.append(rs) + rs = RadioSetting("am_mic", "AM mic level", + RadioSettingValueInteger(0, 100, _settings.am_mic)) + basic.append(rs) + options = ["OFF", "1h", "2h", "3h", "4h", "5h", "6h"] + rs = RadioSetting("apo_time", "APO time", + RadioSettingValueList(options, + options[_settings.apo_time])) + basic.append(rs) + options = ["OFF", "Range", "All"] + rs = RadioSetting("arts_beep", "ARTS beep", + RadioSettingValueList(options, + options[_settings.arts_beep])) + basic.append(rs) + options = ["OFF", "ON", "Auto"] + rs = RadioSetting("backlight", "Backlight", + RadioSettingValueList(options, + options[_settings.backlight])) + panel.append(rs) + options = ["6h", "8h", "10h"] + rs = RadioSetting("batt_chg", "Battery charge", + RadioSettingValueList(options, + options[_settings.batt_chg])) + basic.append(rs) + options = ["440Hz", "880Hz"] + rs = RadioSetting("beep_freq", "Beep frequency", + RadioSettingValueList(options, + options[_settings.beep_freq])) + panel.append(rs) + rs = RadioSetting("beep_volume", "Beep volume", + RadioSettingValueInteger(0, 100, + _settings.beep_volume)) + panel.append(rs) + options = ["4800", "9600", "38400"] + rs = RadioSetting("cat_rate", "CAT rate", + RadioSettingValueList(options, + options[_settings.cat_rate])) + basic.append(rs) + options = ["Blue", "Amber", "Violet"] + rs = RadioSetting("color", "Color", + RadioSettingValueList(options, + options[_settings.color])) + panel.append(rs) + rs = RadioSetting("contrast", "Contrast", + RadioSettingValueInteger(1, 12, + _settings.contrast - 1)) + panel.append(rs) + rs = RadioSetting("cw_delay", "CW delay (*10 ms)", + RadioSettingValueInteger(1, 250, + _settings.cw_delay)) + cw.append(rs) + rs = RadioSetting("cw_id", "CW id", + RadioSettingValueBoolean(_settings.cw_id)) + cw.append(rs) + options = ["Normal", "Reverse"] + rs = RadioSetting("cw_paddle", "CW paddle", + RadioSettingValueList(options, + options[_settings.cw_paddle])) + cw.append(rs) + options = ["%i Hz" % i for i in range(300, 1001, 50)] + rs = RadioSetting("cw_pitch", "CW pitch", + RadioSettingValueList(options, + options[_settings.cw_pitch])) + cw.append(rs) + options = ["%i wpm" % i for i in range(4, 61)] + rs = RadioSetting("cw_speed", "CW speed", + RadioSettingValueList(options, + options[_settings.cw_speed])) + cw.append(rs) + options = ["1:%1.1f" % (i / 10) for i in range(25, 46, 1)] + rs = RadioSetting("cw_weight", "CW weight", + RadioSettingValueList(options, + options[_settings.cw_weight])) + cw.append(rs) + rs = RadioSetting("dig_disp", "Dig disp (*10 Hz)", + RadioSettingValueInteger(-300, 300, + _settings.dig_disp)) + packet.append(rs) + rs = RadioSetting("dig_mic", "Dig mic", + RadioSettingValueInteger(0, 100, + _settings.dig_mic)) + packet.append(rs) + options = ["RTTY", "PSK31-L", "PSK31-U", "USER-L", "USER-U"] + rs = RadioSetting("dig_mode", "Dig mode", + RadioSettingValueList(options, + options[_settings.dig_mode])) + packet.append(rs) + rs = RadioSetting("dig_shift", "Dig shift (*10 Hz)", + RadioSettingValueInteger(-300, 300, + _settings.dig_shift)) + packet.append(rs) + rs = RadioSetting("fm_mic", "FM mic", + RadioSettingValueInteger(0, 100, + _settings.fm_mic)) + basic.append(rs) + options = ["Dial", "Freq", "Panel"] + rs = RadioSetting("lock_mode", "Lock mode", + RadioSettingValueList(options, + options[_settings.lock_mode])) + panel.append(rs) + options = ["Fine", "Coarse"] + rs = RadioSetting("main_step", "Main step", + RadioSettingValueList(options, + options[_settings.main_step])) + panel.append(rs) + rs = RadioSetting("mem_group", "Mem group", + RadioSettingValueBoolean(_settings.mem_group)) + basic.append(rs) + rs = RadioSetting("mic_key", "Mic key", + RadioSettingValueBoolean(_settings.mic_key)) + cw.append(rs) + rs = RadioSetting("mic_scan", "Mic scan", + RadioSettingValueBoolean(_settings.mic_scan)) + basic.append(rs) + options = ["Off", "SSB", "CW"] + rs = RadioSetting("op_filter", "Optional filter", + RadioSettingValueList(options, + options[_settings.op_filter])) + basic.append(rs) + rs = RadioSetting("pkt_mic", "Packet mic", + RadioSettingValueInteger(0, 100, _settings.pkt_mic)) + packet.append(rs) + options = ["1200", "9600"] + rs = RadioSetting("pkt_rate", "Packet rate", + RadioSettingValueList(options, + options[_settings.pkt_rate])) + packet.append(rs) + options = ["Off", "3 sec", "5 sec", "10 sec"] + rs = RadioSetting("resume_scan", "Resume scan", + RadioSettingValueList(options, + options[_settings.resume_scan]) + ) + basic.append(rs) + options = ["Cont", "Chk"] + rs = RadioSetting("scope", "Scope", + RadioSettingValueList(options, + options[_settings.scope])) + basic.append(rs) + rs = RadioSetting("sidetone", "Sidetone", + RadioSettingValueInteger(0, 100, _settings.sidetone)) + cw.append(rs) + options = ["RF-Gain", "Squelch"] + rs = RadioSetting("sql_rf_gain", "Squelch/RF-Gain", + RadioSettingValueList(options, + options[_settings.sql_rf_gain]) + ) + panel.append(rs) + rs = RadioSetting("ssb_mic", "SSB Mic", + RadioSettingValueInteger(0, 100, _settings.ssb_mic)) + basic.append(rs) + options = ["%i" % i for i in range(0, 21)] + options[0] = "Off" + rs = RadioSetting("tot_time", "Time-out timer", + RadioSettingValueList(options, + options[_settings.tot_time])) + basic.append(rs) + rs = RadioSetting("vox_delay", "VOX delay (*100 ms)", + RadioSettingValueInteger(1, 25, _settings.vox_delay)) + basic.append(rs) + rs = RadioSetting("vox_gain", "VOX Gain", + RadioSettingValueInteger(0, 100, _settings.vox_gain)) + basic.append(rs) + rs = RadioSetting("extended_menu", "Extended menu", + RadioSettingValueBoolean(_settings.extended_menu)) + extended.append(rs) + options = ["Tn-Rn", "Tn-Riv", "Tiv-Rn", "Tiv-Riv"] + rs = RadioSetting("dcs_inv", "DCS coding", + RadioSettingValueList(options, + options[_settings.dcs_inv])) + extended.append(rs) + rs = RadioSetting("r_lsb_car", "LSB Rx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.r_lsb_car)) + extended.append(rs) + rs = RadioSetting("r_usb_car", "USB Rx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.r_usb_car)) + extended.append(rs) + rs = RadioSetting("t_lsb_car", "LSB Tx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.t_lsb_car)) + extended.append(rs) + rs = RadioSetting("t_usb_car", "USB Tx carrier point (*10 Hz)", + RadioSettingValueInteger(-30, 30, + _settings.t_usb_car)) + extended.append(rs) + + options = ["Hi", "L3", "L2", "L1"] + rs = RadioSetting("tx_power", "TX power", + RadioSettingValueList(options, + options[_settings.tx_power])) + basic.append(rs) + + options = ["Front", "Rear"] + rs = RadioSetting("hf_antenna", "HF", + RadioSettingValueList(options, + options[_settings.hf_antenna])) + antenna.append(rs) + rs = RadioSetting("sixm_antenna", "6M", + RadioSettingValueList(options, + options[_settings.sixm_antenna] + )) + antenna.append(rs) + rs = RadioSetting("bc_antenna", "Broadcasting", + RadioSettingValueList(options, + options[_settings.bc_antenna])) + antenna.append(rs) + rs = RadioSetting("air_antenna", "Air band", + RadioSettingValueList(options, + options[_settings.air_antenna]) + ) + antenna.append(rs) + rs = RadioSetting("vhf_antenna", "VHF", + RadioSettingValueList(options, + options[_settings.vhf_antenna]) + ) + antenna.append(rs) + rs = RadioSetting("uhf_antenna", "UHF", + RadioSettingValueList(options, + options[_settings.uhf_antenna]) + ) + antenna.append(rs) + + st = RadioSettingValueString(0, 7, ''.join([self._CALLSIGN_CHARSET[x] + for x in self._memobj. + callsign])) + st.set_charset(self._CALLSIGN_CHARSET) + rs = RadioSetting("callsign", "Callsign", st) + cw.append(rs) + + rs = RadioSetting("spl", "Split", + RadioSettingValueBoolean(_settings.spl)) + panelcontr.append(rs) + options = ["None", "Up", "Down"] + rs = RadioSetting("scn_mode", "Scan mode", + RadioSettingValueList(options, + options[_settings.scn_mode])) + panelcontr.append(rs) + rs = RadioSetting("pri", "Priority", + RadioSettingValueBoolean(_settings.pri)) + panelcontr.append(rs) + rs = RadioSetting("dw", "Dual watch", + RadioSettingValueBoolean(_settings.dw)) + panelcontr.append(rs) + rs = RadioSetting("art", "Auto-range transponder", + RadioSettingValueBoolean(_settings.art)) + panelcontr.append(rs) + rs = RadioSetting("nb", "Noise blanker", + RadioSettingValueBoolean(_settings.nb)) + panelcontr.append(rs) + options = ["Auto", "Fast", "Slow", "Off"] + rs = RadioSetting("agc", "AGC", + RadioSettingValueList(options, options[_settings.agc] + )) + panelcontr.append(rs) + options = ["PWR", "ALC", "SWR", "MOD"] + rs = RadioSetting("pwr_meter_mode", "Power meter mode", + RadioSettingValueList(options, + options[ + _settings.pwr_meter_mode + ])) + panelcontr.append(rs) + rs = RadioSetting("vox", "Vox", + RadioSettingValueBoolean(_settings.vox)) + panelcontr.append(rs) + rs = RadioSetting("bk", "Semi break-in", + RadioSettingValueBoolean(_settings.bk)) + cw.append(rs) + rs = RadioSetting("kyr", "Keyer", + RadioSettingValueBoolean(_settings.kyr)) + cw.append(rs) + options = ["enabled", "disabled"] + rs = RadioSetting("fst", "Fast", + RadioSettingValueList(options, options[_settings.fst] + )) + panelcontr.append(rs) + options = ["enabled", "disabled"] + rs = RadioSetting("lock", "Lock", + RadioSettingValueList(options, + options[_settings.lock])) + panelcontr.append(rs) + + return top + + def set_settings(self, settings): + _settings = self._memobj.settings + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + 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 = _settings + setting = element.get_name() + try: + LOG.debug("Setting %s(%s) <= %s" % (setting, + getattr(obj, setting), + element.value)) + except AttributeError: + LOG.debug("Setting %s <= %s" % (setting, element.value)) + if setting == "contrast": + setattr(obj, setting, int(element.value) + 1) + elif setting == "callsign": + self._memobj.callsign = \ + [self._CALLSIGN_CHARSET_REV[x] for x in + str(element.value)] + else: + setattr(obj, setting, element.value) + except: + LOG.debug(element.get_name()) + raise + + diff --git a/tests/images/Yaesu_FT-818.img b/tests/images/Yaesu_FT-818.img new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a86870c3805f17e0c98acb300906098efcb0871e GIT binary patch literal 6730 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