[chirp_devel] [KYD IP-620] Add support for KYD IP-620 radio - issue #2033
# 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
# HG changeset patch # User Alexey K <lepik.stv@gmail.com mailto: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
Nice, thanks! Unfortunately, pasting the patch into your mailer has broken the formatting (wrapped lines, etc). Can you send it again, but as a file attachment? That usually avoids it getting broken.
Also, even better, hg can email directly from the command line with the "patchbomb" extension. You might check that out, but either will work.
Thanks!
--Dan
participants (2)
-
Alex K
-
Dan Smith