diff --git a/chirp/drivers/tk280.py b/chirp/drivers/tk280.py new file mode 100644 index 00000000..c828c0fa --- /dev/null +++ b/chirp/drivers/tk280.py @@ -0,0 +1,1774 @@ +# Copyright 2016 Pavel Milanes, CO7WT, +# Copyright 2024 Dan Smith +# +# 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 . + +import collections +import logging +import struct +import time + +from itertools import chain +from chirp import chirp_common, directory, memmap, errors, util, bitwise +from chirp.settings import RadioSettingGroup, RadioSetting, \ + RadioSettingValueBoolean, RadioSettingValueList, \ + RadioSettingValueString, RadioSettingValueInteger, \ + RadioSettings, MemSetting, RadioSettingValueInvertedBoolean, \ + RadioSettingValueMap, RadioSettingSubGroup + +LOG = logging.getLogger(__name__) + +# IMPORTANT DATA: +# This radios have a span of +# 0x00000 - 0x07FFF => Radio Memory / Settings data +# 0x08000 - => FIRMWARE... hum... + +# Notes: March 5 2024 +# This file was heavily modified from tk760g.py for use with TK-380 by +# Thomas P. It is far from perfect but the main objective was to map a +# Kenwood x80 series to help further development. If radio is programmed in +# trunked mode, this module won't work as the mem map changes drastically. +# Stick to Conventional. +# FYI: Tk-981, 980, 480 and 481 are ONLY trunked mode so have different mem +# map. +# +# I believe the memmap is complete-ish but most likely needs corrections +# (data types, etc) if planning on completing a functional UI section. +# +# Many features are missing from the UI. Such as... +# 2-Tone, FleetSync, Emergency Information, Operator Selectable Tone, +# Test Frequency +# +# Some features are shown in UI but incomplete. Such as... +# Scan Information, DTMF +# +# + +MEM_FORMAT = """ +#seekto 0x0000; +struct { + // x00-x01, Edit>>Model Information>>Radio Format, + // '03 00':Conventional Format, '00 03':Trunked Format + u8 format[2]; + u8 unknown0[12]; // unknown, all xFF in UNPROGRAM mode + u8 groups; // How many banks are programmed + u8 channels; // How many total channels are programmed + ul16 tot; // TOT value: range(15, 600, 15); x04b0 = off + u8 tot_rekey; // TOT Re-key value range(0, 60); off= 0 + u8 unknown1; // unknown + u8 tot_reset; // TOT Re-key value range(0, 60); off= 0 + u8 unknown2; // unknows + u8 tot_alert; // TOT pre alert: range(0,10); 0 = off + u8 unknown3[7]; // 1d, unknown + u8 sql_level; // SQ reference level + u8 battery_save; // Portable: FF=off,32=Long,30=Short,31=Middle + u8 sc_nfo_priority; // Scan '31'=Selected, '30'=Fixed, 'FF'=None + ul16 sc_nfo_lbt_a; // Scan Look Back Time A[sec],range(0.35, 5.0, .05) + ul16 sc_nfo_lbt_b; // Scan Look Back Time B[sec],range(0.5, 5.0, .05) + ul16 sc_nfo_ddtime; // Scan Dropout Delay Time[sec],range(0, 300, 1) + ul16 sc_nfo_dwell; // Scan Dwell Time[sec], range(0, 300, 1) + // Scan Revert Ch 30=Last Called, 31=Last Used, 34=Priority, + // 35=Priority+TalkBack) + u8 sc_nfo_revert; + u8 sc_nfo_grp_scan; // 8th bit, Scan Info, Group, 30=Single, 31=Multi + u8 sc_nfo_prio_grp; // Scan Priority Group, [None,1-32?] + u8 sc_nfo_prio_ch; // Scan Priority Channel, [None,1-128?] + u8 unknown5:2, // unknown + ptt_release_tone:1, // PTT Release Tone, '1'=off, '0'=on + sc_nfo_rev_disp:1, // Scan Revert Ch Display, '1'=disable, '0'=enable + c2t:1, // Clear to transpond: 1=off, + ost_direct:1, // Operator Sel Tone, Direct (1=disable, 0=enable) + ost_backup:1, // Operator Sel Tone, Back Up (1=disable, 0=enable) + unknown117:1; // unknown + u8 unknown113[2]; // 2 bytes, unknown + u8 2t_a_tone_x[6]; // 2-Tone, A Tone [Hz], (41 24 FB 24 87 23) + u8 2t_b_tone_x[6]; // 2-Tone, B Tone [Hz], (41 24 FB 24 87 23) + u8 unknown8[4]; + u8 unknown9[16]; + u8 unknown10[16]; + u8 add[16]; //corresponding chan add/skip values, UNCONFIRMED + u8 unknown11[16]; + u8 unknown12:1, + ptt_inhib_ta:1, // Inhibit PTT ID in TA(TalkAround), 1=off + sel_call_alert_led:1, // Sel Call Alert LED, '1'=enable, '0'=disable + battery_warn:1, // Battery Warning, '0'=enable, '1'=disable + off_hook_decode:1, // off hook decode enabled: 1-off + off_hook_horn_alert:1, // off hook horn alert: 1-off + busy_led:1, // Busy LED, '0'=enable, '1'=disable + disp_char:1; // Opt 1, Display Character (1=ChName, 0=Grp#/Ch#) + u8 unknown14; + u8 unknown15:3, + self_prog:1, // Self programming enabled: 1-on + clone:1, // clone enabled: 1-on + firmware_prog:1, // firmware programming enabled: 1-on + panel_tuning:1, // Panel Tuning, 1-on, Requires panel_test=on + panel_test:1; // Panel test enabled, 1-on + u8 unknown17; + u8 unknown18:5, + warn_tone:1, // warning tone, enabled: 1-on + control_tone:1, // control tone (key tone), enabled: 1-on + poweron_tone:1; // power on tone, enabled: 1-on + u8 unknown19[5]; + u8 min_vol; // minimum volume posible: range(0,32); 0 = off + u8 tone_vol; // minimum tone volume FF=Continuous, range(0, 31) + u8 sub_lcd_disp; // Sub LCD Display (FF=none, 30=Group, 31=Channel) + u8 grp_name_len; // Group Name text length (0-10) + u8 unknown119[2]; // unknown + u8 unknown21:3, // unknown + access_log_sig:1, // Access Logic Signal, '1'=Continuous, '0'=Pulse + sq_logic_sig:1, // Squelch Logic Signal, '1'=COR, '0'=TOR + unknown111:1, + access_log_type:1, // Access Logic, '1'=Active Low, '0'=Active High + sq_logic_type:1; // Squelch Logic, '1'=Active Low, '0'=Active High + u8 unknown106:4, + em_type:2, // Emergency, '11'=None, '10'=DTMF, '00'=FleetSync + em_mode_type:1, // Emergency Mode Type, '1'=Silent, '0'=Audible + em_display:1; // Emergency Display, 'FE'=Revert, 'FF'=Text' + u8 unknown107[2]; + char poweronmesg[12]; // Power on mesg 12 bytes, off is "xFF" * 12 + u8 unknown23[6]; + u8 unknown133; // FleetSync Enhanced, 00:enable, FF:disable + char ident[8]; // radio identification string + u8 unknown26[12]; // Passwords, see passwords struct below + char lastsoftversion[5]; // software version employed to program the radio + char dtmf_prim_code[7]; // DTMF Decode, Primary Code + char dtmf_sec_code[7]; // DTMF Decode, Secondary Code + char dtmf_DBD_code[7]; // DTMF Decode, Dead Beat Disable Code + char ptt_id_bot[16]; // PTT ID Begin of TX + char ptt_id_eot[16]; // PTT ID End of TX + ul16 d_auto_r_timer; // DTMF Auto Reset Timer [sec], 'Off', '0-300' + u8 d_enc_dig_time; // DTMF Encode Digit Time [dig/sec], 6,8,10,15 + u8 unknown100; + ul16 d_enc_first_d; // DTMF Encode First Digit [msec], 0,100,500,1000 + ul16 d_enc_sym_d; // DTMF Encode * and # Digit [msec], 0,100,500,1000 + u8 d_dir_access:1, // DTMF Encode * Direct, '1'=disable, '0'=enable + unknown101:1, + d_store_send:1, // DTMF Enc Store and Send, '1'=disable, '0'=enable + d_man_dial:1, // DTMF Manual Dial, '1'=enable, '0'=disable + d_side_tone:1, // DTMF Side Tone, '1'=enable, '0'=disable + d_db_dis_resp:1, // DTMF Dead Beat Disable, 1=TX Inh, 0=TX/RX Inh + d_sec_dec_resp:1, // DTMF Secondary Decode, 1=Alert, 0=Transpond + d_prim_dec_resp:1; // DTMF Primary Decode, 1=Alert, 0=Transpond + u8 unknown102:1, + d_call_alert:1, // DTMF Call Alert, '1'=Normal, '0'=Continuous + // Decode Response, changes to '1' when "Alert+Transpond" selected + unknown103:1, // Secondary + unknown132:1, // Primary + d_kp_auto_ptt:1, // DTMF Keypad Auto-PTT, '0'=enable, '1'=disable + ptt_id_when:2, // PTT ID, '10'=BOT, '01'=EOT, '00'=Both + signalling_type:1; //Signalling, '1'=OR, '0'=AND +} settings; + +#seekto 0xA7; +struct { +// 0xA7, M or P... trying to work around absent model and type in tk-380 +// but doesn't work. Still need the id struct. + char type; +} id; + +#seekto 0xAF; +struct { + char radio[6]; // 0xAF-0xB4, 6 digit Radio Password + char data[6]; // 0xB5-0xBA, 6 digit Data Password +} passwords; + +//These are ALL the keys on TK-380 keypad version. These locations are assigned +// functions from values in KEYS below +#seekto 0x0110; +struct { + u8 kA; // A button + u8 kLEFT; // Triangle to Left + u8 kRIGHT; // Triangle to Right + u8 kSIDE1; // Side button 1 (lamp) + u8 kSCN; // S switch + u8 kMON; // Side button 2 (mon) + u8 kORANGE; // Orange button on portable, Foot Switch on mobile +// PF1(ORANGE) on portable, Group Up (Right Side Up Arrow) on mobile + u8 kPF1; +// PF2(BLACK) on portable, Group Down (Right Side Down Arrow) on mobile + u8 kPF2; + u8 kVOL_UP; // Volume Up (Left Side Up Arrow), Mobile only + u8 kVOL_DOWN; // Volume Down (Left Side Down Arrow), Mobile only + u8 unknown30[9]; // unknown + u8 kP_KNOB; // Just portable: channel knob + u8 unknown131[4]; // unknown + u8 unknown31[7]; // unknown + u8 k0; // Numkey 0 + u8 k1; // Numkey 1 + u8 k2; // Numkey 2 + u8 k3; // Numkey 3 + u8 k4; // Numkey 4 + u8 k5; // Numkey 5 + u8 k6; // Numkey 6 + u8 k7; // Numkey 7 + u8 k8; // Numkey 8 + u8 k9; // Numkey 9 + u8 unknown130[4]; // Unknown + u8 kASTR; // Numkey * + u8 kPOUND; // Numkey # +} keys; + +#seekto 0x0140; +struct { + lbcd tf01_rx[4]; + lbcd tf01_tx[4]; + u8 tf01_u_rx; + u8 tf01_u_tx; + lbcd tf02_rx[4]; + lbcd tf02_tx[4]; + u8 tf02_u_rx; + u8 tf02_u_tx; + lbcd tf03_rx[4]; + lbcd tf03_tx[4]; + u8 tf03_u_rx; + u8 tf03_u_tx; + lbcd tf04_rx[4]; + lbcd tf04_tx[4]; + u8 tf04_u_rx; + u8 tf04_u_tx; + lbcd tf05_rx[4]; + lbcd tf05_tx[4]; + u8 tf05_u_rx; + u8 tf05_u_tx; + lbcd tf06_rx[4]; + lbcd tf06_tx[4]; + u8 tf06_u_rx; + u8 tf06_u_tx; + lbcd tf07_rx[4]; + lbcd tf07_tx[4]; + u8 tf07_u_rx; + u8 tf07_u_tx; + lbcd tf08_rx[4]; + lbcd tf08_tx[4]; + u8 tf08_u_rx; + u8 tf08_u_tx; + lbcd tf09_rx[4]; + lbcd tf09_tx[4]; + u8 tf09_u_rx; + u8 tf09_u_tx; + lbcd tf10_rx[4]; + lbcd tf10_tx[4]; + u8 tf10_u_rx; + u8 tf10_u_tx; + lbcd tf11_rx[4]; + lbcd tf11_tx[4]; + u8 tf11_u_rx; + u8 tf11_u_tx; + lbcd tf12_rx[4]; + lbcd tf12_tx[4]; + u8 tf12_u_rx; + u8 tf12_u_tx; + lbcd tf13_rx[4]; + lbcd tf13_tx[4]; + u8 tf13_u_rx; + u8 tf13_u_tx; + lbcd tf14_rx[4]; + lbcd tf14_tx[4]; + u8 tf14_u_rx; + u8 tf14_u_tx; + lbcd tf15_rx[4]; + lbcd tf15_tx[4]; + u8 tf15_u_rx; + u8 tf15_u_tx; + lbcd tf16_rx[4]; + lbcd tf16_tx[4]; + u8 tf16_u_rx; + u8 tf16_u_tx; +} test_freq; + +#seekto 0x1E7; +struct { + u8 d_enc_hold_time; // DTMF Encode Hold Time [sec], Off, 0.5-2.0, .1 incr + u8 unknown120; // unknown + u8 ptt_id_type; // PTT ID Type, '00'=DTMF, '01'=FleetSync, NOT emergency + u8 unknown112; // unknown + // Com 0(Accessory Connector), FF=none, 33=rem, 30=Data, + // 35=Data+GPS NOT related to emergency + u8 com_0; + // Com 1(Internal Port), FF=none, 33=rem, 36=man down in, 31=GPS, + // NOT related to emergency but convenient to put here + u8 com_1; + // Com 2(Internal Port), FF=none, 33=rem, 36=man down in, 31=GPS, + // 32=AUX Hook/PTT, 34=Data PTT, 35=Data+GPS + u8 com_2; + u8 em_group; // Emergency Group + u8 em_chan; // Emergency Channel + ul16 em_key_delay; // Emergency Key Delay Time [sec] Off, .1-5.0 in .1 incr + u8 em_active_time; // Emergency Active time [sec], 1-60 + u8 unknown_105; // unknown + u8 em_int_time; // Emergency Interval Time [sec], 30-180 + u8 unknown107; // unknown + char em_text[10]; // Emergency Text + char line1[32]; // Embedded Message Line 1 + char line2[32]; // Embedded Message Line 2 + u8 em_dtmf_id[16]; // x240-x24F, 16 bytes, Emergency DTMF ID +} misc; + +#seekto 0x0300; +struct { + u8 group; // Group number + u8 number; // Memory number in zone + u8 group_index;// The *index* of the group record + u8 index; // Index of memory in array +} group_mapping[250]; + +#seekto 0x700; +struct { + ul16 ost_dec_tone; // Operator Selectable Tones, QT/DQT Decode + ul16 ost_enc_tone; // Operator Selectable Tones, QT/DQT Encode + char ost_name[10]; // Operator Selectable Tones, OST Name + u8 unknown118[2]; // unknown +} ost[16]; + +#seekto 0x800; +struct { + u8 2t_a_tone_y[4]; // 2t_a_tone_x and 2t_a_tone_z + u8 2t_b_tone_y[4]; // 2t_b_tone_x and 2t_b_tone_z + u8 2t_c_tone_y[4]; // 2t_c_tone_z + u8 2t_a_tone_z[2]; // 2t_a_tone_x and 2t_a_tone_y + u8 2t_b_tone_z[2]; // 2t_b_tone_x and 2t_b_tone_y + u8 2t_c_tone_z[2]; // 2t_c_tone_y + // Decoder1 Call Format, (30=A-B, 31=A-C, 32=C-B, 33=A, 34=B, 35=C) + u8 2t_dec1_ca_form; + // Decoder2 Call Format, (FF=None, 30=A-B, 31=A-C, 32=C-B, 33=A, 34=B, 35=C) + u8 2t_dec2_ca_form; + u8 2t_call_alert; // 2-Tone, Call Alert, (FF=no, x30=Normal, x31=Cont) + u8 2t_ar_timer[2]; // 2-Tone, Auto Reset[sec], range(Off, 1-300) incr of 1 + u8 unknown115:5, + 2t_transpond:1, // Transpond (1=disable, 0=enable) + 2t_dec2_c_type:1, // Decoder2 Call Type, (1=Individual, 0=Group) + 2t_dec1_c_type:1; // Decoder1 Call Type, (1=Individual, 0=Group) + u8 unknown116[8]; + +} 2tone[3]; // Attempt at capturing 2-Tone 1, 2 and 3 + +#seekto 0x1000; +struct { + u8 number; // Group Number + u8 channels; // Channels in Group + char name[10]; // Group Name + u8 data_grp; // Data Group + u8 data_chan; // Data Channel + u8 unknown108; + u8 unknown109; +} groups[250]; + +#seekto 0x2000; +struct { + u8 number; // Channel Number + u8 group; // to which bank/group it belongs + char name[10]; // Channel name, 10 chars + lbcd rxfreq[4]; // rx freq + lbcd txfreq[4]; // tx freq + u8 rx_unkw; // unknown yet + u8 tx_unkw; // unknown yet + ul16 rx_tone; // rx tone + ul16 tx_tone; // tx tone + u8 unknown23[5]; // unknown yet + u8 signalling; // Option Sig, FF=off, 30=DTMF, 32=2-Tone 1,32=FleetSync, + u8 unknown24:1, // unknown + ptt_id:1, // PTT ID, '1'=off, '0'=on + beat_shift:1, // Beat Shift, 1 = off + busy_lock:1, // BCL, 1=none, 0=QT/DQT Tone, see also 2 bits in x2021 + data_en:1, // Data, 1=enable + power:1, // TX Power: 0 low / 1 high + compander:1, // Compander, 1 = off + wide:1; // Wide/Narrow, wide 1 / 0 narrow + u8 unknown27:4, // unknown + busy_lock_opt:2,// Busy Lock, 11=none, 00=Carrier, 01=OptSig, + unknown28:2; // unknown + u8 unknown29[14]; // unknown yet +} memory[250]; + +#seekto 0x5000; +struct { + u8 d_num; // DTMF Memory number + u8 unknown32; // unknown + char d_an[10]; // A/N + u8 unknown33[4]; // unknown + u8 d_code[8]; // Code + u8 unknown34[8]; // unkown +} dtmf_memory[32]; + +#seekto 0x6000; +struct { + // method unknown so far + u8 fs_fleet_id[3]; // FleetSync, Fleet(Own):100-349 and ID(Own): 1000-3999 + u8 unknown126[2]; + ul16 fs_max_ack_wt; // FleetSync, Maximum ACK Wait [sec]: 0.5-60, .10 incr + ul16 fs_dtx_mod_dt; // FleetSync, Data TX Mod Delay [msec]: 0-6000, 1 incr + u8 unknown125[3]; + u8 fs_uid_enc_blk[4]; // FleetSync, UnitID Enc Block: 1000-4999, method unk + u8 fs_gtc_count; // FleetSync, GTC Count: 0-5 + ul16 fs_tx_bw_time; // FleetSync, TX Busy Wait [sec]: 0.5-60.0, .10 incr + ul16 fs_ack_delay; // FleetSync, ACK Delay Time[sec]: 0.1-60.0, .10 incr + u8 fs_num_retries; // FleetSync, Number of Retries: 0-8 + ul16 fs_txdel_rxcap; // FleetSync, TX Delay (RX Capt): 0.0-25.0, .10 incr + u8 unknown124[3]; + u8 fs_baud; // FleetSync, Baud Rate [bps]: (x31=2400, 30=1200) + ul16 fs_mm_timer; // FleetSync, Message Mode Timer[sec]: (off, 1-300) + u8 fs_em_stat_resp; // FleetSync, Emerg Status Resp: (x00=none, x01=Alert) + u8 fs_ptt_st; // FleetSync, PTT ID Side Tone: (1=enable, 0=disable) + u8 unknown127[3]; + u8 fs_stat_m_data; // FleetSync, Status Msg on Data Ch 1=dis, 0=en + u8 fs_caller_id_st:1, // FleetSync, Caller ID Stack: 1=dis, 0=en + unknown121:1, + fs_stat_8090:1, // FleetSync, Status 80-99(Special): 1=dis, 0=en + unknown122:1, + data_tx_qt:1, // Opt Feat2, Data TX with QT/DQT, '1'=dis, '0'en + fs_man_dial:1, // FleetSync, Manual Dial: 0=disable, 1=enable + fs_if_call:1, // FleetSync, Inter-fleet Call: 1=disable, 0=enable + fs_rand_acc:1; // FleetSync, Random Access (Cont): 0=enable, 1=disable + u8 unknown123:5, + fs_ss_val:1, // FleetSync, Stun Status Valid: (1=enable, 0=disable) + fs_ca_cont:1, // FleetSync, Call Alert(Cont): (1=enable, 0=disable) + fs_call_id_disp:1; // FleetSync, Caller ID Display (1=disable, 0=enable) +} fleetsync; + +#seekto 0x6040; +struct { + u8 em_call_fleet; // x6040, 1 byte, Emergency FleetSync Call Fleet #, 100-350 + u8 em_call_id[2]; // x6041, 2 byte, Emergency FleetSync Call ID #, 1000-3999 +} emergency; + +#seekto 0x6C00; +struct { + u8 fs_idl_fleet; // FleetSync, ID List, Fleet: 100-349 + ul16 fs_idl_id; // FleetSync, ID: 1000-4999 x004E=ALL + char fs_id_name[10]; // FleetSync, ID List, ID Name + u8 fs_idl_tx_inhibit; // FleetSync, ID List, TX Inhibit: FF=No, FE=Yes + u8 unknown127[2]; +} fs_id_list[64]; + +#seekto 0x7000; +struct { + u8 fs_sl_status; // FleetSync, Status List, Status: 10-99 + u8 unknown120; + char fs_stat_name[16]; // FleetSync, Status List, Status Name + u8 fs_sl_tx_inhibit; // FleetSync, Status List, TX Inhibit: FE=Yes, FF=No + u8 unknown128[13]; +} fs_stat_list[50]; +""" + +NOTE = """ MENTAL NOTE ABOUT RADIO MEM + +The OEM insist on not reading/writing some mem segments, see below + +read: (hex) + 00 - 03 + 07 - 08 + 10 + 20 - 21 + 58 - 7F + +write: (hex) + 00 - 03 + 07 - 08 + 10 + 20 - 21 + 60 - 7F + + +This can be an artifact to just read/write in the needed mem space and speed +up things, if so the first read blocks has all the data about channel groups +and freq/tones & names employed. + +This is a copied trick from the "60G series" ones and may use the same schema. + +I must investigate further on this. +""" + +MEM_SIZE = 0x8000 # 32,768 bytes (128 blocks of 256 bytes) +BLOCK_SIZE = 256 +MEM_BLOCKS = range(0, MEM_SIZE // BLOCK_SIZE) +# undefined yet... +RO_BLOCKS = chain(range(0x10, 0x1F), range(0x59, 0x5f), range(0x4FFF, 0x50FF)) + +# define and empty block of data, as it will be used a lot in this code +EMPTY_BLOCK = b"\xFF" * 256 + +ACK_CMD = b"\x06" +NAK_CMD = b"\x15" + +# TK-280:1,5 TK-380:1,4 TK-780:25 TK-880:5,25 +POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1), + chirp_common.PowerLevel("High", watts=4)] + +MODES = ["NFM", "FM"] # 12.5 / 25 Khz +VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + "_-*()/\\-+=)." +SKIP_VALUES = ["", "S"] + +TONES = chirp_common.TONES +# TONES.remove(254.1) +DTCS_CODES = chirp_common.DTCS_CODES + + +TOT = [("off", 0x04B0)] + [("%s" % x, x) for x in range(15, 615, 15)] +TOT_PRE = ["off"] + ["%s" % x for x in range(1, 11)] +TOT_REKEY = ["off"] + ["%s" % x for x in range(1, 61)] +TOT_RESET = ["off"] + ["%s" % x for x in range(1, 16)] +VOL = ["off"] + ["%s" % x for x in range(1, 32)] +TVOL = ["%s" % x for x in range(0, 33)] +TVOL[32] = "Continous" +SQL = ["off"] + ["%s" % x for x in range(1, 10)] +SIG_TYPE = ["AND", "OR"] +CM0 = { + 0xFF: "None", + 0x30: "Data", + 0x31: "GPS", + 0x32: "AUX Hook/PTT", + 0x33: "REM", + 0x34: "Data PTT", + 0x35: "Data+GPS", + 0x36: "Man Down In", +} +BSAVE = {0xFF: "Off", 0x32: "Long", 0x30: "Short", 0x31: "Middle"} +PIDT = {0x00: "DTMF", 0x01: "FleetSync"} +PID = {0o10: "BOT", 0o01: "EOT", 0o00: "Both"} +SLT = {1: "Active Low", 0: "Active High"} +SLS = {1: "COR", 0: "TOR"} +ALT = {1: "Active Low", 0: "Active High"} +ALS = {1: "Continuous", 0: "Pulse"} +SIP = {0x31: "Selected", 0x30: "Fixed", 0xFF: "None"} +PG = ["None"] + ["%s" % x for x in range(1, 32)] +PCH = ["None"] + ["%s" % x for x in range(1, 250)] +LBTA = [x / 100.0 for x in range(35, 505, 5)] +LBTB = [x / 100.0 for x in range(50, 505, 5)] +REVCH = { + 0x30: "Last Called", + 0x31: "Last Used", + 0x34: "Priority", + 0x35: "Priority+TalkBack", +} +DDT = ["%s" % x for x in range(0, 300)] +DWT = ["%s" % x for x in range(0, 300)] +GRPSC = {0x30: "Single", 0x31: "Multi"} +BCL_OPTS = ['Off', 'QT/DQT', 'Option Signalling', 'Carrier Only'] + +# For debugging purposes +debug = True + +KEYS = { + 0x30: "Memory(RCL/STO)", + 0x31: "DTMF ID(BOT)", + 0x32: "DTMF ID(EOT)", + 0x33: "Display character", + 0x34: "Emergency", + 0x35: "Home Channel", # Possible portable only, check it + 0x37: "CH down", + 0x38: "CH up", + 0x39: "Key lock", + 0x3a: "Lamp", # Portable only + 0x3b: "Public address", + 0x3c: "Reverse", # Not all firmware versions? + 0x3d: "Horn alert", + 0x3e: "Memory(RCL)", + 0x3f: "Memory(STO)", + 0x40: "Monitor A: Open Momentary", + 0x41: "Monitor B: Open Toggle", + 0x42: "Monitor C: Carrier Squelch Momentary", + 0x43: "Monitor D: Carrier Squelch Toogle", + 0x44: "AUX", + 0x45: "Redial", + 0x46: "RF Power Low", # portable only ? + 0x47: "Scan", + 0x48: "Scan Del/Add", + 0x4a: "GROUP down", + 0x4b: "GROUP up", + 0x4e: "Operator Selectable Tone", + 0x4f: "None", + 0x50: "VOL down", + 0x51: "VOL up", + 0x52: "Talk around", + 0x5d: "AUX", + 0xa1: "Channel Up/Down", # Knob for portables only + 0xa2: "Group Up/Down" # Knob for portables only + } +KEY_MAP = [(v, k) for k, v in KEYS.items()] + + +def _close_radio(radio): + """Get the radio out of program mode""" + try: + radio.pipe.write(b"E") + except Exception as e: + LOG.error('Failed to close radio: %s' % e) + + +def _checksum(data): + """the radio block checksum algorithm""" + cs = 0 + for byte in data: + cs += byte + return cs % 256 + + +def _make_frame(cmd, addr): + """Pack the info in the format it likes""" + return struct.pack(">BH", cmd[0], addr) + + +def _open_radio(radio, status): + """Open the radio into program mode and check if it's the correct model""" + radio.pipe.baudrate = 9600 + radio.pipe.parity = "E" + radio.pipe.timeout = 1 + + LOG.debug("Entering program mode.") + tries = 10 + + status.msg = "Entering program mode..." + radio.status_fn(status) + + for i in range(0, tries): + radio.pipe.write(b"PROGRAM") + ack = radio.pipe.read(1) + LOG.debug('Ack: %r' % ack) + if not ack: + LOG.debug('No response from radio, will retry') + time.sleep(0.5) + elif ack != ACK_CMD: + LOG.debug('Received unexpected response from radio: %r' % ack) + radio.pipe.flush() + time.sleep(0.5) + else: + break + + if ack != ACK_CMD: + raise errors.RadioError('Failed to put radio into programming mode') + + radio.pipe.write(b"\x02") + rid = radio.pipe.read(8) + if radio.TYPE not in rid: + LOG.debug("Incorrect model ID:") + LOG.debug(util.hexprint(rid)) + raise errors.RadioError( + "Incorrect model ID, got %s, it doesn't contain %s" % + (rid.strip(b"\xff"), radio.TYPE)) + + LOG.debug("Full ident string is:") + LOG.debug(util.hexprint(rid)) + exchange_ack(radio.pipe) + + status.msg = "Radio ident success!" + radio.status_fn(status) + + radio.pipe.write(b"P") + ver = radio.pipe.read(10) + LOG.debug("Version returned by the radios is:") + LOG.debug(util.hexprint(ver)) + exchange_ack(radio.pipe) + # the radio that was processed returned this: + # v2.00k.. [76 32 2e 30 30 6b ef ff] + + # now the OEM writes simply "O" and gets no answer... + # after that we are ready to receive the radio image or to write to it + radio.pipe.write(b'O') + + radio.metadata = {'tkx80_ver': ver.strip(b'\xFF'), + 'tkx80_rid': rid.strip(b'\xFF')} + + +def exchange_ack(pipe): + pipe.write(ACK_CMD) + ack = pipe.read(1) + if ack == NAK_CMD: + LOG.debug('Radio sent explicit NAK') + raise errors.RadioError('Radio sent NAK') + elif ack != ACK_CMD: + LOG.debug('Radio sent unexpected response: %r' % ack) + raise errors.RadioError('Radio did not ack') + + +def read_block(pipe): + cmd = pipe.read(1) + if cmd == b'Z': + block = b'\xFF' * BLOCK_SIZE + else: + block = pipe.read(BLOCK_SIZE) + checksum = pipe.read(1) + calc = _checksum(block) + if calc != checksum[0] and cmd != b'Z': + LOG.debug('Checksum %i does not match %i', calc, checksum[0]) + raise errors.RadioError('Checksum reading block from radio') + exchange_ack(pipe) + return block + + +def do_download(radio): + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE // BLOCK_SIZE + radio.status_fn(status) + + data = b"" + count = 0 + _open_radio(radio, status) + + status.msg = "Cloning from radio..." + for addr in MEM_BLOCKS: + radio.pipe.write(_make_frame(b"R", addr)) + block = read_block(radio.pipe) + data += block + status.cur = count + radio.status_fn(status) + count += 1 + + return memmap.MemoryMapBytes(data) + + +def do_upload(radio): + status = chirp_common.Status() + status.cur = 0 + status.max = MEM_SIZE // BLOCK_SIZE + status.msg = "Cloning to radio..." + radio.status_fn(status) + + _open_radio(radio, status) + + status.msg = "Cloning to radio..." + for block in MEM_BLOCKS: + if 0x50 < block < 0x60: + # This causes a NAK if the radio does not have this feature + LOG.debug('Skipping DTMF memory block 0x%x', block) + continue + data = radio.get_mmap()[block * BLOCK_SIZE:(block + 1) * BLOCK_SIZE] + if data == EMPTY_BLOCK: + frame = _make_frame(b'Z', block) + b'\xFF' + else: + cs = _checksum(data) + frame = _make_frame(b'W', block) + data + bytes([cs]) + radio.pipe.write(frame) + try: + exchange_ack(radio.pipe) + except Exception: + LOG.error('Failed to send block 0x%x', block) + raise + status.cur = block + radio.status_fn(status) + + +class KenwoodTKx80(chirp_common.CloneModeRadio): + """Kenwood Series 80 Radios base class""" + VENDOR = "Kenwood" + BAUD_RATE = 9600 + _memsize = MEM_SIZE + NAME_LENGTH = 8 + _range = [] + _upper = 250 + _steps = chirp_common.COMMON_TUNING_STEPS + VARIANT = "" + MODEL = "" + FORMATS = [directory.register_format('Kenwood KPG-49D', '.dat')] + + def load_mmap(self, filename): + if filename.lower().endswith('.dat'): + with open(filename, 'rb') as f: + f.seek(0x32) + self._mmap = memmap.MemoryMapBytes(f.read()) + LOG.info('Loaded DAT file at offset 0x32') + self.process_mmap() + else: + return super().load_mmap(filename) + + def save_mmap(self, filename): + dat_header = (b'KPG49D\xFF\xFF\xFF\xFFV4.02P0' + + self.MODEL[3:].encode() + + b'\x04\xFF\xF1\xFF' + + b'\xFF' * 26) + + if filename.lower().endswith('.dat'): + with open(filename, 'wb') as f: + f.write(dat_header) + f.write(self._mmap.get_packed()) + LOG.info('Write DAT file') + else: + super().save_mmap(filename) + + @classmethod + def get_prompts(cls): + rp = chirp_common.RadioPrompts() + rp.experimental = \ + ('This driver is experimental and supports only conventional ' + 'zones.') + return rp + + def get_features(self): + """Return information about this radio's features""" + rf = chirp_common.RadioFeatures() + rf.has_settings = True + rf.has_tuning_step = False + rf.has_name = True + rf.has_offset = True + rf.has_mode = True + rf.has_dtcs = True + rf.has_rx_dtcs = True + rf.has_dtcs_polarity = True + rf.has_ctone = True + rf.has_cross = True + rf.valid_modes = MODES + rf.valid_duplexes = ["", "-", "+", "off"] + rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross'] + rf.valid_cross_modes = [ + "Tone->Tone", + "DTCS->", + "->DTCS", + "Tone->DTCS", + "DTCS->Tone", + "->Tone", + "DTCS->DTCS"] + rf.valid_power_levels = POWER_LEVELS + rf.valid_characters = VALID_CHARS + rf.valid_skips = SKIP_VALUES + rf.valid_dtcs_codes = DTCS_CODES + rf.valid_bands = self._range + # FIXME: The 280 supports finer steps + rf.valid_tuning_steps = self._steps + rf.valid_name_length = 10 + rf.memory_bounds = (1, self._upper) + rf.has_sub_devices = True + rf.has_bank = False + return rf + + def get_sub_devices(self): + if not self._memobj: + return [TKx80Group(self, 1, 'Group 1')] + return sorted([TKx80Group(self, + self._memobj.groups[i].number, + str(self._memobj.groups[i].name).strip()) + for i in range(250) + if self._memobj.groups[i].number <= 250], + key=lambda z: z.group) + + def sync_in(self): + """Do a download of the radio eeprom""" + try: + self._mmap = do_download(self) + except errors.RadioError: + raise + except Exception as e: + LOG.exception('Unexpected radio error: %s' % e) + raise errors.RadioError(str(e)) + finally: + _close_radio(self) + self.process_mmap() + + def sync_out(self): + """Do an upload to the radio eeprom""" + + try: + do_upload(self) + except errors.RadioError: + raise + except Exception as e: + raise errors.RadioError("Failed to communicate with radio: %s" % e) + finally: + _close_radio(self) + + def process_mmap(self): + self._memobj = bitwise.parse(MEM_FORMAT, self._mmap) + + def _decode_tone(self, val): + """Parse the tone data to decode from mem, it returns: + Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)""" + val = int(val) + if val == 65535: + return '', None, None + elif val >= 0x2800: + code = int("%03o" % (val & 0x07FF)) + pol = (val & 0x8000) and "R" or "N" + return 'DTCS', code, pol + else: + a = val / 10.0 + return 'Tone', a, None + + def _encode_tone(self, memval, mode, value, pol): + """Parse the tone data to encode from UI to mem""" + if mode == '': + memval.set_raw(b"\xff\xff") + elif mode == 'Tone': + memval.set_value(int(value * 10)) + elif mode == 'DTCS': + val = int("%i" % value, 8) + 0x2800 + if pol == "R": + val += 0xA000 + memval.set_value(val) + else: + raise Exception("Internal error: invalid mode `%s'" % mode) + + def _get_scan(self, chan): + """Get the channel scan status from the 16 bytes array on the eeprom + then from the bits on the byte, return '' or 'S' as needed""" + result = "S" + byte = int(chan/8) + bit = chan % 8 + res = self._memobj.settings.add[byte] & (pow(2, bit)) + if res > 0: + result = "" + + return result + + def _set_scan(self, chan, value): + """Set the channel scan status from UI to the mem_map""" + byte = int(chan/8) + bit = chan % 8 + + # get the actual value to see if I need to change anything + actual = self._get_scan(chan) + if actual != value: + # I have to flip the value + rbyte = self._memobj.settings.add[byte] + rbyte = rbyte ^ pow(2, bit) + self._memobj.settings.add[byte] = rbyte + + def _get_memory_mapping(self, group, number, allocate=False): + """Find a virtual memory mapping in the group's index. + + Raise IndexError if not mapped. + """ + empty = None + free_memories = set(range(250)) + this_group_index = None + # Count to 150 and... + for i in range(250): + # Look for an existing mapping for this group,number + mapping = self._memobj.group_mapping[i] + if mapping.group == group and mapping.number == number: + # Direct hit, return + return mapping + elif empty is None and mapping.group == 0xFF: + # Record the earliest-available mapping + empty = i + elif mapping.index < 255: + # Any valid mapping consumes a slot in the memory array, + # so keep track of which ones are unused + free_memories.remove(int(mapping.index)) + + # Also record the index of this group in case we have to allocate + # below + if self._memobj.groups[i].number == group: + this_group_index = i + + if allocate: + if this_group_index is None: + raise errors.RadioError( + 'Allocate for group %i did not find a record' % group) + # Use the first available memory location + memory = list(sorted(free_memories))[0] + self._memobj.group_mapping[empty].group = group + self._memobj.group_mapping[empty].number = number + self._memobj.group_mapping[empty].group_index = this_group_index + self._memobj.group_mapping[empty].index = memory + self._memobj.groups[this_group_index].channels += 1 + LOG.info(('Allocating slot %i memory %i for %i-%i ' + 'grp index %i channels %i') % ( + empty, memory, group, number, this_group_index, + self._memobj.groups[this_group_index].channels)) + self._compact_mappings() + # The above may have rearranged things so re-search for our + # new slot + return self._get_memory_mapping(group, number) + else: + raise IndexError('Memory %i-%i not mapped' % (group, number)) + + def _delete_memory_mapping(self, group, number): + mapping = self._get_memory_mapping(group, number) + mem = self._memobj.memory[mapping.index] + group_rec = self._memobj.groups[mapping.group_index] + group_rec.channels -= 1 + LOG.info( + 'Deleting memory mapping for %i-%i memory %i (%i in group)' % ( + group, number, mapping.index, group_rec.channels)) + mem.fill_raw(b'\xFF') + mapping.fill_raw(b'\xFF') + self._compact_mappings() + + def _compact_mappings(self): + """Survey all the memories in mapping list and update.""" + group_counts = collections.defaultdict(lambda: 0) + valid = [] + # Count to 250 and ... + for i in range(250): + # Find all the valid mappings and count up number of valid + # memories in each group + mapping = self._memobj.group_mapping[i] + if mapping.group < 255 and mapping.number < 255: + valid.append((int(mapping.group), int(mapping.number), + int(mapping.group_index), int(mapping.index))) + group_counts[int(mapping.group)] += 1 + + # The mappings need to be in order, at least by group, perhaps by + # memory number as well + valid.sort(key=lambda x: (x[0], x[1])) + + memories = 0 + groups = set() + # Count to 250 again and... + for i in range(250): + # Update all the mappings with the new sorted survey results + try: + group, number, group_index, index = valid[i] + memories += 1 + groups.add(group) + except IndexError: + group = number = group_index = index = 0xFF + self._memobj.group_mapping[i].group = group + self._memobj.group_mapping[i].number = number + self._memobj.group_mapping[i].group_index = group_index + self._memobj.group_mapping[i].index = index + + # Also update the groups with correct memory counts + group = int(self._memobj.groups[i].number) + if group in group_counts: + self._memobj.groups[i].channels = group_counts[group] + + # Radio-wide group and memory counts need to be updated + self._memobj.settings.groups = len(groups) + self._memobj.settings.channels = memories + LOG.info('%i groups, %i memories', + self._memobj.settings.groups, + self._memobj.settings.channels) + + @classmethod + def match_model(cls, filedata, filename): + model = cls.MODEL[3:].encode() + if (filename.endswith('.dat') and + b'KPG49D' in filedata and model in filedata[:0x32]): + return True + return False + + def _get_settings_groups(self, groups): + group_index = {} + unused_slots = set() + + # Survey the currently-mapped groups + for i in range(250): + if self._memobj.groups[i].number < 0xFF: + group_index[int(self._memobj.groups[i].number)] = i + else: + unused_slots.add(i) + + for i in range(250): + group_number = i + 1 + try: + group = self._memobj.groups[group_index[group_number]] + group_name = str(group.name).strip('\xFF') + group_count = int(group.channels) + enabled = True + except KeyError: + group_name = '' + group_count = 0 + enabled = False + name = RadioSetting('group-%i-name' % group_number, 'Name', + RadioSettingValueString( + 0, 10, group_name)) + enable = RadioSetting('group-%i-enable' % group_number, 'Enabled', + RadioSettingValueBoolean(enabled)) + enable.set_volatile(True) + enable.set_doc( + 'Changing this requires a save and re-load of the file!') + count = RadioSetting('group-%i-count' % group_number, + 'Active Memories', + RadioSettingValueInteger(0, 250, group_count)) + if group_count: + # Can't delete groups with active memories + enable.value.set_mutable(False) + enable.set_doc('Delete all memories from group to disable') + else: + name.value.set_mutable(False) + count.value.set_mutable(False) + groups.append(RadioSettingSubGroup( + 'group-%i' % group_number, + 'Group %i' % group_number, + name, enable, count)) + + def get_settings(self): + """Translate the bit in the mem_struct into settings in the UI""" + sett = self._memobj.settings + msc = self._memobj.misc + keys = self._memobj.keys + passwd = self._memobj.passwords + fsync = self._memobj.fleetsync + + optfeat1 = RadioSettingGroup("optfeat1", "Optional Features 1") + optfeat2 = RadioSettingGroup("optfeat2", "Optional Features 2") + dealer = RadioSettingGroup("dealer", "Dealer Settings") + fkeys = RadioSettingGroup("keys", "Controls") + scaninf = RadioSettingGroup("scaninf", "Scan Information") + dtmfset = RadioSettingGroup("dtmfset", "DTMF") + groups = RadioSettingGroup("groups", "Groups") + + top = RadioSettings(optfeat1, optfeat2, dealer, fkeys, scaninf, + dtmfset, groups) + + self._get_settings_groups(groups) + # If user changes passwords to blank, which is actually x20 not xFF, + # that sets impossible password. + # Read-only to view unknown passwords only + radiop = RadioSetting( + "passwords.radio", "Radio Password", + RadioSettingValueString(0, 6, + str(passwd.radio).strip('\xFF'))) + radiop.value.set_mutable(False) + optfeat1.append(radiop) + + datap = RadioSetting( + "passwords.data", "Data Password", + RadioSettingValueString(0, 6, str(passwd.data).strip('\xFF'))) + datap.value.set_mutable(False) + optfeat1.append(datap) + + pom = MemSetting( + "settings.poweronmesg", "Power on message", + RadioSettingValueString(0, 12, + str(sett.poweronmesg).strip('\xFF'), + mem_pad_char='\xFF')) + optfeat1.append(pom) + + sigtyp = MemSetting( + "settings.signalling_type", "Signalling Type", + RadioSettingValueList(SIG_TYPE, + current_index=int(sett.signalling_type))) + optfeat1.append(sigtyp) + + if self.TYPE[0] == "P": + bsav = MemSetting( + "settings.battery_save", "Battery Save", + RadioSettingValueList(BSAVE.values(), + current_index=sett.battery_save)) + optfeat1.append(bsav) + + tot = MemSetting("settings.tot", "Time Out Timer (TOT)", + RadioSettingValueMap( + TOT, sett.tot)) + optfeat1.append(tot) + + totalert = MemSetting("settings.tot_alert", "TOT pre alert", + RadioSettingValueList( + TOT_PRE, + current_index=sett.tot_alert)) + optfeat1.append(totalert) + + totrekey = MemSetting("settings.tot_rekey", "TOT re-key time", + RadioSettingValueList( + TOT_REKEY, + current_index=sett.tot_rekey)) + optfeat1.append(totrekey) + + totreset = MemSetting("settings.tot_reset", "TOT reset time", + RadioSettingValueList( + TOT_RESET, + current_index=sett.tot_reset)) + optfeat1.append(totreset) + + c2t = MemSetting("settings.c2t", "Clear to Transpond", + RadioSettingValueInvertedBoolean(not sett.c2t)) + optfeat1.append(c2t) + + if self.TYPE[0] == "P": + bw = MemSetting("settings.battery_warn", "Battery Warning", + RadioSettingValueInvertedBoolean( + sett.battery_warn)) + optfeat1.append(bw) + + if self.TYPE[0] == "M": + ohd = MemSetting("settings.off_hook_decode", "Off Hook Decode", + RadioSettingValueInvertedBoolean( + sett.off_hook_decode)) + optfeat1.append(ohd) + + ohha = MemSetting("settings.off_hook_horn_alert", + "Off Hook Horn Alert", + RadioSettingValueInvertedBoolean( + sett.off_hook_horn_alert)) + optfeat1.append(ohha) + + bled = MemSetting('settings.busy_led', 'Busy LED', + RadioSettingValueInvertedBoolean( + not bool(sett.busy_led))) + optfeat1.append(bled) + + scled = MemSetting("settings.sel_call_alert_led", + "Selective Call Alert LED", + RadioSettingValueBoolean(sett.sel_call_alert_led)) + optfeat1.append(scled) + + minvol = MemSetting("settings.min_vol", "Minimum Volume", + RadioSettingValueList( + VOL, current_index=sett.min_vol)) + optfeat2.append(minvol) + + # FIXME: Not sure this is right + tv = int(sett.tone_vol) + if tv == 255: + tv = 32 + tvol = MemSetting("settings.tone_vol", "Tone Volume", + RadioSettingValueList(TVOL, current_index=tv)) + optfeat2.append(tvol) + + """sql = RadioSetting("settings.sql_level", "SQL Ref Level", + RadioSettingValueList( + SQL, SQL[int(sett.sql_level)])) + optfeat1.append(sql)""" + + # Tone Volume Section + ptone = MemSetting("settings.poweron_tone", "Power On Tone", + RadioSettingValueBoolean(sett.poweron_tone)) + optfeat2.append(ptone) + + wtone = MemSetting("settings.warn_tone", "Warning Tone", + RadioSettingValueBoolean(sett.warn_tone)) + optfeat2.append(wtone) + + ctone = MemSetting("settings.control_tone", "Control (Key) Tone", + RadioSettingValueBoolean(sett.control_tone)) + optfeat2.append(ctone) + + pttr = MemSetting("settings.ptt_release_tone", "PTT Release Tone", + RadioSettingValueInvertedBoolean( + not sett.ptt_release_tone)) + optfeat2.append(pttr) + + # Save Battery only for portables? + """if self.TYPE[0] == "P": + bs = int(sett.battery_save) == 0x32 and True or False + bsave = RadioSetting("settings.battery_save", "Battery Saver", + RadioSettingValueBoolean(bs)) + optfeat1.append(bsave)""" + + # PTT ID Section + pdt = MemSetting("misc.ptt_id_type", "PTT ID Type", + RadioSettingValueList( + PIDT.values(), + current_index=msc.ptt_id_type)) + optfeat2.append(pdt) + + bot = str(sett.ptt_id_bot).strip("\xFF") + pttbot = MemSetting("settings.ptt_id_bot", "PTT Begin of TX", + RadioSettingValueString(0, 16, bot, + mem_pad_char='\xFF')) + optfeat2.append(pttbot) + + eot = str(sett.ptt_id_eot).strip("\xFF") + ptteot = MemSetting("settings.ptt_id_eot", "PTT End of TX", + RadioSettingValueString(0, 16, eot, + mem_pad_char='\xFF')) + optfeat2.append(ptteot) + + inhta = MemSetting("settings.ptt_inhib_ta", + "PTT Inhibit ID in TA(TalkAround)", + RadioSettingValueInvertedBoolean( + not sett.ptt_inhib_ta)) + optfeat2.append(inhta) + + svp = str(sett.lastsoftversion).strip("\xFF") + sver = RadioSetting("not.softver", "Last Used Software Version", + RadioSettingValueString(0, 5, svp, + mem_pad_char='\xFF')) + sver.value.set_mutable(False) + dealer.append(sver) + + try: + vtmp = str(self.metadata.get('tkx80_ver', '(unknown)')) + except AttributeError: + vtmp = '' + frev = RadioSetting("not.ver", "Radio Version", + RadioSettingValueString(0, 10, vtmp)) + frev.set_doc('Radio version (as downloaded)') + frev.value.set_mutable(False) + dealer.append(frev) + + l1 = str(msc.line1).strip("\xFF") + line1 = MemSetting("misc.line1", "Comment 1", + RadioSettingValueString(0, 32, l1, + mem_pad_char='\xFF')) + dealer.append(line1) + + l2 = str(msc.line2).strip("\xFF") + line2 = MemSetting("misc.line2", "Comment 2", + RadioSettingValueString(0, 32, l2, + mem_pad_char='\xFF')) + dealer.append(line2) + + panel = MemSetting("settings.panel_test", "Panel Test", + RadioSettingValueBoolean(sett.panel_test)) + optfeat2.append(panel) + + ptun = MemSetting("settings.panel_tuning", "Panel Tuning", + RadioSettingValueBoolean(sett.panel_tuning)) + optfeat2.append(ptun) + + fmw = MemSetting("settings.firmware_prog", "Firmware Programming", + RadioSettingValueBoolean(sett.firmware_prog)) + optfeat2.append(fmw) + + clone = MemSetting("settings.clone", "Allow clone", + RadioSettingValueBoolean(sett.clone)) + optfeat2.append(clone) + + sprog = MemSetting("settings.self_prog", "Self Programming", + RadioSettingValueBoolean(sett.self_prog)) + optfeat2.append(sprog) + + # Logic Signal Section + sqlt = MemSetting("settings.sq_logic_type", "Squelch Logic Type", + RadioSettingValueList( + SLT.values(), + current_index=sett.sq_logic_type)) + optfeat2.append(sqlt) + + sqls = MemSetting("settings.sq_logic_sig", "Squelch Logic Signal", + RadioSettingValueList( + SLS.values(), + current_index=sett.sq_logic_sig)) + optfeat2.append(sqls) + + aclt = MemSetting("settings.access_log_type", "Acess Logic Type", + RadioSettingValueList( + ALT.values(), + current_index=sett.access_log_type)) + optfeat2.append(aclt) + + acls = MemSetting("settings.access_log_sig", "Acess Logic Signal", + RadioSettingValueList( + ALS.values(), + current_index=sett.access_log_sig)) + optfeat2.append(acls) + + # Extended Function Section + dtxqt = MemSetting("fleetsync.data_tx_qt", "Data TX with QT/DQT", + RadioSettingValueInvertedBoolean( + not fsync.data_tx_qt)) + optfeat2.append(dtxqt) + + drdt = MemSetting("settings.sc_nfo_ddtime", + "Dropout Delay Time[sec]", + RadioSettingValueList( + DDT, current_index=sett.sc_nfo_ddtime)) + scaninf.append(drdt) + + dwet = MemSetting("settings.sc_nfo_dwell", "Dwell Time[sec]", + RadioSettingValueList( + DWT, + current_index=sett.sc_nfo_dwell)) + scaninf.append(dwet) + + rchd = MemSetting("settings.sc_nfo_rev_disp", + "Revert Channel Display", + RadioSettingValueInvertedBoolean( + not bool(sett.sc_nfo_rev_disp))) + scaninf.append(rchd) + + # DTMF Settings + # Decode Section + deccode = str(sett.dtmf_prim_code).strip("\xFF") + decpc = MemSetting("settings.dtmf_prim_code", "Decode Primary Code", + RadioSettingValueString(0, 7, deccode)) + dtmfset.append(decpc) + + deccode = str(sett.dtmf_sec_code).strip("\xFF") + decpc = MemSetting("settings.dtmf_sec_code", "Decode Secondary Code", + RadioSettingValueString(0, 7, deccode)) + dtmfset.append(decpc) + + deccode = str(sett.dtmf_DBD_code).strip("\xFF") + decpc = MemSetting("settings.dtmf_DBD_code", + "Decode Dead Beat Disable Code", + RadioSettingValueString(0, 7, deccode, + mem_pad_char='\xFF')) + dtmfset.append(decpc) + + # front keys + # The Mobile only parameters are wraped here + if self.TYPE[0] == ord("M"): + vu = MemSetting("keys.kVOL_UP", "VOL UP(Left Arrow Up)", + RadioSettingValueList( + KEYS.values(), + current_index=keys.kVOL_UP)) + fkeys.append(vu) + + vd = MemSetting("keys.kVOL_DOWN", "VOL DOWN(Left Arrow Down)", + RadioSettingValueList( + KEYS.values(), + current_index=keys.kVOL_DOWN)) + fkeys.append(vd) + + chu = MemSetting("keys.kPF1", "Group UP(Right Side Up Arrow)", + RadioSettingValueList( + KEYS.values(), + current_index=keys.kPF1)) + fkeys.append(chu) + + chd = MemSetting("keys.kPF2", + "Group DOWN(Right Side Down Arrow)", + RadioSettingValueList( + KEYS.values(), + current_index=keys.kPF2)) + fkeys.append(chd) + + foot = MemSetting("keys.kORANGE", "Foot switch", + RadioSettingValueList( + KEYS.values(), + current_index=keys.kORANGE)) + fkeys.append(foot) + + if self.TYPE[0] == ord("P"): + knob = MemSetting("keys.kP_KNOB", "Knob", + RadioSettingValueMap( + KEY_MAP, + keys.kP_KNOB)) + fkeys.append(knob) + + orange = MemSetting("keys.kPF1", "Orange", + RadioSettingValueMap( + KEY_MAP, keys.kPF1)) + fkeys.append(orange) + + side1 = MemSetting("keys.kSIDE1", "Side 1", + RadioSettingValueMap( + KEY_MAP, keys.kSIDE1)) + fkeys.append(side1) + + mon_name = "MON" + if self.TYPE[0] == ord("P"): + mon_name = "Side 2" + + mon = MemSetting("keys.kMON", mon_name, + RadioSettingValueMap( + KEY_MAP, keys.kMON)) + fkeys.append(mon) + + sbtn = MemSetting("keys.kSCN", "Scan/S", + RadioSettingValueMap( + KEY_MAP, keys.kSCN)) + fkeys.append(sbtn) + + abtn = MemSetting("keys.kA", "A", + RadioSettingValueMap( + KEY_MAP, keys.kA)) + fkeys.append(abtn) + + bbtn = MemSetting("keys.kLEFT", "Left Arrow(B on Portable)", + RadioSettingValueMap( + KEY_MAP, keys.kLEFT)) + fkeys.append(bbtn) + + cbtn = MemSetting("keys.kRIGHT", "Right Arrow(C on Portable)", + RadioSettingValueMap( + KEY_MAP, keys.kRIGHT)) + fkeys.append(cbtn) + + # TODO Make the following (keypad 0-9,*,#) contingent on variant. + # Only concerned with TK-380 for now + for key in '0123456789': + btn = MemSetting("keys.k%s" % key, "Keypad %s" % key, + RadioSettingValueMap( + KEY_MAP, getattr(keys, 'k%s' % key))) + fkeys.append(btn) + + starbtn = MemSetting("keys.kASTR", "Keypad *", + RadioSettingValueMap( + KEY_MAP, keys.kASTR)) + fkeys.append(starbtn) + + poundbtn = MemSetting("keys.kPOUND", "Keypad #", + RadioSettingValueMap( + KEY_MAP, keys.kPOUND)) + fkeys.append(poundbtn) + + return top + + def _set_settings_groups(self, settings): + group_index = {} + unused_slots = [] + + # Survey the currently-mapped groups + for i in range(250): + if self._memobj.groups[i].number < 0xFF: + group_index[int(self._memobj.groups[i].number)] = i + else: + unused_slots.append(i) + + allocated = False + for i in range(250): + group_number = i + 1 + enable_setting = settings.get('group-%i-enable' % group_number) + name_setting = settings.get('group-%i-name' % group_number) + try: + group = self._memobj.groups[group_index[group_number]] + except KeyError: + if enable_setting and enable_setting.value: + slot = unused_slots.pop(0) + LOG.debug('Allocating group slot %i for group %i', + slot, group_number) + group = self._memobj.groups[slot] + group.number = group_number + group.channels = 0 + group.name = (name_setting and str(name_setting.value) or + str(group_number).ljust(10)) + allocated = True + group = None + if enable_setting and not enable_setting.value and group: + # Group is allocated, UI asked to disable it + group.fill_raw(b'\xFF') + elif group and name_setting: + # Group is allocated, set name + group.name = name_setting.value + if allocated: + LOG.debug('Compacting group mappings after new allocation') + self._compact_mappings() + + def set_settings(self, settings): + # All the actual settings we care about are MemSetting and can be + # direct applied. + all_other_settings = settings.apply_to(self._memobj) + # This should just be group geometry settings at this point + other_settings = {s.get_name(): s for s in all_other_settings} + self._set_settings_groups(other_settings) + + +class TKx80Group(KenwoodTKx80): + def __init__(self, parent, group, name): + self._parent = parent + self._group = group + self.VARIANT = name + self.TYPE = parent.TYPE + + @property + def group(self): + return self._group + + @property + def _memobj(self): + return self._parent._memobj + + def load_mmap(self, filename): + self._parent.load_mmap(filename) + + def get_sub_devices(self): + return [] + + def get_features(self): + rf = self._parent.get_features() + rf.has_sub_devices = False + rf.memory_bounds = (1, 250) + return rf + + def get_raw_memory(self, number): + """Return a raw representation of the memory object, which + is very helpful for development""" + try: + mapping = self._get_memory_mapping(self.group, number) + except IndexError: + return 'Memory not set' + return repr(self._memobj.memory[mapping.index]) + + def get_memory(self, number): + mem = chirp_common.Memory() + mem.number = number + try: + mapping = self._get_memory_mapping(self.group, number) + except IndexError: + mem.empty = True + return mem + + _mem = self._memobj.memory[mapping.index] + + mem.freq = int(_mem.rxfreq) * 10 + if _mem.txfreq.get_raw()[0] == 0xFF: + mem.offset = 0 + mem.duplex = "off" + else: + chirp_common.split_to_offset(mem, mem.freq, int(_mem.txfreq) * 10) + + mem.name = str(_mem.name).rstrip() + mem.power = POWER_LEVELS[_mem.power] + mem.mode = MODES[_mem.wide] + mem.skip = self._get_scan(number - 1) + + txtone = self._decode_tone(_mem.tx_tone) + rxtone = self._decode_tone(_mem.rx_tone) + chirp_common.split_tone_decode(mem, txtone, rxtone) + + mem.extra = RadioSettingGroup("extra", "Extra") + + bs = MemSetting("beat_shift", "Beat shift", + RadioSettingValueInvertedBoolean( + not bool(_mem.beat_shift))) + mem.extra.append(bs) + + cp = MemSetting("compander", "Compander", + RadioSettingValueInvertedBoolean( + not bool(_mem.compander))) + mem.extra.append(cp) + + pttid = MemSetting("ptt_id", "PTTID", + RadioSettingValueInvertedBoolean(not _mem.ptt_id)) + mem.extra.append(pttid) + + if _mem.busy_lock == 1: + index = 0 + elif _mem.busy_lock_opt == 1: + index = 2 + elif _mem.busy_lock_opt == 0: + index = 3 + else: + index = 1 + bl = RadioSetting("busy_lock", "Busy Channel lock", + RadioSettingValueList(BCL_OPTS, current_index=index)) + + mem.extra.append(bl) + + return mem + + def set_memory(self, mem): + try: + mapping = self._get_memory_mapping(self.group, mem.number, + allocate=True) + except IndexError as e: + # Out of memory + raise errors.RadioError(str(e)) + + _mem = self._memobj.memory[mapping.index] + + if mem.empty: + self._delete_memory_mapping(self.group, mem.number) + return + + _mem.rxfreq = mem.freq // 10 + + # this are a mistery yet, but so falr there is no impact + # whit this default values for new channels + if int(_mem.rx_unkw) == 0xff: + _mem.rx_unkw = 0x35 + _mem.tx_unkw = 0x32 + + if mem.duplex == "+": + _mem.txfreq = (mem.freq + mem.offset) // 10 + elif mem.duplex == "-": + _mem.txfreq = (mem.freq - mem.offset) // 10 + elif mem.duplex == "off": + _mem.txfreq.fill_raw(b'\xFF') + else: + _mem.txfreq = mem.freq // 10 + + ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \ + chirp_common.split_tone_encode(mem) + self._encode_tone(_mem.tx_tone, txmode, txtone, txpol) + self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol) + + _namelength = self.get_features().valid_name_length + _mem.name = mem.name.ljust(_namelength) + + try: + _mem.power = POWER_LEVELS.index(mem.power) + except ValueError: + _mem.power = POWER_LEVELS[0] + + _mem.wide = MODES.index(mem.mode) + self._set_scan(mem.number - 1, mem.skip) + + _mem.number = mem.number + _mem.group = self.group + + if mem.extra: + bcl = int(mem.extra['busy_lock'].value) + if bcl == 0: + _mem.busy_lock = 1 + _mem.busy_lock_opt = 3 + elif bcl == 2: + _mem.busy_lock = 0 + _mem.busy_lock_opt = 1 + elif bcl == 3: + _mem.busy_lock = 0 + _mem.busy_lock_opt = 0 + elif bcl == 1: + _mem.busy_lock = 0 + _mem.busy_lock_opt = 3 + + # extra settings + mem.extra['ptt_id'].apply_to_memobj(_mem) + mem.extra['compander'].apply_to_memobj(_mem) + mem.extra['beat_shift'].apply_to_memobj(_mem) + + return mem + + +@directory.register +class TK280_Radios(KenwoodTKx80): + """Kenwood TK-280 Radio""" + MODEL = "TK-280" + TYPE = b"P0280" + # VARIANTS = { + # # VERIFIED variant. Range expanded for ham bands. Orig: 146, 174 + # b"P0280\x04\xFF": (250, 144, 174, "K Non-Keypad Model"), + # b"P0280\x05\xFF": (250, 136, 162, "K2 Non-Keypad Model"), + # # Range expanded for ham bands. Orig: 146, 174 + # b"P0280\xFF\xFF": (250, 144, 174, "K3 Keypad Model"), + # b"P0280\xFF\xFF": (250, 136, 162, "K4 Keypad Model"), + # } + _range = [(136000000, 174000000)] + _steps = chirp_common.COMMON_TUNING_STEPS + (2.5, 6.25) + + +@directory.register +class TK380_Radios(KenwoodTKx80): + """Kenwood TK-380 Radio """ + MODEL = "TK-380" + TYPE = b"P0380" + # VARIANTS = { + # # Range expanded for ham bands. Orig: 450, 490 + # b"P0380\x06\xFF": (250, 420, 490, "K Non-Keypad Model"), + # # Range expanded for ham bands. Orig: 470, 512 + # b"P0380\x07\xFF": (250, 420, 512, "K2 Non-Keypad Model"), + # # Range expanded for ham bands. Orig: 400, 430 + # b"P0380\x08\xFF": (250, 400, 450, "K3 Non-Keypad Model"), + # # VERIFIED variant. Range expanded for ham bands. Orig: 450, 490 + # b"P0380\x0a\xFF": (250, 420, 490, "K4 Keypad Model"), + # # Range expanded for ham bands. Orig: 400, 430 + # b"P0380\xFF\xFF": (250, 400, 450, "K6 Keypad Model"), + # # Range expanded for ham bands. Orig: 470, 520 + # b"P0380\xFF\xFF": (250, 420, 520, "K5 Keypad Model"), + # } + _range = [(400000000, 520000000)] + _steps = chirp_common.COMMON_TUNING_STEPS + (6.25,) + + +# Not yet tested +class TK780_Radios(KenwoodTKx80): + """Kenwood TK-780 Radio """ + MODEL = "TK-780" + TYPE = b"M0780" + # VARIANTS = { + # # VERIFIED variant. #Range expanded for ham bands. Orig: 146, 174 + # b"M0780\x04\xFF": (250, 144, 174, "K"), + # b"M0780\x05\xFF": (250, 136, 174, "K2"), + # } + _range = [(136000000, 174000000)] + + +# Not yet tested +class TK880_Radios(KenwoodTKx80): + """Kenwood TK-880 Radio """ + MODEL = "TK-880" + TYPE = b"M0880" + # VARIANTS = { + # # VERIFIED variant. #Range expanded for ham bands. Orig: 450, 490 + # b"M0880\x06\xFF": (250, 420, 490, "K"), + # # Range expanded for ham bands. Orig: 485, 512 + # b"M0880\x07\xFF": (250, 420, 512, "K2"), + # # Range expanded for ham bands. Orig: 400, 430 + # b"M0880\x08\xFF": (250, 400, 450, "K3"), + # } + _range = [(400000000, 520000000)]