# HG changeset patch # User angelof9 # Date 1651855800 -7200 # Fri May 06 18:50:00 2022 +0200 # Node ID 8a76c318a07f803f507cb09be62e099d25013370 # Parent 4d991cdfc502315cd779bf3c2ce2dd3cec0c7bac [RT-490] New driver for Radtel RT-490 and 8 other clones (Fix issue #9665) Add driver for: - Radtel RT-490, - MMLradio 8629, - JJCC 8629, - Socotran JC8629, - Socotran FB8629, - Jianpai 8629, - Boristone 8RS, - Abbree AR869, - HamGeek HG590. Also support more features than official software (more info on issue details) diff --git a/chirp/drivers/rt490.py b/chirp/drivers/rt490.py new file mode 100644 --- /dev/null +++ b/chirp/drivers/rt490.py @@ -0,0 +1,1466 @@ +# Copyright (c) 2022 +# +# BSD 2-Clause "Simplified" License +# https://opensource.org/licenses/BSD-2-Clause +# +# Redistribution and use in source and binary forms, +# with or without modification, are permitted provided +# that the following conditions are met: +# +# 1. Redistributions of source code must retain the above +# copyright notice, this list of conditions and +# the following disclaimer. +# 2. Redistributions in binary form must reproduce the +# above copyright notice, this list of conditions and +# the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +# THE POSSIBILITY OF SUCH DAMAGE. + +import time +import os +import struct +import unittest +import logging +from chirp import chirp_common, directory, memmap +from chirp import bitwise, errors, util +from chirp.settings import RadioSetting, RadioSettingGroup, \ + RadioSettingValueInteger, RadioSettingValueList, \ + RadioSettingValueBoolean, RadioSettings, \ + RadioSettingValueString, RadioSettingValueMap, \ + RadioSettingValueFloat, InvalidValueError +from textwrap import dedent +LOG = logging.getLogger(__name__) +try: + from builtins import bytes + has_future = True +except ImportError: + has_future = False + LOG.debug('python-future package is not available; ' + '%s requires it' % __name__) + +# 'True' or 'False' +# True unlocks: +# - FM preset +# - Channel memory # preset +# - Killswitch (with revive killed radios) +# - Bands settings +# - Disable radio identification string verification +# (good to recover from a bad state) +RT490_EXPERIMENTAL = True + +MEM_FORMAT_RT490 = """ +struct { // Memory settings + lbcd rxfreq[4]; + lbcd txfreq[4]; + ul16 rxtone; + ul16 txtone; + u8 signal; // int 0->14, Signal 1->15 + u8 pttid; // [ 'OFF', 'BOT', 'EOT', 'Both'] + u8 dcp:4, // What is DCP ? FHSS ? DC-FHSS ??? TODO + power:4; // POWER_LEVELS + u8 unknown3_0:1, // Used by the driver to store AM/NAM flag + // (thank you Radtel for free space) + narrow:1, // bool true=NFM false=FM (col[7] a) + unknown3_1:1, + unknown3_2:1, + bcl:1, // bool (col[9] a2) + scan:1, // bool (col[10] a3) + tx_enable:1, // bool (col[1] a4) + learn:1; // bool ??? TODO (col[14] a5) +} memory[%(memsize)d]; +#seekto 0x1000; // Memory names (@4096) +struct { + char name[12]; + u8 ffpad[4]; +} memname[%(memsize)d]; +#seekto 0x2000; // GOOD DCP keys ??? TODO ?? (@8192) +struct { + u8 code[4]; +} memcode[%(memsize)d]; // up to @x2400 + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +#seekto 0x3400; // Custom ANI Names (@13312) +struct { + char name[10]; + u8 ffpad[6]; +} custom_ani_names[10]; + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +#seekto 0x3500; // ANI Codes (@13568) +struct { + u8 anicode[6]; + u8 ffpad[10]; +} anicodes[60]; + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +#seekto 0x3900; // Custom channel names (@14592) +struct { + char name[10]; + u8 ffpad[6]; +} custom_channel_names[10]; + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +#seekto 0x3A00; // Settings (@14848) +struct { + u8 squelch; // 0: int 0 -> 9 + u8 savemode; // 1: [ 'OFF', 'Normal', 'Super', 'Deep' ] + u8 vox; // 2: off=0, 1 -> 9 + u8 backlight; // 3: [ 'OFF', '5s', '15s', '20s', '30s', '1m', '2m', '3m' ] + u8 tdr; // 4: bool + u8 timeout; // 5: n*30seconds, 0-240s + u8 beep; // 6: bool + u8 voice; // 7: bool + u8 byte_not_used_10; // 8: Allways 1 + u8 dtmfst; // 9: [ 'OFF', 'KB Side Tone', 'ANI Side Tone', 'KB ST + ANI ST' ] + u8 scanmode; // 10: [ 'TO', 'CO', 'SE' ] + u8 pttid; // 11: [ 'OFF', 'BOT', 'EOT', 'Both'] + u8 pttiddelay; // 12: [ '0', '100ms', '200ms', '400ms', '600ms', '800ms', '1000ms' ] + u8 cha_disp; // 13: [ 'Name', 'Freq', 'Channel ID' ] + u8 chb_disp; // 14: [ 'Name', 'Freq', 'Channel ID' ] + u8 bcl; // 15: bool + u8 autolock; // 0: [ 'OFF', '5s', '10s', 15s' ] + u8 alarm_mode; // 1: [ 'Site', 'Tone', 'Code' ] + u8 alarmsound; // 2: bool + u8 txundertdr; // 3: [ 'OFF', 'A', 'B' ] + u8 tailnoiseclear; // 4: [off, on] + u8 rptnoiseclear; // 5: n*100ms, 0-1000 + u8 rptnoisedelay; // 6: n*100ms, 0-1000 + u8 roger; // 7: bool + u8 active_channel; // 8: 0 or 1 + u8 fmradio; // 9: boolean, inverted + u8 workmodeb:4, // 10:up [ 'VFO', 'CH Mode' ] + workmodea:4; // 10:down [ 'VFO', 'CH Mode' ] + u8 kblock; // 11: bool // TODO TEST WITH autolock + u8 powermsg; // 12: 0=Image / 1=Voltage + u8 byte_not_used_21; // 13: Allways 0 + u8 rpttone; // 14: [ '1000Hz', '1450Hz', '1750Hz', '2100Hz' ] + u8 byte_not_used_22; // 15: pad with xFF + u8 vox_delay; // 0: [str(float(a)/10)+'s' for a in range(5,21)] '0.5s' to '2.0s' + u8 timer_menu_quit; // 1: [ '5s', '10s', '15s', '20s', '25s', '30s', '35s', '40s', '45s', '50s', '60s' ] + u8 byte_not_used_30; // 2: pad with xFF + u8 byte_not_used_31; // 3: pad with xFF + u8 enable_killsw; // 4: bool + u8 display_ani; // 5: bool + u8 byte_not_used_32; // 6: pad with xFF + u8 enable_gps; // 7: bool + u8 scan_dcs; // 8: [ 'All', 'Receive', 'Transmit' ] + u8 ani_id; // 9: int 0-59 (ANI 1-60) + u8 rx_time; // 10: bool + u8 ffpad0[5]; // 11: Pad xFF + u8 cha_memidx; // 0: Memory index when channel A use memories + u8 byte_not_used_40; + u8 chb_memidx; // 2: Memory index when channel B use memories + u8 byte_not_used_41; + u8 ffpad1[10]; + ul16 fmpreset; +} settings; + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +struct settings_vfo_chan { + u8 rxfreq[8]; // 0 + ul16 rxtone; // 8 + ul16 txtone; // 10 + ul16 byte_not_used0; // 12 Pad xFF + u8 sftd:4, // 14 Shift dir [ 'OFF', '+', '-' ] + signal:4; // 14 int 0->14, Signal 1->15 + u8 byte_not_used1; // 15 Pad xFF + u8 power; // 16:0 POWER_LEVELS + u8 fhss:4, // 17 [ 'OFF', 'FHSS 1', 'FHSS 2', 'FHSS 3', 'FHSS 4' ] + narrow:4; // 17 bool true=NFM false=FM + u8 byte_not_used2; // 18 Pad xFF but received 0x00 ??? + u8 freqstep; // 19:3 [ '2.5 KHz', '5.0 KHz', '6.25 KHz', '10.0 KHz', '12.5 KHz', '20.0 KHz', '25.0 KHz', '50.0 KHz' ] + u8 byte_not_used3; // 20:4 Pad xFF but received 0x00 ??? TODO + u8 offset[6]; // 21:5 Freq NN.NNNN (without the dot) TEST TEST + u8 byte_not_used4; // 27:11 Pad xFF + u8 byte_not_used5; // 28 Pad xFF + u8 byte_not_used6; // 29 Pad xFF + u8 byte_not_used7; // 30 Pad xFF + u8 byte_not_used8; // 31:15 Pad xFF +}; +#seekto 0x3A40; // VFO A/B (@14912) +struct { + struct settings_vfo_chan vfo_a; + struct settings_vfo_chan vfo_b; +} settings_vfo; +#seekto 0x3A80; // Side keys settings (@14976) +struct { // Values from Radio + u8 pf2_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan', '29': 'Search, '1': 'PPT B' } + u8 pf2_long; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan', '29': 'Search' } + u8 pf3_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan', '29': 'Search' } + u8 ffpad; // Pad xFF +} settings_sidekeys; +struct dtmfcode { + u8 code[5]; // 5 digits DTMF + u8 ffpad[11]; // Pad xFF +}; + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +#seekto 0x3B00; // DTMF (@15104) +struct dtmfcode settings_dtmfgroup[15]; +struct { // @15296+3x16 + u8 byte_not_used1; // 0: Pad xFF something here + u8 byte_not_used2; // 1: Pad xFF something here + u8 byte_not_used3; // 2: Pad xFF something here + u8 byte_not_used4; // 3: Pad xFF + u8 byte_not_used5; // 4: Pad xFF + u8 unknown_dtmf; // 5: 0 TODO ???? wtf is alarmcode/alarmcall TODO + u8 pttid; // 6: [off, BOT, EOT, Both] + u8 dtmf_speed_on; // 7: ['50ms', '100ms', '200ms', '300ms', '500ms'] + u8 dtmf_speed_off; // 8:0 ['50ms', '100ms', '200ms', '300ms', '500ms'] +} settings_dtmf; + // Filled with xFF during download (=> no need to fill with xFF), ready to upload +#seekto 0x3C00; // DTMF Kill/ReLive Codes (@15360) +struct { + u8 kill_dtmf[6]; // 0: Kill DTMF + u8 ffpad1[2]; // Pad xFF + u8 revive_dtmf[6]; // 8: Revive DTMF + u8 ffpad2[2]; // Pad xFF +} settings_killswitch; + // Some unknown data between 0x3E00 and 0x3F00 +#seekto 0x3F80; // Hmm hmm +struct { + u8 unknown_data_0[16]; + u8 unknown_data_1; + u8 active; // Bool radio killed (killed=0, active=1) + u8 unknown_data_2[46]; +} management_settings; +struct band { + u8 enable; // 0 bool / enable-disable Tx on band + bbcd freq_low[2]; // 1 lowest band frequency + bbcd freq_high[2]; // 3 highest band frequency +}; +#seekto 0x3FC0; // Bands settings (@16320) +struct { + struct band band136; // 0 Settings for 136MHz band + struct band band400; // 5 Settings for 400MHz band + struct band band200; // 10 Settings for 200MHz band + u8 byte_not_used1; // 15 + struct band band350; // 0 Settings for 350MHz band + u8 byte_not_used2[43];// 5 +} settings_bands; +""" + +def _rt490_enter_programming_mode(radio): + serial = radio.pipe + + try: + serial.write(radio._magic) + ack = serial.read(1) + except: + raise errors.RadioError("Error communicating with radio") + + if not ack: + raise errors.RadioError("No response from radio") + elif ack != radio.CMD_ACK: + raise errors.RadioError("Radio refused to enter programming mode") + + try: + serial.write("\x46") + ident = serial.read(8) + except: + raise errors.RadioError("Error communicating with radio") + + if not ident.startswith(radio._fingerprint) and not RT490_EXPERIMENTAL: + LOG.debug(util.hexprint(ident)) + raise errors.RadioError("Radio returned unknown identification string") + +def _rt490_exit_programming_mode(radio): + serial = radio.pipe + try: + serial.write(radio.CMD_EXIT) + except: + raise errors.RadioError("Radio refused to exit programming mode") + +def _rt490_read_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'R', block_addr, block_size) + expectedresponse = cmd + LOG.debug("Reading block %04x..." % (block_addr)) + + try: + serial.write(cmd) + response = serial.read(4 + block_size) + if response[:4] != expectedresponse: + raise Exception("Error reading block %04x." % (block_addr)) + + block_data = response[4:] + except: + raise errors.RadioError("Failed to read block at %04x" % block_addr) + + return block_data + +def _rt490_write_block(radio, block_addr, block_size): + serial = radio.pipe + + cmd = struct.pack(">cHb", 'W', block_addr, block_size) + data = radio.get_mmap()[block_addr:block_addr + block_size] + + LOG.debug("Writing Data:") + LOG.debug(util.hexprint(cmd + data)) + + try: + serial.write(cmd + data) + if serial.read(1) != radio.CMD_ACK: + raise Exception("No ACK") + except: + raise errors.RadioError("Failed to send block " + "to radio at %04x" % block_addr) + +def do_download(radio): + LOG.debug("download") + _rt490_enter_programming_mode(radio) + + data = "" + + status = chirp_common.Status() + status.msg = "Cloning from radio" + + status.cur = 0 + status.max = radio._memsize + + for addr in range(0, radio._memsize, radio.BLOCK_SIZE): + status.cur = addr + radio.BLOCK_SIZE + radio.status_fn(status) + + block = _rt490_read_block(radio, addr, radio.BLOCK_SIZE) + data += block + + LOG.debug("Address: %04x" % addr) + LOG.debug(util.hexprint(block)) + + _rt490_exit_programming_mode(radio) + + return memmap.MemoryMap(data) + +def do_upload(radio): + status = chirp_common.Status() + status.msg = "Uploading to radio" + + _rt490_enter_programming_mode(radio) + + status.cur = 0 + status.max = radio._memsize + + for start_addr, end_addr in radio._ranges: + for addr in range(start_addr, end_addr, radio.BLOCK_SIZE): + status.cur = addr + radio.BLOCK_SIZE + radio.status_fn(status) + _rt490_write_block(radio, addr, radio.BLOCK_SIZE) + + _rt490_exit_programming_mode(radio) + +class RT490Radio(chirp_common.CloneModeRadio): + """RADTEL RT-490""" + VENDOR = "Radtel" + MODEL = "RT-490" + BLOCK_SIZE = 0x40 # 64 bytes + CMD_EXIT = "E" + BAUD_RATE = 9600 + # Advertised bands + """ + VALID_BANDS = [ + ( 66000000, 108000000), + (108000000, 136000000), + (136000000, 180000000), + (200000000, 260000000), + (350000000, 400000000), + (400000000, 520000000), + ]""" + # Tx/Rx bands + VALID_BANDS = [ + (108000000, 136000000), + (136000000, 180000000), + (200000000, 260000000), + (350000000, 400000000), + (400000000, 520000000), + ] + VALID_MODES = [ "FM", "NFM", "AM", "NAM" ] + + POWER_LEVELS = [ chirp_common.PowerLevel("High", watts=5), + chirp_common.PowerLevel("Low", watts=3) ] + POWER_LEVELS_LIST = [ str(i) for i in POWER_LEVELS ] + FHSS_LIST = [ 'OFF', 'ENCRYPT 1', 'ENCRYPT 2', 'ENCRYPT 3', 'ENCRYPT 4' ] + DCP_LIST = ['OFF', 'DCP1', 'DCP2', 'DCP3', 'DCP4' ] # Same as FHSS ? Seems yes + TUNING_STEPS = [ 2.5, 5.0, 6.25, 10.0, 12.5, 20.0, 25.0, 50.0 ] + TUNING_STEPS_LIST = [ str(i)+' KHz' for i in TUNING_STEPS ] + SIGNAL = [ str(i) for i in range(1, 16) ] + DTCS_CODES = list(sorted(chirp_common.DTCS_CODES + [645])) + PTTID = [ 'OFF', 'BOT', 'EOT', 'Both' ] + PTTIDDELAYS = [ '0', '100ms', '200ms', '400ms', '600ms', '800ms', '1000ms' ] + DTMFCHARS = '0123456789ABCD*#' + DTMF_SPEEDS = [ '50ms', '100ms', '200ms', '300ms', '500ms' ] + SIDEKEY_VALUEMAP = [ ('FM', 7), ('Tx Power', 10), ('Scan', 28), ('Search', 29), ('PTT B', 1) ] + KEY_CHARS = '0123456789ABCDEF' + FULL_CHARSET_ASCII = "".join( [chr(x) for x in range(ord(" "), ord("~") + 1)] + + [chr(x) for x in range(128, 255)] + [ chr(0) ] ) + VFO_SFTD = [ 'OFF', '+', '-' ] + WORKMODES = [ 'VFO', 'Memory Mode' ] + SAVEMODES = [ 'OFF', 'Normal', 'Super', 'Deep' ] + DISPLAYMODES = [ 'Name', 'Freq', 'Memory ID' ] + SCANMODES = [ 'TO', 'CO', 'SE' ] + ALARMMODES = [ 'On Site', 'Send Sound', 'Send Code' ] + TDRTXMODES = [ 'OFF', 'A', 'B' ] + SCANDCSMODES = [ 'All', 'Receive', 'Transmit' ] + POWERMESSAGES = [ 'Image', 'Voltage' ] + FMRADIO = [ 'ON', 'OFF' ] + ENABLERADIO = [ 'Killed', 'Active' ] + CHANNELS = [ 'A', 'B' ] + TOT_LIST = [ 'OFF' ] + [ str(i*30) + "s" for i in range(1,9) ] + VOX_LIST = [ 'OFF' ] + [ str(i) for i in range(1,9) ] + BACKLIGHT_TO = [ 'OFF', '5s', '10s', '15s', '20s', '30s', '1m', '2m', '3m' ] + AUTOLOCK_TO = [ 'OFF', '5s', '10s', '15s' ] + MENUEXIT_TO = [ '5s', '10s', '15s', '20s', '25s', '30s', '35s', '40s', '45s', '50s', '60s' ] + SQUELCHLVLS = [ str(i) for i in range(10) ] + ANI_IDS = [ str(i+1) for i in range(60) ] + VOXDELAYLIST = [str(float(a)/10)+'s' for a in range(5,21)] + DTMFSTLIST = [ 'OFF', 'DT Side Tone', 'ANI Side Tone', 'DT ST + ANI ST' ] + RPTTONES = [ '1000Hz', '1450Hz', '1750Hz', '2100Hz' ] + RPTNOISE = [str(a)+'s' for a in range(11)] + CMD_ACK = "\x06" + # magic = progmode + modelType + garbage (works with any last char) + _magic = "PROGROM" + "JJCC" + "U" + # fingerprint is default band ranges of the radio + # the driver can change band ranges and fingerprint will + # change accordingly, so it is not used to verify radio id. + _fingerprint = "\x01\x36\x01\x80\x04\x00\x05\x20" + _memory_size = _upper = 256 # Number of memory slots + _mem_params = (_upper-1) + _frs = _murs = _pmr = _gmrs = True + # 16KB of memory, download read everything + # same as official software (remark: loops if overread :)) + _memsize = 16384 + # Ranges of memory used when uploading data to radio + # same as official software + _ranges = [ (0x0000, 0x2400), (0x3400, 0x3C40), (0x3FC0, 0x4000) ] + if RT490_EXPERIMENTAL: + # Experimental driver (already heavily tested) + _ranges = [ (0x0000, 0x2400), (0x3400, 0x3C40), (0x3F80, 0x4000) ] + # Danger zone + #_ranges = [ (0x0000, 0x2500), (0x3400, 0x3C40), (0x3E00, 0x4000) ] + + def set_settings(self, settings): + for element in settings: + if not isinstance(element, RadioSetting): + self.set_settings(element) + continue + else: + self._set_setting(element) + + def _set_setting(self, setting): # WIP + key = setting.get_name() + val = setting.value + if key.startswith('dummy'): + return + elif key.startswith('settings_dtmfgroup.'): + if str(val) == "": + setattr(self._memobj.settings_dtmfgroup[int(key.split('@')[1])-1], 'code', [0xFF]*5) + else: + tmp = [self.DTMFCHARS.index(c) for c in str(val)] + tmp += [ 0xFF ] * (5 - len(tmp)) + setattr(self._memobj.settings_dtmfgroup[int(key.split('@')[1])-1], 'code', tmp) + elif key.startswith('settings.'): + if key.endswith('_memidx'): + val = int(val) - 1 + if key.endswith('fmpreset'): + tmp = val.get_value() * 10 + setattr(self._memobj.settings, key.split('.')[1], tmp) + else: + setattr(self._memobj.settings, key.split('.')[1], int(val)) + elif key.startswith('settings_dtmf.'): + attr = key.split('.')[1] + setattr(self._memobj.settings_dtmf, attr, int(val)) + if attr.startswith('pttid'): + setattr(self._memobj.settings, attr, int(val)) + elif key.startswith('settings_sidekeys.'): + setattr(self._memobj.settings_sidekeys, key.split('.')[1], int(val)) + elif key.startswith('settings_vfo.'): # TODO rx/tx tones + tmp = key.split('.') + attr = tmp[2] + vfo = tmp[1] + #LOG.debug(">>> PRE key '%s'" % key) + #LOG.debug(">>> PRE val '%s'" % val) + if attr.startswith('rxfreq'): + value = chirp_common.parse_freq(str(val)) / 10 + for i in range(7, -1, -1): + self._memobj.settings_vfo[vfo].rxfreq[i] = value % 10 + value /= 10 + elif attr.startswith('offset'): + value = int(float(str(val)) * 10000) + for i in range(5, -1, -1): + self._memobj.settings_vfo[vfo].offset[i] = value % 10 + value /= 10 + else: + setattr(self._memobj.settings_vfo[vfo], attr, int(val)) + elif key.startswith('settings_bands.'): + tmp = key.split('.') + attr = tmp[2] + band = tmp[1] + setattr(self._memobj.settings_bands[band], attr, int(val)) + elif key.startswith('settings_killswitch.'): + attr = key.split('.')[1] + if attr.endswith('dtmf'): + if str(val) == "": + setattr(self._memobj.settings_killswitch, attr, [0x01, 0x02, 0x01, 0x03, 0x01, 0x04]) + else: + setattr(self._memobj.settings_killswitch, attr, [self.DTMFCHARS.index(c) for c in str(val)]) + elif attr.startswith('enable'): + setattr(self._memobj.settings_killswitch, attr, int(val)) + else: + LOG.debug(">>> TODO key '%s'" % key) + LOG.debug(">>> TODO val '%s'" % val) + elif key.startswith('custom_') or key.startswith('anicode') or key.startswith('memcode'): + tmp = key.split('@') + if key.startswith('anicode'): + val = [self.DTMFCHARS.index(c) for c in str(val)] + val += [ 0xFF ] * (6 - len(val)) + for i in range(10): + self._memobj[str(tmp[0])][int(tmp[2])]["ffpad"][i] = 0xFF + elif key.startswith('memcode'): + if len(str(val)) > 0: + tmpp = str(val).zfill(6) + val = self._encode_key(tmpp) + else: + val = (0xFF, 0xFF, 0xFF, 0xFF) + if key.startswith('custom_'): + for i in range(6): + self._memobj[str(tmp[0])][int(tmp[2])]["ffpad"][i] = 0xFF + setattr(self._memobj[str(tmp[0])][int(tmp[2])], str(tmp[1]), val) + elif key.startswith('management_settings'): + setattr(self._memobj.management_settings, key.split('.')[1], int(val)) + else: + LOG.debug(">>> TODO _set_setting key '%s'" % key) + LOG.debug(">>> TODO _set_setting val '%s'" % val) + + def _get_settings_bands(self): + ret = RadioSettingGroup('bands', 'Bands') + bands = [ ('136', self._memobj.settings_bands.band136), ('200', self._memobj.settings_bands.band200), + ('350', self._memobj.settings_bands.band350), ('400', self._memobj.settings_bands.band400) ] + for label, band in bands: + rs = RadioSetting('settings_bands.band%s.enable' % label, 'Enable Band %s' % label, + RadioSettingValueBoolean(band.enable)) + ret.append(rs) + rsi = RadioSettingValueInteger(1, 1000, band.freq_low) + #if label == '136' or label == '400': + # rsi.set_mutable(False) + rs = RadioSetting("settings_bands.band%s.freq_low" % label, + "Band %s Lower Limit (MHz) (EXPERIMENTAL)" % label, + rsi) + ret.append(rs) + rsi = RadioSettingValueInteger(1, 1000, band.freq_high) + #if label == '350': + # rsi.set_mutable(False) + rs = RadioSetting("settings_bands.band%s.freq_high" % label, + "Band %s Upper Limit (MHz) (EXPERIMENTAL)" % label, + rsi) + ret.append(rs) + return ret + + def _get_settings_ks(self): + ret = RadioSettingGroup('killswitch', 'Killswitch') + # Kill Enable/Disable enable_killsw + ret.append(RadioSetting('settings.enable_killsw', 'Enable Killswitch', + RadioSettingValueBoolean(self._memobj.settings.enable_killsw))) + # Kill DTMF + cur = ''.join( + self.DTMFCHARS[i] + for i in self._memobj.settings_killswitch.kill_dtmf if int(i) < 0xF) + ret.append( + RadioSetting( 'settings_killswitch.kill_dtmf', 'DTMF Kill', + RadioSettingValueString(6, 6, cur, + autopad=False, + charset=self.DTMFCHARS))) + # Revive DTMF + cur = ''.join( + self.DTMFCHARS[i] + for i in self._memobj.settings_killswitch.revive_dtmf if int(i) < 0xF) + ret.append( + RadioSetting( 'settings_killswitch.revive_dtmf', 'DTMF Revive', + RadioSettingValueString(6, 6, cur, + autopad=False, + charset=self.DTMFCHARS))) + # Enable/Disable entire radio + rs = RadioSettingValueString(0, 255, "Can be used to revive radio") + rs.set_mutable(False) + ret.append(RadioSetting('dummy', 'Factory reserved', rs)) + tmp = 1 if int(self._memobj.management_settings.active) > 0 else 0 + ret.append(RadioSetting('management_settings.active', 'Radio Status', + RadioSettingValueList(self.ENABLERADIO, + self.ENABLERADIO[tmp]))) + return ret + + def _get_settings_dtmf(self): + dtmf = RadioSettingGroup('dtmf', 'DTMF') + # DTMF Group + msgs = [ "Allowed chars (%s)" % self.DTMFCHARS, + "Input from 0 to 5 characters." + ] + for msg in msgs: + rsvs = RadioSettingValueString(0, 255, msg, autopad=False) + rsvs.set_mutable(False) + rs = RadioSetting('dummy_dtmf_msg_%i' % msgs.index(msg), 'Input rule %i' % int(msgs.index(msg)+1), rsvs) + dtmf.append(rs) + for i in range(1, 16): + cur = ''.join( + self.DTMFCHARS[i] + for i in self._memobj.settings_dtmfgroup[i - 1].code if int(i) < 0xF) + dtmf.append( + RadioSetting( + 'settings_dtmfgroup.code@%i' % i, 'PTT ID Code %i' % i, + RadioSettingValueString(0, 5, cur, + autopad=False, + charset=self.DTMFCHARS))) + # DTMF Speed (on time in ms) + dtmf_speed_on = int(self._memobj.settings_dtmf.dtmf_speed_on) + if dtmf_speed_on > len(self.DTMF_SPEEDS)-1: + self._memobj.settings_dtmf.dtmf_speed_on = 0 + LOG.debug('DTMF Speed On overflow') + cur = self.DTMF_SPEEDS[dtmf_speed_on] + dtmf.append( + RadioSetting( + 'settings_dtmf.dtmf_speed_on', 'DTMF Speed (on time in ms)', + RadioSettingValueList(self.DTMF_SPEEDS, cur))) + # DTMF Speed (on time in ms) + dtmf_speed_off = int(self._memobj.settings_dtmf.dtmf_speed_off) + if dtmf_speed_off > len(self.DTMF_SPEEDS)-1: + self._memobj.settings_dtmf.dtmf_speed_off = 0 + LOG.debug('DTMF Speed Off overflow') + cur = self.DTMF_SPEEDS[dtmf_speed_off] + dtmf.append( + RadioSetting( + 'settings_dtmf.dtmf_speed_off', 'DTMF Speed (off time in ms)', + RadioSettingValueList(self.DTMF_SPEEDS, cur))) + # PTT ID + pttid = int(self._memobj.settings_dtmf.pttid) + if pttid > len(self.PTTID)-1: + self._memobj.settings_dtmf.pttid = 0 + LOG.debug('PTT ID overflow') + cur = self.PTTID[pttid] + dtmf.append( + RadioSetting( + 'settings_dtmf.pttid', 'Send DTMF Code (PTT ID)', + RadioSettingValueList(self.PTTID, cur))) + # PTT ID Delay + pttiddelay = int(self._memobj.settings.pttiddelay) + if pttiddelay > len(self.PTTIDDELAYS)-1: + self._memobj.settings.pttiddelay = 0 + LOG.debug('PTT ID Delay overflow') + cur = self.PTTIDDELAYS[pttiddelay] + dtmf.append( + RadioSetting( + 'settings.pttiddelay', 'PTT ID Delay', + RadioSettingValueList(self.PTTIDDELAYS, cur))) + dtmf.append( + RadioSetting( + 'settings.dtmfst', 'DTMF Side Tone (Required for GPS ID)', + RadioSettingValueList(self.DTMFSTLIST, self.DTMFSTLIST[self._memobj.settings.dtmfst]))) + return dtmf + + def _get_settings_sidekeys(self): + ret = RadioSettingGroup('sidekeys', 'Side Keys') + ret.append(RadioSetting( 'settings_sidekeys.pf2_short', 'Side key 1 (PTT2) Short press', + RadioSettingValueMap( self.SIDEKEY_VALUEMAP, self._memobj.settings_sidekeys.pf2_short))) + ret.append(RadioSetting( 'settings_sidekeys.pf2_long', 'Side key 1 (PTT2) Long press', + RadioSettingValueMap( self.SIDEKEY_VALUEMAP[:-1], self._memobj.settings_sidekeys.pf2_long))) + ret.append(RadioSetting( 'settings_sidekeys.pf3_short', 'Side key 2 (PTT3) Short press', + RadioSettingValueMap( self.SIDEKEY_VALUEMAP[:-1], self._memobj.settings_sidekeys.pf3_short))) + rs = RadioSettingValueString(0, 255, "MONI") + rs.set_mutable(False) + ret.append(RadioSetting( 'dummy', 'Side key 2 (PTT3) Long press', rs)) + return ret + + def _get_settings_vfo(self, vfo, chan): # WIP TODO Rx/Tx tones + ret = RadioSettingGroup('settings_vfo@%s' % chan.lower(), 'VFO %s Settings' % chan) + ret.append(RadioSetting('settings.workmode%s' % chan.lower(), 'VFO %s Workmode' % chan, + RadioSettingValueList( + self.WORKMODES, + self.WORKMODES[self._memobj.settings['workmode'+chan.lower()]]))) + tmp = ''.join(self.DTMFCHARS[i] for i in self._memobj.settings_vfo['vfo_'+chan.lower()].rxfreq if i < 0xFF) + ret.append(RadioSetting('settings_vfo.vfo_%s.rxfreq' % chan.lower(), 'Rx Frequency', + RadioSettingValueFloat( + 66, 550, chirp_common.format_freq(int(tmp) * 10), resolution=0.00001, precision=5 ))) + # TODO Rx/Tx tones + ret.append(RadioSetting('settings_vfo.vfo_%s.sftd' % chan.lower(), 'Freq offset direction', + RadioSettingValueList( + self.VFO_SFTD, + self.VFO_SFTD[self._memobj.settings_vfo['vfo_'+chan.lower()].sftd]))) + tmp = ''.join(self.DTMFCHARS[i] for i in self._memobj.settings_vfo['vfo_'+chan.lower()].offset if i < 0xFF) + ret.append(RadioSetting('settings_vfo.vfo_%s.offset' % chan.lower(), 'Tx Offset', + RadioSettingValueFloat( + 0, 99.9999, float(tmp) / 10000) )) + ret.append(RadioSetting('settings_vfo.vfo_%s.signal' % chan.lower(), 'PTT ID Code (S-Code)', + RadioSettingValueList(self.SIGNAL, + self.SIGNAL[self._memobj.settings_vfo['vfo_'+chan.lower()].signal]))) + ret.append(RadioSetting('settings_vfo.vfo_%s.power' % chan.lower(), 'Tx Power', + RadioSettingValueList(self.POWER_LEVELS_LIST, + self.POWER_LEVELS_LIST[self._memobj.settings_vfo['vfo_'+chan.lower()].power]))) + ret.append(RadioSetting('settings_vfo.vfo_%s.fhss' % chan.lower(), 'FHSS (Encryption)', + RadioSettingValueList(self.FHSS_LIST, + self.FHSS_LIST[self._memobj.settings_vfo['vfo_'+chan.lower()].fhss]))) + ret.append(RadioSetting('settings_vfo.vfo_%s.narrow' % chan.lower(), 'Wide / Narrow', + RadioSettingValueList(['Wide', 'Narrow'], + ['Wide', 'Narrow'][self._memobj.settings_vfo['vfo_'+chan.lower()].narrow]))) + ret.append(RadioSetting('settings_vfo.vfo_%s.freqstep' % chan.lower(), 'Tuning Step', + RadioSettingValueList(self.TUNING_STEPS_LIST, + self.TUNING_STEPS_LIST[self._memobj.settings_vfo['vfo_'+chan.lower()].freqstep]))) + return ret + + def _get_custom_channel_names(self): + ret = RadioSettingGroup('ccn', 'Custom Channel Names') + msgs = [ "Add custom chan names to radio", + "-> Menu 09 CH-NAME", + "Allowed chars (ASCII only)", + "Input from 0 to 10 characters." + ] + for msg in msgs: + rsvs = RadioSettingValueString(0, 255, msg, autopad=False) + rsvs.set_mutable(False) + rs = RadioSetting('dummy_cnames_msg_%i' % msgs.index(msg), 'Input rule %i' % int(msgs.index(msg)+1), rsvs) + ret.append(rs) + for i in range(0, len(self._memobj.custom_channel_names)): + tmp = ''.join([ str(j) for j in self._memobj.custom_channel_names[i].name + if ord(str(j)) < 0xFF and ord(str(j)) > 0x00]) + ret.append( RadioSetting( 'custom_channel_names@name@%i' % i, + 'Custom Channel Name (%i)' % i, + RadioSettingValueString(0, 10, tmp, autopad=True, + charset=self.FULL_CHARSET_ASCII))) + return ret + + def _get_custom_ani_names(self): + ret = RadioSettingGroup('can', 'Custom ANI Names') + msgs = [ "Can be used as radio id in GPS.", + "Allowed chars (ASCII only)", + "Input from 0 to 10 characters." + ] + for msg in msgs: + rsvs = RadioSettingValueString(0, 255, msg, autopad=False) + rsvs.set_mutable(False) + rs = RadioSetting('dummy_caninames_msg_%i' % msgs.index(msg), 'Input rule %i' % int(msgs.index(msg)+1), rsvs) + ret.append(rs) + for i in range(0, len(self._memobj.custom_ani_names)): + tmp = ''.join([ str(j) for j in self._memobj.custom_ani_names[i].name + if ord(str(j)) < 0xFF and ord(str(j)) > 0x00]) + ret.append( RadioSetting( 'custom_ani_names@name@%i' % i, + 'Custom ANI Name (%i)' % (i+51), + RadioSettingValueString(0, 10, tmp, autopad=True, + charset=self.FULL_CHARSET_ASCII))) + return ret + + def _get_anicodes(self): + ret = RadioSettingGroup('ani', 'ANI Codes') + split = len(self._memobj.anicodes) - len(self._memobj.custom_ani_names) + msgs = [ "Allowed chars (%s)" % self.DTMFCHARS, + "Input from 0 to 6 characters." + ] + for msg in msgs: + rsvs = RadioSettingValueString(0, 255, msg, autopad=False) + rsvs.set_mutable(False) + rs = RadioSetting('dummy_canic_msg_%i' % msgs.index(msg), 'Input rule %i' % int(msgs.index(msg)+1), rsvs) + ret.append(rs) + + for i in range(0, split): + tmp = ''.join([ self.DTMFCHARS[int(j)] for j in self._memobj.anicodes[i].anicode if int(j) < 0xFF ]) + #LOG.debug("ANI Code (%i) '%s'" % (i, tmp) ) + ret.append( RadioSetting( 'anicodes@anicode@%i' % i, + 'ANI-ID (%i) Code' % (i+1), + RadioSettingValueString(0, 6, tmp, autopad=False, + charset=self.DTMFCHARS))) + for i in range(split, len(self._memobj.anicodes)): + tmp = ''.join([ self.DTMFCHARS[int(j)] for j in self._memobj.anicodes[i].anicode if int(j) < 0xFF ]) + tmp2 = ''.join([ str(j) for j in self._memobj.custom_ani_names[i-split].name + if ord(str(j)) < 0xFF and ord(str(j)) > 0x00]) + #LOG.debug("ANI Code (%s) (%i) '%s'" % (tmp2, i, tmp) ) + ret.append( RadioSetting( 'anicodes@anicode@%i' % i, + 'ANI-ID (%s) (%i) Code' % (tmp2, i+1), + RadioSettingValueString(0, 6, tmp, autopad=False, + charset=self.DTMFCHARS))) + return ret + + def _get_settings_adv(self): + ret = RadioSettingGroup('advanced', 'Advanced') + if RT490_EXPERIMENTAL: + ret.append(RadioSetting("settings.cha_memidx", "Channel A Memory index", + RadioSettingValueInteger(1, self._memory_size, int(self._memobj.settings.cha_memidx)+1))) + ret.append(RadioSetting("settings.chb_memidx", "Channel B Memory index", + RadioSettingValueInteger(1, self._memory_size, int(self._memobj.settings.chb_memidx)+1))) + ret.append(RadioSetting('settings.vox', 'VOX Sensitivity', + RadioSettingValueList(self.VOX_LIST, + self.VOX_LIST[self._memobj.settings.vox]))) + ret.append(RadioSetting('settings.vox_delay', 'VOX Delay', + RadioSettingValueList(self.VOXDELAYLIST, + self.VOXDELAYLIST[self._memobj.settings.vox_delay]))) + ret.append(RadioSetting('settings.tdr', 'Dual Receive (TDR)', + RadioSettingValueBoolean(self._memobj.settings.tdr))) + ret.append(RadioSetting('settings.txundertdr', 'Tx under TDR', + RadioSettingValueList(self.TDRTXMODES, + self.TDRTXMODES[self._memobj.settings.txundertdr]))) + ret.append(RadioSetting('settings.voice', 'Menu Voice Prompts', + RadioSettingValueBoolean(self._memobj.settings.voice))) + ret.append(RadioSetting('settings.scanmode', 'Scan Mode', + RadioSettingValueList(self.SCANMODES, + self.SCANMODES[self._memobj.settings.scanmode]))) + ret.append(RadioSetting('settings.bcl', 'Busy Channel Lockout', + RadioSettingValueBoolean(self._memobj.settings.bcl))) + ret.append(RadioSetting('settings.display_ani', 'Display ANI ID', + RadioSettingValueBoolean(self._memobj.settings.display_ani))) + ret.append(RadioSetting('settings.ani_id', 'ANI ID', + RadioSettingValueList(self.ANI_IDS, + self.ANI_IDS[self._memobj.settings.ani_id]))) + ret.append(RadioSetting('settings.alarm_mode', 'Alarm Mode', + RadioSettingValueList(self.ALARMMODES, + self.ALARMMODES[self._memobj.settings.alarm_mode]))) + ret.append(RadioSetting('settings.alarmsound', 'Alarm Sound', + RadioSettingValueBoolean(self._memobj.settings.alarmsound))) + ret.append(RadioSetting('settings.fmradio', 'Enable FM Radio', + RadioSettingValueList(self.FMRADIO, + self.FMRADIO[self._memobj.settings.fmradio]))) + if RT490_EXPERIMENTAL: + tmp = self._memobj.settings.fmpreset / 10.0 + if tmp < 65.0 or tmp > 108.0: + tmp = 80.0 + ret.append(RadioSetting("settings.fmpreset", "FM Radio Freq", + RadioSettingValueFloat(65, 108, tmp, resolution=0.1, precision=1))) + ret.append(RadioSetting('settings.kblock', 'Enable Keyboard Lock', + RadioSettingValueBoolean(self._memobj.settings.kblock))) + ret.append(RadioSetting('settings.autolock', 'Autolock Keyboard', + RadioSettingValueList(self.AUTOLOCK_TO, + self.AUTOLOCK_TO[self._memobj.settings.autolock]))) + ret.append(RadioSetting('settings.timer_menu_quit', 'Menu Exit Time', + RadioSettingValueList(self.MENUEXIT_TO, + self.MENUEXIT_TO[self._memobj.settings.timer_menu_quit]))) + ret.append(RadioSetting('settings.enable_gps', 'Enable GPS', + RadioSettingValueBoolean(self._memobj.settings.enable_gps))) + ret.append(RadioSetting('settings.scan_dcs', 'CDCSS Save Modes', + RadioSettingValueList(self.SCANDCSMODES, + self.SCANDCSMODES[self._memobj.settings.scan_dcs]))) + ret.append(RadioSetting('settings.tailnoiseclear', 'Tail Noise Clear', + RadioSettingValueBoolean(self._memobj.settings.tailnoiseclear))) + ret.append(RadioSetting('settings.rptnoiseclear', 'Rpt Noise Clear', + RadioSettingValueList(self.RPTNOISE, + self.RPTNOISE[self._memobj.settings.rptnoiseclear]))) + ret.append(RadioSetting('settings.rptnoisedelay', 'Rpt Noise Delay', + RadioSettingValueList(self.RPTNOISE, + self.RPTNOISE[self._memobj.settings.rptnoisedelay]))) + ret.append(RadioSetting('settings.rpttone', 'Rpt Tone', + RadioSettingValueList(self.RPTTONES, + self.RPTTONES[self._memobj.settings.rpttone]))) + return ret + + def _get_settings_basic(self): + ret = RadioSettingGroup('basic', 'Basic') + ret.append(RadioSetting('settings.squelch', 'Carrier Squelch Level', + RadioSettingValueList(self.SQUELCHLVLS, + self.SQUELCHLVLS[self._memobj.settings.squelch]))) + ret.append(RadioSetting('settings.savemode', 'Battery Savemode', + RadioSettingValueList(self.SAVEMODES, + self.SAVEMODES[self._memobj.settings.savemode]))) + ret.append(RadioSetting('settings.backlight', 'Backlight Timeout', + RadioSettingValueList(self.BACKLIGHT_TO, + self.BACKLIGHT_TO[self._memobj.settings.backlight]))) + ret.append(RadioSetting('settings.timeout', 'Timeout Timer (TOT)', + RadioSettingValueList(self.TOT_LIST, + self.TOT_LIST[self._memobj.settings.timeout]))) + ret.append(RadioSetting('settings.beep', 'Beep', + RadioSettingValueBoolean(self._memobj.settings.beep))) + ret.append(RadioSetting('settings.active_channel', 'Active Channel', + RadioSettingValueList(self.CHANNELS, + self.CHANNELS[self._memobj.settings.active_channel]))) + ret.append(RadioSetting('settings.cha_disp', 'Channel A Display Mode', + RadioSettingValueList(self.DISPLAYMODES, + self.DISPLAYMODES[self._memobj.settings.cha_disp]))) + ret.append(RadioSetting('settings.chb_disp', 'Channel B Display Mode', + RadioSettingValueList(self.DISPLAYMODES, + self.DISPLAYMODES[self._memobj.settings.chb_disp]))) + ret.append(RadioSetting('settings.roger', 'Roger Beep', + RadioSettingValueBoolean(self._memobj.settings.roger))) + ret.append(RadioSetting('settings.powermsg', 'Power Message', + RadioSettingValueList(self.POWERMESSAGES, + self.POWERMESSAGES[self._memobj.settings.powermsg]))) + ret.append(RadioSetting('settings.rx_time', 'Show RX Time', + RadioSettingValueBoolean(self._memobj.settings.rx_time))) + return ret + + def _get_memcodes(self): + ret = RadioSettingGroup('mc', 'Memory Channel Privacy Codes') + msgs = [ "Only hexadecimal chars accepted.", + "Allowed chars (%s)" % self.KEY_CHARS, + "Input from 0 to 6 characters. If code", + "length is less than 6 chars it will be", + "padded with leading zeros.", + "Ex: 1D32EB or 0F12 or AB521, etc...", + "Enable Code for the Location on the", + "'Other' tab in 'Memory Properties'." + ] + for msg in msgs: + rsvs = RadioSettingValueString(0, 255, msg, autopad=False) + rsvs.set_mutable(False) + rs = RadioSetting('dummy_memcodes_msg_%i' % msgs.index(msg), 'Input rule %i' % int(msgs.index(msg)+1), rsvs) + ret.append(rs) + for i in range(self._memory_size): + code = "" + if self._memobj.memcode[i].code[3] < 0xFF: + code = self._decode_key(self._memobj.memcode[i].code) + code = code.zfill(6) + rsvs = RadioSettingValueString(0, 6, code, autopad=False, + charset=self.KEY_CHARS) + rs = RadioSetting('memcode@code@%i' % i, + 'Memory Location (%i) Privacy Code' % int(i+1), rsvs) + ret.append(rs) + return ret + + def get_settings(self): + radio_settings = [] + basic = self._get_settings_basic() + radio_settings.append(basic) + adv = self._get_settings_adv() + radio_settings.append(adv) + vfoa = self._get_settings_vfo(self._memobj.settings_vfo.vfo_a, 'A') + radio_settings.append(vfoa) + vfob = self._get_settings_vfo(self._memobj.settings_vfo.vfo_b, 'B') + radio_settings.append(vfob) + sk = self._get_settings_sidekeys() + radio_settings.append(sk) + dtmf = self._get_settings_dtmf() + radio_settings.append(dtmf) + ccn = self._get_custom_channel_names() + radio_settings.append(ccn) + can = self._get_custom_ani_names() + radio_settings.append(can) + ani = self._get_anicodes() + radio_settings.append(ani) + mcodes = self._get_memcodes() + radio_settings.append(mcodes) + if RT490_EXPERIMENTAL: + ks = self._get_settings_ks() + radio_settings.append(ks) + bands = self._get_settings_bands() + radio_settings.append(bands) + top = RadioSettings(*radio_settings) + return top + + def get_raw_memory(self, number): + return repr(self._memobj.memory[number]) + + # TODO Add Code when RadioSettingValueString is fixed + def _get_extra(self, _mem, num): + group = RadioSettingGroup('extra', 'Extra') + #LOG.debug("Get extra %i" % num) + + s = RadioSetting('bcl', 'Busy Channel Lockout', + RadioSettingValueBoolean(_mem.bcl)) + group.append(s) + + dcp = int(_mem.dcp) + if dcp > len(self.FHSS_LIST)-1: + _mem.dcp = cur = 0 + LOG.debug('DCP ID / FHSS overflow for channel %d' % num) + cur = self.FHSS_LIST[dcp] + s = RadioSetting('dcp', 'FHSS (Encryption)', + RadioSettingValueList(self.FHSS_LIST, cur)) + group.append(s) + + # Does not work, no error, why ??? TODO + """ + code = "" + if self._memobj.memcode[num-1].code[3] < 0xFF: + code = self._decode_key(self._memobj.memcode[num-1].code) + code = code.zfill(6) + LOG.debug('CODE "%s"' % code) + s = RadioSetting('dcp_code', 'DCP code', + RadioSettingValueString(0, 6, code, + autopad=False, + charset=self.KEY_CHARS)) + group.append(s) """ + + pttid = int(_mem.pttid) + if pttid > len(self.PTTID)-1: + _mem.pttid = cur = 0 + LOG.debug('PTTID overflow for channel %d' % num) + cur = self.PTTID[pttid] + s = RadioSetting('pttid', 'Send DTMF Code (PTT ID)', + RadioSettingValueList(self.PTTID, cur)) + group.append(s) + + cur = self.SIGNAL[int(_mem.signal)] + s = RadioSetting('signal', 'PTT ID Code (S-Code)', + RadioSettingValueList(self.SIGNAL, cur)) + group.append(s) + + s = RadioSetting('learn', 'Use Memory Privacy Code as Tx/Rx DCS (Learn)', + RadioSettingValueBoolean(_mem.learn)) + group.append(s) + + return group + + # TODO Add Code when RadioSettingValueString is fixed + def _set_extra(self, _mem, mem): + memidx = mem.number - 1 + _mem.bcl = int(mem.extra['bcl'].value) + _mem.dcp = int(mem.extra['dcp'].value) + _mem.pttid = int(mem.extra['pttid'].value) + _mem.signal = int(mem.extra['signal'].value) + #self._memobj.memcode[mem.number].code = self._encode_key(mem.extra['dcp_code'].value) + if (int(mem.extra['learn'].value) > 0) and (self._memobj.memcode[mem.number-1].code[3] == 0xA0): + _mem.learn = 1 + elif (int(mem.extra['learn'].value) > 0) and (self._memobj.memcode[mem.number-1].code[3] != 0xA0): + _mem.learn = 0 + raise InvalidValueError(dedent("""\ + >>Use Memory Privacy Code as Tx/Rx DCS (Learn)<< requires + that a memory code has been previously set for this memory. + + Go in 'Settings' -> 'Memory Channel Privacy Codes' and set + a code for the current memory before enabling 'Learn'. + """)) + else: + _mem.learn = 0 + + def _is_txinh(self, _mem): + raw_tx = "" + for i in range(0, 4): + raw_tx += _mem.txfreq[i].get_raw() + return raw_tx == "\xFF\xFF\xFF\xFF" + + def get_memory(self, num): + memidx = num - 1 + _mem = self._memobj.memory[memidx] + _nam = self._memobj.memname[memidx] + + mem = chirp_common.Memory() + mem.number = num + if int(_mem.rxfreq) == 166666665: + mem.empty = True + return mem + + mem.name = ''.join([str(c) for c in _nam.name + if ord(str(c)) < 127]).rstrip() + mem.freq = int(_mem.rxfreq) * 10 + offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10 + if self._is_txinh(_mem) or _mem.tx_enable == 0: + mem.duplex = 'off' + #_mem.txfreq = _mem.rxfreq # TODO REMOVE (force fix broken saves) + elif offset == 0: + mem.duplex = '' + mem.offset = mem.freq + elif abs(offset) < 100000000: + mem.duplex = offset < 0 and '-' or '+' + mem.offset = abs(offset) + else: + mem.duplex = 'split' + mem.offset = int(_mem.txfreq) * 10 + + mem.power = self.POWER_LEVELS[_mem.power] + + if _mem.unknown3_0 and _mem.narrow: + mem.mode = 'NAM' + elif _mem.unknown3_0 and not _mem.narrow: + mem.mode = 'AM' + elif not _mem.unknown3_0 and _mem.narrow: + mem.mode = 'NFM' + elif not _mem.unknown3_0 and not _mem.narrow: + mem.mode = 'FM' + else: + LOG.exception('Failed to get mode for %i' % num) + + mem.skip = '' if _mem.scan else 'S' + + #LOG.warning('got txtone: %s' % repr(self._decode_tone(_mem.txtone))) + #LOG.warning('got rxtone: %s' % repr(self._decode_tone(_mem.rxtone))) + txtone = self._decode_tone(_mem.txtone) + rxtone = self._decode_tone(_mem.rxtone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + try: + mem.extra = self._get_extra(_mem, num) + except: + LOG.exception('Failed to get extra for %i' % num) + return mem + + def set_memory(self, mem): + memidx = mem.number - 1 + _mem = self._memobj.memory[memidx] + _nam = self._memobj.memname[memidx] + + if mem.empty: + _mem.set_raw(b'\xff' * 16) + _nam.set_raw(b'\xff' * 16) + return + + if int(_mem.rxfreq) == 166666665: + LOG.debug('Initializing new memory %i' % memidx) + _mem.set_raw(b'\x00' * 16) + + _nam.name = mem.name.ljust(12, chr(255)) # with xFF pad (mimic factory behavior) + + _mem.rxfreq = mem.freq // 10 + _mem.tx_enable = 1 + if mem.duplex == '': + _mem.txfreq = mem.freq // 10 + elif mem.duplex == 'split': + _mem.txfreq = mem.offset // 10 + elif mem.duplex == 'off': + _mem.tx_enable = 0 + _mem.txfreq = mem.freq // 10 # Optional but keeps compat with vendor software + elif mem.duplex == '-': + _mem.txfreq = (mem.freq - mem.offset) // 10 + elif mem.duplex == '+': + _mem.txfreq = (mem.freq + mem.offset) // 10 + else: + raise errors.RadioError('Unsupported duplex mode %r' % mem.duplex) + + txtone, rxtone = chirp_common.split_tone_encode(mem) + #LOG.warning('tx tone is %s' % repr(txtone)) + #LOG.warning('rx tone is %s' % repr(rxtone)) + _mem.txtone = self._encode_tone(*txtone) + _mem.rxtone = self._encode_tone(*rxtone) + + try: + _mem.power = self.POWER_LEVELS.index(mem.power) + except ValueError: + _mem.power = 0 + + if int(_mem.rxfreq) < 30000000: + _mem.unknown3_0 = mem.mode in [ 'AM', 'NAM' ] + else: + _mem.unknown3_0 = 0 + _mem.narrow = mem.mode[0] == 'N' + + _mem.scan = mem.skip != 'S' + + if mem.extra: + self._set_extra(_mem, mem) + + def sync_out(self): + try: + do_upload(self) + except errors.RadioError: + raise + except Exception as e: + LOG.exception('General failure') + raise errors.RadioError('Failed to upload to radio: %s' % e) + + def sync_in(self): + self._mmap = do_download(self) + self.process_mmap() + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT_RT490 % + { "memsize": self._memory_size }, self._mmap) + + def get_features(self): # GOOD ? + rf = chirp_common.RadioFeatures() + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_bank = False + rf.has_tuning_step = True + rf.has_cross = True + rf.has_name = True + rf.has_settings = True + rf.valid_modes = self.VALID_MODES + rf.valid_tmodes = [ "", "Tone", "TSQL", "DTCS", "Cross" ] + rf.valid_duplexes = [ '', "+", "-", 'split', 'off'] + rf.valid_cross_modes = [ 'Tone->Tone', 'DTCS->', '->DTCS', 'Tone->DTCS', + 'DTCS->Tone', '->Tone', 'DTCS->DTCS' ] + rf.valid_tuning_steps = self.TUNING_STEPS + rf.valid_bands = self.VALID_BANDS + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_name_length = 12 + rf.memory_bounds = (1, self._memory_size) + rf.can_odd_split = True + return rf + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.pre_upload = _(dedent("""\ + This driver is in development and should be considered + as experimental. + """)) + rp.experimental = _(dedent("""\ + This driver is in development and should be considered + as experimental. + """)) + rp.info = _(dedent("""\ + This driver is in development and should be considered + as experimental. + """)) + return rp + + def _encode_key(self, key): + arr = bytearray(4) + arr[3] = 160 + arr[2] = self.KEY_CHARS.index(key[0]) #<< 4 + arr[2] = arr[2]<<4 + arr[2] |= self.KEY_CHARS.index(key[1]) + arr[1] = self.KEY_CHARS.index(key[2]) #<< 4 + arr[1] = arr[1]<<4 + arr[1] |= self.KEY_CHARS.index(key[3]) + arr[0] = self.KEY_CHARS.index(key[4]) #<< 4 + arr[0] = arr[0]<<4 + arr[0] |= self.KEY_CHARS.index(key[5]) + return arr + + def _decode_key(self, key): + ret = "" + if key[3] == 0xA0: + ret += self.KEY_CHARS[key[2] >> 4] + ret += self.KEY_CHARS[key[2] & 0xF] + ret += self.KEY_CHARS[key[1] >> 4] + ret += self.KEY_CHARS[key[1] & 0xF] + ret += self.KEY_CHARS[key[0] >> 4] + ret += self.KEY_CHARS[key[0] & 0xF] + LOG.debug('DCP Code: "%s"' % ret) + return ret + + def _decode_tone(self, toneval): + if toneval in (0, 0xFFFF): + #LOG.debug('no tone value: %s' % toneval) + return None, None, None + elif toneval < 670: + toneval = toneval - 1 + index = toneval % len(RT490Radio.DTCS_CODES) + if index != int(toneval): + pol = 'R' + # index -= 1 + else: + pol = 'N' + return 'DTCS', RT490Radio.DTCS_CODES[index], pol + else: + return 'Tone', toneval / 10.0, 'N' + + def _encode_tone(self, mode, val, pol): + if not mode: + return 0x0000 + elif mode == 'Tone': + return int(val * 10) + elif mode == 'DTCS': + index = RT490Radio.DTCS_CODES.index(val) + if pol == 'R': + index += len(RT490Radio.DTCS_CODES) + index += 1 + #LOG.debug('Encoded dtcs %s/%s to %04x' % (val, pol, index)) + return index + else: + raise errors.RadioError('Unsupported tone mode %r' % mode) + +class MML8629Alias(chirp_common.Alias): + VENDOR = "MMLradio" + MODEL = "JC-8629" + +class JJCC8629Alias(chirp_common.Alias): + VENDOR = "JJCC" + MODEL = "JC-8629" + +class SocotranJC8629Alias(chirp_common.Alias): + VENDOR = "Socotran" + MODEL = "JC-8629" + +class SocotranFB8629Alias(chirp_common.Alias): + VENDOR = "Socotran" + MODEL = "FB-8629" + +class Jianpai8629Alias(chirp_common.Alias): + VENDOR = "Jianpai" + MODEL = "8800 Plus" + +class Boristone8RSAlias(chirp_common.Alias): + VENDOR = "Boristone" + MODEL = "8RS" + +class AbbreeAR869Alias(chirp_common.Alias): + VENDOR = "Abbree" + MODEL = "AR-869" + +class HamGeekHG590Alias(chirp_common.Alias): + VENDOR = "HamGeek" + MODEL = "HG-590" + +@directory.register +class RT490RadioGeneric(RT490Radio): + ALIASES = [ MML8629Alias, JJCC8629Alias, SocotranJC8629Alias, + SocotranFB8629Alias, Jianpai8629Alias, Boristone8RSAlias, + AbbreeAR869Alias, HamGeekHG590Alias ] + + +IMHEX_DESC = """ +// Perfect tool for binary reverse engineering +// https://github.com/WerWolv/ImHex +// Below is the pattern +""" +IMHEX_PATTERN = """ +struct memory { // Memory settings + u8 rxfreq[4]; + u8 txfreq[4]; + u16 rxtone; + u16 txtone; + u8 signal; // int 0->14, Signal 1->15 + u8 pttid; // [ 'OFF', 'BOT', 'EOT', 'Both'] + u8 dcp_power; // POWER_LEVELS + u8 unknown3_0_narrow_unknown3_1_bcl_scan_tx_enable_learn; // bool ??? TODO +}; +struct memname { + char name[12]; + padding[4]; +}; +struct memcode { + u8 code[4]; +}; +struct custom_ani_names { + char name[12]; + padding[4]; +}; +struct anicodes { + u8 anicode[6]; + padding[10]; +}; +struct custom_channel_names { + char name[12]; + padding[4]; +}; +bitfield workmode { + b : 4; + a : 4; +}; +struct settings { + u8 squelch; // 0: int 0 -> 9 + u8 savemode; // 1: [ 'OFF', 'Normal', 'Super', 'Deep' ] + u8 vox; // 2: off=0, 1 -> 9 + u8 backlight; // 3: [ 'OFF', '5s', '15s', '20s', '30s', '1m', '2m', '3m' ] + u8 tdr; // 4: bool + u8 timeout; // 5: n*30seconds, 0-240s + u8 beep; // 6: bool + u8 voice; // 7: bool + u8 byte_not_used_10; // 8: Allways 1 + u8 dtmfst; // 9: [ 'OFF', 'KB Side Tone', 'ANI Side Tone', 'KB ST + ANI ST' ] + u8 scanmode; // 10: [ 'TO', 'CO', 'SE' ] + u8 pttid; // 11: [ 'OFF', 'BOT', 'EOT', 'Both'] + u8 pttiddelay; // 12: [ '0', '100ms', '200ms', '400ms', '600ms', '800ms', '1000ms' ] + u8 cha_disp; // 13: [ 'Name', 'Freq', 'Channel ID' ] + u8 chb_disp; // 14: [ 'Name', 'Freq', 'Channel ID' ] + u8 bcl; // 15: bool + u8 autolock; // 0: [ 'OFF', '5s', '10s', 15s' ] + u8 alarm_mode; // 1: [ 'Site', 'Tone', 'Code' ] + u8 alarmsound; // 2: bool + u8 txundertdr; // 3: [ 'OFF', 'A', 'B' ] + u8 tailnoiseclear; // 4: [off, on] + u8 rptnoiseclear; // 5: n*100ms, 0-1000 + u8 rptnoisedelay; // 6: n*100ms, 0-1000 + u8 roger; // 7: bool + u8 active_channel; // 8: 0 or 1 + u8 fmradio; // 9: boolean, inverted + workmode _workmode; // 10:up [ 'VFO', 'CH Mode' ] + u8 kblock; // 11: bool // TODO TEST WITH autolock + u8 powermsg; // 12: 0=Image / 1=Voltage + u8 byte_not_used_21; // 13: Allways 0 + u8 rpttone; // 14: [ '1000Hz', '1450Hz', '1750Hz', '2100Hz' ] + u8 byte_not_used_22; // 15: pad with xFF + u8 vox_delay; // 0: [str(float(a)/10)+'s' for a in range(5,21)] '0.5s' to '2.0s' + u8 timer_menu_quit; // 1: [ '5s', '10s', '15s', '20s', '25s', '30s', '35s', '40s', '45s', '50s', '60s' ] + u8 byte_not_used_30; // 2: pad with xFF + u8 byte_not_used_31; // 3: pad with xFF + u8 enable_killsw; // 4: bool + u8 display_ani; // 5: bool + u8 byte_not_used_32; // 6: pad with xFF + u8 enable_gps; // 7: bool + u8 scan_dcs; // 8: [ 'All', 'Receive', 'Transmit' ] + u8 ani_id; // 9: int 0-59 (ANI 1-60) + u8 rx_time; // 10: bool + padding[5]; // 11: Pad xFF + u8 cha_memidx; // 0: Memory index when channel A use memories + u8 byte_not_used_40; + u8 chb_memidx; // 2: Memory index when channel B use memories + u8 byte_not_used_41; + padding[10]; + u16 fmpreset; +}; +struct settings_vfo_chan { + u8 rxfreq[8]; // 0 + u16 rxtone; // 8 + u16 txtone; // 10 + u16 byte_not_used0; // 12 Pad xFF + u8 sftd_signal; // 14 int 0->14, Signal 1->15 + u8 byte_not_used1; // 15 Pad xFF + u8 power; // 16:0 POWER_LEVELS + u8 fhss_narrow; // 17 bool true=NFM false=FM + u8 byte_not_used2; // 18 Pad xFF but received 0x00 ??? + u8 freqstep; // 19:3 [ '2.5 KHz', '5.0 KHz', '6.25 KHz', '10.0 KHz', '12.5 KHz', '20.0 KHz', '25.0 KHz', '50.0 KHz' ] + u8 byte_not_used3; // 20:4 Pad xFF but received 0x00 ??? TODO + u8 offset[6]; // 21:5 Freq NN.NNNN (without the dot) TEST TEST + u8 byte_not_used4; // 27:11 Pad xFF + u8 byte_not_used5; // 28 Pad xFF + u8 byte_not_used6; // 29 Pad xFF + u8 byte_not_used7; // 30 Pad xFF + u8 byte_not_used8; // 31:15 Pad xFF +}; +struct settings_vfo { + settings_vfo_chan vfo_a; + settings_vfo_chan vfo_b; +}; +struct settings_sidekeys { // Values from Radio + u8 pf2_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan', '29': 'Search, '1': 'PPT B' } + u8 pf2_long; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan', '29': 'Search' } + u8 pf3_short; // { '7': 'FM', '10': 'Tx Power', '28': 'Scan', '29': 'Search' } + u8 ffpad; // Pad xFF +}; +struct dtmfcode { + u8 code[5]; // 5 digits DTMF + padding[11]; // Pad xFF +}; +struct settings_dtmf { // @15296+3x16 + u8 byte_not_used1; // 0: Pad xFF + u8 byte_not_used2; // 1: Pad xFF + u8 byte_not_used3; // 2: Pad xFF + u8 byte_not_used4; // 3: Pad xFF + u8 byte_not_used5; // 4: Pad xFF + u8 unknown_dtmf; // 5: 0 TODO ???? wtf is alarmcode/alarmcall TODO + u8 pttid; // 6: [off, BOT, EOT, Both] + u8 dtmf_speed_on; // 7: ['50ms', '100ms', '200ms', '300ms', '500ms'] + u8 dtmf_speed_off; // 8:0 ['50ms', '100ms', '200ms', '300ms', '500ms'] +}; +struct settings_dtmf_global { + dtmfcode settings_dtmfgroup[15]; + settings_dtmf _settings_dtmf; +}; +struct settings_killswitch { + u8 kill_dtmf[6]; // 0: Kill DTMF + padding[2]; // Pad xFF + u8 revive_dtmf[6]; // 8: Revive DTMF + padding[2]; // Pad xFF +}; +struct management_settings { + u8 unknown_data_0[16]; + u8 unknown_data_1; + u8 active; // Bool radio killed (killed=0, active=1) + padding[46]; +}; +struct band { + u8 enable; // 0 bool / enable-disable Tx on band + u8 freq_low[2]; // 1 lowest band frequency + u8 freq_high[2]; // 3 highest band frequency +}; +struct settings_bands { + band band136; // 0 Settings for 136MHz band + band band400; // 5 Settings for 400MHz band + band band200; // 10 Settings for 200MHz band + padding[1]; // 15 + band band350; // 0 Settings for 350MHz band + padding[43];// 5 +}; + +memory mem[256] @ 0x0000; +memname mname[256] @ 0x1000; +memcode mcode[256] @ 0x2000; +custom_ani_names caninames[10] @ 0x3400; +anicodes anic[60] @ 0x3500; +custom_channel_names ccnames[10] @ 0x3900; +settings _settings @ 0x3A00; +settings_vfo _settings_vfo @ 0x3A40; +settings_sidekeys _settings_sidekeys @ 0x3A80; +settings_dtmf_global _settings_dtmf_global @ 0x3B00; +settings_killswitch _settings_killswitch @ 0x3C00; +management_settings _management_settings @ 0x3F80; +settings_bands _settings_bands @ 0x3FC0; +""" \ No newline at end of file