From 0863e4e39b7a4052cf9dbdb554ac4fec875d0e30 Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Tue, 14 Jan 2025 17:07:05 -0800 Subject: [PATCH] Add Kenwood TK-280,380 support This started from the latest version of the module attached to issue #4935 and contains a lot of memory-mapping work from others. From there, this has been modernized, and cleaned up. The clone routines have been cleaned up to be deterministic and not dependent on platform timing behavior (which also makes them way faster). It also changes from a "show all the memories, assign to groups in the bank editor" model to one similar to the other Kenwood commercial drivers in tree (i.e. sub devices for groups). Tested fairly extensively with a TK-280 and TK-380 (both version 2.0). Fixes #4935 --- chirp/drivers/tk280.py | 1774 +++++++++++++++++++++++++++++++ tests/images/Kenwood_TK-280.img | Bin 0 -> 32957 bytes 2 files changed, 1774 insertions(+) create mode 100644 chirp/drivers/tk280.py create mode 100644 tests/images/Kenwood_TK-280.img 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)] diff --git a/tests/images/Kenwood_TK-280.img b/tests/images/Kenwood_TK-280.img new file mode 100644 index 0000000000000000000000000000000000000000..f6bc6c13d07bb717fc635e20874769bf26143c8f GIT binary patch literal 32957 zcmeI)&rTCT9KiA4&?2TGkwa~-RjME=Znw0s#)JHK8%PULTJ|KAe+w-VQKY;8S9t^D z6Zi(cg1&)AFB(ZmOeDC@t`Z6ed|PPe{w&$-5$~ZIBWUjefX6^CzDr~-hTgW#)4t?8DVet zcyOwx+b~lZYbcRO^k@6J^yzr`5A}##2xjRWc!?`l+mqL>cXZz9x_Rq%_nmxi-`)KE z{E6~9f0XM)K>v_}#6(mqYf9m9RAeTaNlU?si&*iDsjs4=67y?Ky4DoSrbWWK7S?S? zS)U_oF3P0{qEH8dM!URu<)olvNBV& zwruysqP^-=W~-AMW!KK?{$oxtlb$uLl^|A4Z8)P->;5(D%H-JCL%U~u(Dw71)1}|z@ literal 0 HcmV?d00001