# HG changeset patch # User Alexey K lepik.stv@gmail.com # Date 1444251379 -10800 # Wed Oct 07 23:56:19 2015 +0300 # Node ID 6213e68e50e323221bf1939101bfc4169621a8cd # Parent 39ec1a8bc0b04f90f47e8e76795a7abbe5b1af40 [KYD IP-620] Add support for KYD IP-620 radio - issue #2033
diff -r 39ec1a8bc0b0 -r 6213e68e50e3 chirp/drivers/kyd_IP620.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/drivers/kyd_IP620.py Wed Oct 07 23:56:19 2015 +0300 @@ -0,0 +1,619 @@ +# Copyright 2015 Lepik.stv lepik.stv@gmail.com +# based on modification of Dan Smith's and Jim Unroe original work +# +# 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/. + +"""KYD IP-620 radios management module""" + +# TODO: Power on message +# TODO: Channel name +# TODO: Tuning step + +import struct +import time +import os +import logging +from chirp import util, chirp_common, bitwise, memmap, errors, directory +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueInteger, RadioSettingValueString, \ + RadioSettings + +LOG = logging.getLogger(__name__) + +IP620_MEM_FORMAT = """ +#seekto 0x0000; +struct { // Channel memory structure + lbcd rx_freq[4]; // RX frequency + lbcd tx_freq[4]; // TX frequency + ul16 rx_tone; // RX tone + ul16 tx_tone; // TX tone + u8 unknown_1:4 // n-a + busy_loc:2, // NO-00, Crrier wave-01, SM-10 + n_a:2; // n-a + u8 unknown_2:1 // n-a + scan_add:1, // Scan add + n_a:1, // n-a + w_n:1, // Narrow-0 Wide-1 + lout:1, // LOCKOUT OFF-0 ON-1 + n_a_:1, // n-a + power:2; // Power low-00 middle-01 high-10 + u8 unknown_3; // n-a + u8 unknown_4; // n-a +} memory[200]; + +#seekto 0x1000; +struct { + u8 chan_name[6]; //Channel name + u8 unknown_1[10]; +} chan_names[200]; + +#seekto 0x0C80; +struct { // Settings memory structure ( A-Frequency mode ) + lbcd freq_a_rx[4]; + lbcd freq_a_tx[4]; + ul16 freq_a_rx_tone; // RX tone + ul16 freq_a_tx_tone; // TX tone + u8 unknown_1_5:4 + freq_a_busy_loc:2, + n_a:2; + u8 unknown_1_6:3 + freq_a_w_n:1, + n_a:1, + na:1, + freq_a_power:2; + u8 unknown_1_7; + u8 unknown_1_8; +} settings_freq_a; + +#seekto 0x0E20; +struct { + u8 chan_disp_way; // Channel display way + u8 step_freq; // Step frequency KHz + u8 rf_sql; // Squelch level + u8 bat_save; // Battery Saver + u8 chan_pri; // Channel PRI + u8 end_beep; // End beep + u8 tot; // Time-out timer + u8 vox; // VOX Gain + u8 chan_pri_num; // Channel PRI time Sec + u8 n_a_2; + u8 ch_mode; // CH mode + u8 n_a_3; + u8 call_tone; // Call tone + u8 beep; // Beep + u8 unknown_1_1[2]; + u8 unknown_1_2[8]; + u8 scan_rev; // Scan rev + u8 unknown_1_3[2]; + u8 enc; // Frequency lock + u8 vox_dly; // VOX Delay + u8 wait_back_light;// Wait back light + u8 unknown_1_4[2]; +} settings; + +#seekto 0x0E40; +struct { + u8 fm_radio; // FM radio + u8 auto_lock; // Auto lock + u8 unknown_1[8]; + u8 pon_msg[6]; //Power on msg +} settings_misc; + +#seekto 0x1C80; +struct { + u8 unknown_1[16]; + u8 unknown_2[16]; +} settings_radio_3; +""" + +CMD_ACK = "\x06" +WRITE_BLOCK_SIZE = 0x10 +READ_BLOCK_SIZE = 0x40 + +CHAR_LENGTH_MAX = 6 + +OFF_ON_LIST = ["OFF", "ON"] +ON_OFF_LIST = ["ON", "OFF"] +NO_YES_LIST = ["NO", "YES"] +STEP_LIST = ["5.0", "6.25", "10.0", "12.5", "25.0"] +BAT_SAVE_LIST = ["OFF", "0.2 Sec", "0.4 Sec", "0.6 Sec", "0.8 Sec","1.0 Sec"] +SHIFT_LIST = ["", "-", "+"] +SCANM_LIST = ["Time", "Carrier wave", "Search"] +ENDBEEP_LIST = ["OFF", "Begin", "End", "Begin/End"] +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.00), chirp_common.PowerLevel("Medium", watts=2.50), chirp_common.PowerLevel("High", watts=5.00)] +TIMEOUT_LIST = ["OFF", "1 Min", "3 Min", "10 Min"] +TOTALERT_LIST = ["", "OFF"] + ["%s seconds" % x for x in range(1, 11)] +VOX_LIST = ["OFF"] + ["%s" % x for x in range(1, 17)] +VOXDELAY_LIST = ["0.3 Sec", "0.5 Sec", "1.0 Sec", "1.5 Sec", "2.0 Sec", "3.0 Sec", "4.0 Sec", "5.0 Sec"] +PRI_NUM = [3, 5, 8, 10] +PRI_NUM_LIST = [str(x) for x in PRI_NUM] +CH_FLAG_LIST = ["Channel+Freq", "Channel+Name"] +BACKLIGHT_LIST = ["Always Off", "Auto", "Always On"] +BUSYLOCK_LIST = ["NO", "Carrier", "SM"] +KEYBLOCK_LIST = ["Manual", "Auto"] +CALLTONE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8", "1750"] +RFSQL_LIST = ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6","S-7", "S-8", "S-FULL"] + +IP620_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ?+-* " + +IP620_BANDS = [ + (136000000, 174000000), + (200000000, 260000000), + (300000000, 340000000), # <--- this band supports only Russian model (ARGUT A-36) + (350000000, 390000000), + (400000000, 480000000), + (420000000, 510000000), + (450000000, 520000000), +] + +@directory.register +class IP620Radio(chirp_common.CloneModeRadio, + chirp_common.ExperimentalRadio): + """KYD IP-620""" + VENDOR = "KYD" + MODEL = "IP-620" + BAUD_RATE = 9600 + + _ranges = [ + (0x0000, 0x2000), + ] + _memsize = 0x2000 + + def _ip620_exit_programming_mode(self): + try: + self.pipe.write("\x06") + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Radio refused to exit programming mode: %s" % e) + + def _ip620_enter_programming_mode(self): + try: + self.pipe.write("iUHOUXUN") + self.pipe.write("\x02") + time.sleep(0.2) + _ack = self.pipe.read(1) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error communicating with radio: %s" % e) + if not _ack: + raise errors.RadioError("No response from radio") + elif _ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + try: + self.pipe.write("\x02") + _ident = self.pipe.read(8) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error communicating with radio: %s" % e) + if not _ident.startswith("\x06\x4B\x47\x36\x37\x01\x56\xF8"): + print util.hexprint(_ident) + raise errors.RadioError("Radio returned unknown identification string") + try: + self.pipe.write(CMD_ACK) + _ack = self.pipe.read(1) + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Error communicating with radio: %s" % e) + if _ack != CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + def _ip620_write_block(self, block_addr): + _cmd = struct.pack(">cHb", 'W', block_addr, WRITE_BLOCK_SIZE) + _data = self.get_mmap()[block_addr:block_addr + WRITE_BLOCK_SIZE] + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(_cmd + _data)) + try: + self.pipe.write(_cmd + _data) + if self.pipe.read(1) != CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + + def _ip620_read_block(self, block_addr): + _cmd = struct.pack(">cHb", 'R', block_addr, READ_BLOCK_SIZE) + _expectedresponse = "W" + _cmd[1:] + LOG.debug("Reading block %04x..." % (block_addr)) + try: + self.pipe.write(_cmd) + _response = self.pipe.read(4 + READ_BLOCK_SIZE) + if _response[:4] != _expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + _block_data = _response[4:] + self.pipe.write(CMD_ACK) + _ack = self.pipe.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 _do_download(self): + self._ip620_enter_programming_mode() + _data = "" + _status = chirp_common.Status() + _status.msg = "Cloning from radio" + _status.cur = 0 + _status.max = self._memsize + for _addr in range(0, self._memsize, READ_BLOCK_SIZE): + _status.cur = _addr + READ_BLOCK_SIZE + self.status_fn(_status) + _block = self._ip620_read_block(_addr) + _data += _block + LOG.debug("Address: %04x" % _addr) + LOG.debug(util.hexprint(_block)) + self._ip620_exit_programming_mode() + return memmap.MemoryMap(_data) + + def _do_upload(self): + _status = chirp_common.Status() + _status.msg = "Uploading to radio" + self._ip620_enter_programming_mode() + _status.cur = 0 + _status.max = self._memsize + for _start_addr, _end_addr in self._ranges: + for _addr in range(_start_addr, _end_addr, WRITE_BLOCK_SIZE): + _status.cur = _addr + WRITE_BLOCK_SIZE + self.status_fn(_status) + self._ip620_write_block(_addr) + self._ip620_exit_programming_mode() + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = ("This radio driver is currently under development. " + "There are no known issues with it, but you should " + "proceed with caution. However, proceed at your own risk!") + return rp + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_bank = False + rf.has_ctone = True + rf.has_cross = False + rf.has_rx_dtcs = True + rf.has_tuning_step = False + rf.can_odd_split = False + rf.has_name = False + rf.valid_skips = [] + rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_duplexes = SHIFT_LIST + rf.valid_modes = ["FM", "NFM"] + rf.memory_bounds = (1, 200) + rf.valid_bands = IP620_BANDS + rf.valid_characters = ''.join(set(IP620_CHARSET)) + rf.valid_name_length = CHAR_LENGTH_MAX + return rf + + def process_mmap(self): + self._memobj = bitwise.parse(IP620_MEM_FORMAT, self._mmap) + + def sync_in(self): + try: + self._mmap = self._do_download() + except errors.RadioError: + raise + except Exception, e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + self.process_mmap() + + def sync_out(self): + self._do_upload() + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number - 1]) + + def _get_tone(self, _mem, mem): + def _get_dcs(val): + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return code, pol + + if _mem.tx_tone != 0xFFFF and _mem.tx_tone > 0x2800: + tcode, tpol = _get_dcs(_mem.tx_tone) + mem.dtcs = tcode + txmode = "DTCS" + elif _mem.tx_tone != 0xFFFF: + mem.rtone = _mem.tx_tone / 10.0 + txmode = "Tone" + else: + txmode = "" + + if _mem.rx_tone != 0xFFFF and _mem.rx_tone > 0x2800: + rcode, rpol = _get_dcs(_mem.rx_tone) + mem.rx_dtcs = rcode + rxmode = "DTCS" + elif _mem.rx_tone != 0xFFFF: + mem.ctone = _mem.rx_tone / 10.0 + rxmode = "Tone" + else: + rxmode = "" + + if txmode == "Tone" and not rxmode: + mem.tmode = "Tone" + elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone: + mem.tmode = "TSQL" + elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs: + mem.tmode = "DTCS" + elif rxmode or txmode: + mem.tmode = "Cross" + mem.cross_mode = "%s->%s" % (txmode, rxmode) + + if mem.tmode == "DTCS": + mem.dtcs_polarity = "%s%s" % (tpol, rpol) + + LOG.debug("Got TX %s (%i) RX %s (%i)" % (txmode, _mem.tx_tone, + rxmode, _mem.rx_tone)) + + def get_memory(self, number): + _mem = self._memobj.memory[number - 1] + _nam = self._memobj.chan_names[number - 1] + + def _is_empty(): + for i in range(0, 4): + if _mem.rx_freq[i].get_raw() != "\xFF": + return False + return True + + mem = chirp_common.Memory() + mem.number = number + + if _is_empty(): + mem.empty = True + return mem + + mem.freq = int(_mem.rx_freq) * 10 + + if int(_mem.rx_freq) == int(_mem.tx_freq): + mem.duplex = "" + mem.offset = 0 + else: + mem.duplex = int(_mem.rx_freq) > int(_mem.tx_freq) and "-" or "+" + mem.offset = abs(int(_mem.rx_freq) - int(_mem.tx_freq)) * 10 + + mem.mode = _mem.w_n and "FM" or "NFM" + self._get_tone(_mem, mem) + mem.power = POWER_LEVELS[_mem.power] + + mem.extra = RadioSettingGroup("Extra", "extra") + rs = RadioSetting("lout", "Lock out", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_mem.lout])) + mem.extra.append(rs) + + rs = RadioSetting("busy_loc", "Busy lock", + RadioSettingValueList(BUSYLOCK_LIST, + BUSYLOCK_LIST[_mem.busy_loc])) + mem.extra.append(rs) + + rs = RadioSetting("scan_add", "Scan add", + RadioSettingValueList(NO_YES_LIST, + NO_YES_LIST[_mem.scan_add])) + mem.extra.append(rs) + #TODO: Show name channel +## count = 0 +## for i in _nam.chan_name: +## if i == 0xFF: +## break +## try: +## mem.name += IP620_CHARSET[i] +## except Exception: +## LOG.error("Unknown name char %i: 0x%02x (mem %i)" % +## (count, i, number - 1)) +## mem.name += " " +## count += 1 +## mem.name = mem.name.rstrip() + + return mem + + def _set_tone(self, mem, _mem): + def _set_dcs(code, pol): + val = int("%i" % code, 8) + 0x2800 + if pol == "R": + val += 0x8000 + return val + + if mem.tmode == "Cross": + tx_mode, rx_mode = mem.cross_mode.split("->") + elif mem.tmode == "Tone": + tx_mode = mem.tmode + rx_mode = None + else: + tx_mode = rx_mode = mem.tmode + + if tx_mode == "DTCS": + _mem.tx_tone = mem.tmode != "DTCS" and \ + _set_dcs(mem.dtcs, mem.dtcs_polarity[0]) or \ + _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[0]) + elif tx_mode: + _mem.tx_tone = tx_mode == "Tone" and \ + int(mem.rtone * 10) or int(mem.ctone * 10) + else: + _mem.tx_tone = 0xFFFF + + if rx_mode == "DTCS": + _mem.rx_tone = _set_dcs(mem.rx_dtcs, mem.dtcs_polarity[1]) + elif rx_mode: + _mem.rx_tone = int(mem.ctone * 10) + else: + _mem.rx_tone = 0xFFFF + + LOG.debug("Set TX %s (%i) RX %s (%i)" % (tx_mode, _mem.tx_tone, + rx_mode, _mem.rx_tone)) + + def set_memory(self, mem): + _mem = self._memobj.memory[mem.number - 1] + if mem.empty: + _mem.set_raw("\xFF" * (_mem.size() / 8)) + return + + _mem.rx_freq = mem.freq / 10 + if mem.duplex == "OFF": + for i in range(0, 4): + _mem.tx_freq[i].set_raw("\xFF") + elif mem.duplex == "+": + _mem.tx_freq = (mem.freq + mem.offset) / 10 + elif mem.duplex == "-": + _mem.tx_freq = (mem.freq - mem.offset) / 10 + else: + _mem.tx_freq = mem.freq / 10 + + _mem.w_n = mem.mode == "FM" + self._set_tone(mem, _mem) + _mem.power = mem.power == POWER_LEVELS[1] + + for setting in mem.extra: + setattr(_mem, setting.get_name(), setting.value) + + def get_settings(self): + _settings = self._memobj.settings + _settings_misc = self._memobj.settings_misc + basic = RadioSettingGroup("basic", "Basic Settings") + top = RadioSettings(basic) + + rs = RadioSetting("rf_sql", "Squelch level (SQL)", + RadioSettingValueList(RFSQL_LIST, + RFSQL_LIST[_settings.rf_sql])) + basic.append(rs) + + rs = RadioSetting("step_freq", "Step frequency KHz (STP)", + RadioSettingValueList(STEP_LIST, + STEP_LIST[_settings.step_freq])) + basic.append(rs) + + rs = RadioSetting("fm_radio", "FM radio (DW)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings_misc.fm_radio])) + basic.append(rs) + + rs = RadioSetting("call_tone", "Call tone (CK)", + RadioSettingValueList(CALLTONE_LIST, + CALLTONE_LIST[_settings.call_tone])) + basic.append(rs) + + rs = RadioSetting("tot", "Time-out timer (TOT)", + RadioSettingValueList(TIMEOUT_LIST, + TIMEOUT_LIST[_settings.tot])) + basic.append(rs) + + rs = RadioSetting("chan_disp_way", "Channel display way", + RadioSettingValueList(CH_FLAG_LIST, + CH_FLAG_LIST[_settings.chan_disp_way])) + basic.append(rs) + + rs = RadioSetting("vox", "VOX Gain (VOX)", + RadioSettingValueList(VOX_LIST, + VOX_LIST[_settings.vox])) + basic.append(rs) + + rs = RadioSetting("vox_dly", "VOX Delay", + RadioSettingValueList(VOXDELAY_LIST, + VOXDELAY_LIST[_settings.vox_dly])) + basic.append(rs) + + rs = RadioSetting("beep", "Beep (BP)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings.beep])) + basic.append(rs) + + rs = RadioSetting("auto_lock", "Auto lock (KY)", + RadioSettingValueList(NO_YES_LIST, + NO_YES_LIST[_settings_misc.auto_lock])) + basic.append(rs) + + rs = RadioSetting("bat_save", "Battery Saver (SAV)", + RadioSettingValueList(BAT_SAVE_LIST, + BAT_SAVE_LIST[_settings.bat_save])) + basic.append(rs) + + rs = RadioSetting("chan_pri", "Channel PRI (PRI)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings.chan_pri])) + basic.append(rs) + + rs = RadioSetting("chan_pri_num", "Channel PRI time Sec (PRI)", + RadioSettingValueList(PRI_NUM_LIST, + PRI_NUM_LIST[_settings.chan_pri_num])) + basic.append(rs) + + rs = RadioSetting("end_beep", "End beep (ET)", + RadioSettingValueList(ENDBEEP_LIST, + ENDBEEP_LIST[_settings.end_beep])) + basic.append(rs) + + rs = RadioSetting("ch_mode", "CH mode", + RadioSettingValueList(ON_OFF_LIST, + ON_OFF_LIST[_settings.ch_mode])) + basic.append(rs) + + rs = RadioSetting("scan_rev", "Scan rev (SCAN)", + RadioSettingValueList(SCANM_LIST, + SCANM_LIST[_settings.scan_rev])) + basic.append(rs) + + rs = RadioSetting("enc", "Frequency lock (ENC)", + RadioSettingValueList(OFF_ON_LIST, + OFF_ON_LIST[_settings.enc])) + basic.append(rs) + + rs = RadioSetting("wait_back_light", "Wait back light (LED)", + RadioSettingValueList(BACKLIGHT_LIST, + BACKLIGHT_LIST[_settings.wait_back_light])) + basic.append(rs) + + return top + + def _set_misc_settings(self, settings): + for element in settings: + try: + setattr(self._memobj.settings_misc, + element.get_name(), + element.value) + except Exception, e: + LOG.debug(element.get_name()) + raise + + def set_settings(self, settings): + _settings = self._memobj.settings + _settings_misc = self._memobj.settings_misc + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + if not element.changed(): + continue + try: + setting = element.get_name() + if setting in ["auto_lock","fm_radio"]: + oldval = getattr(_settings_misc, setting) + else: + oldval = getattr(_settings, setting) + + newval = element.value + + LOG.debug("Setting %s(%s) <= %s" % (setting, oldval, newval)) + if setting in ["auto_lock","fm_radio"]: + setattr(_settings_misc, setting, newval) + else: + setattr(_settings, setting, newval) + except Exception, e: + LOG.debug(element.get_name()) + raise