1
|
# Copyright 2019 Dan Clemmensen <DanClemmensen@Gmail.com>
|
2
|
# Derives loosely from two sources released under GPLv2:
|
3
|
# ./template.py, Copyright 2012 Dan Smith <dsmith@danplanet.com>
|
4
|
# ./ft60.py, Copyright 2011 Dan Smith <dsmith@danplanet.com>
|
5
|
# Edited 2020 Bernhard Hailer AE6YN <ham73tux@gmail.com>
|
6
|
#
|
7
|
# This program is free software: you can redistribute it and/or modify
|
8
|
# it under the terms of the GNU General Public License as published by
|
9
|
# the Free Software Foundation, either version 3 of the License, or
|
10
|
# (at your option) any later version.
|
11
|
#
|
12
|
# This program is distributed in the hope that it will be useful,
|
13
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
# GNU General Public License for more details.
|
16
|
#
|
17
|
# You should have received a copy of the GNU General Public License
|
18
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
19
|
|
20
|
"""
|
21
|
CHIRP driver for Yaesu radios that use the SCU-35 cable. This includes at
|
22
|
least the FT-4X, FT-4V, FT-65, and FT-25. This driver will not work with older
|
23
|
Yaesu models.
|
24
|
"""
|
25
|
import logging
|
26
|
import struct
|
27
|
import copy
|
28
|
from chirp import chirp_common, directory, memmap, bitwise, errors, util
|
29
|
from chirp.settings import RadioSetting, RadioSettingGroup, \
|
30
|
RadioSettingValueList, RadioSettingValueString, RadioSettings
|
31
|
|
32
|
LOG = logging.getLogger(__name__)
|
33
|
|
34
|
|
35
|
# Layout of Radio memory image.
|
36
|
# This module and the serial protocol treat the FT-4 memory as 16-byte blocks.
|
37
|
# There in nothing magic about 16-byte blocks, but it simplifies the
|
38
|
# description. There are 17 groups of blocks, each with a different purpose
|
39
|
# and format. Five groups consist of channel memories, or "mems" in CHIRP.
|
40
|
# A "mem" describes a radio channel, and all "mems" have the same internal
|
41
|
# format. Three of the groups consist of bitmaps, which all have the same
|
42
|
# internal mapping. also groups for Name, misc, DTMF digits, and prog,
|
43
|
# plus some unused groups. The MEM_FORMAT describes the radio image memory.
|
44
|
# MEM_FORMAT is parsed in module ../bitwise.py. Syntax is similar but not
|
45
|
# identical to C data and structure definitions.
|
46
|
|
47
|
# Define the structures for each type of group here, but do not associate them
|
48
|
# with actual memory addresses yet
|
49
|
MEM_FORMAT = """
|
50
|
struct slot {
|
51
|
u8 tx_pwr; //0, 1, 2 == lo, medium, high
|
52
|
bbcd freq[4]; // Hz/10 but must end in 00
|
53
|
u8 tx_ctcss; //see ctcss table, but radio code = CHIRP code+1. 0==off
|
54
|
u8 rx_ctcss; //see ctcss table, but radio code = CHIRP code+1. 0==off
|
55
|
u8 tx_dcs; //see dcs table, but radio code = CHIRP code+1. 0==off
|
56
|
u8 rx_dcs; //see dcs table, but radio code = CHIRP code+1. 0==off
|
57
|
u8 duplex; //(auto,offset). (0,2,4,5)= (+,-,0, auto)
|
58
|
ul16 offset; //little-endian binary * scaler, +- per duplex
|
59
|
//scaler is 25 kHz for FT-4, 50 kHz for FT-65.
|
60
|
u8 tx_width; //0=wide, 1=narrow
|
61
|
u8 step; //STEPS (0-9)=(auto,5,6.25,10,12.5,15,20,25,50,100) kHz
|
62
|
u8 sql_type; //(0-6)==(off,r-tone,t-tone,tsql,rev tn,dcs,pager)
|
63
|
u8 unused;
|
64
|
};
|
65
|
// one bit per channel. 220 bits (200 mem+ 2*10 PMS) padded to fill
|
66
|
//exactly 2 blocks
|
67
|
struct bitmap {
|
68
|
u8 b8[28];
|
69
|
u8 unused[4];
|
70
|
};
|
71
|
//name struct occupies half a block (8 bytes)
|
72
|
//the code restricts the actual len to 6 for an FT-4
|
73
|
struct name {
|
74
|
u8 chrs[8]; //[0-9,A-z,a-z, -] padded with spaces
|
75
|
};
|
76
|
//txfreq struct occupies 4 bytes (1/4 slot)
|
77
|
struct txfreq {
|
78
|
bbcd freq[4];
|
79
|
};
|
80
|
|
81
|
//miscellaneous params. One 4-block group.
|
82
|
//"SMI": "Set Mode Index" of the FT-4 radio keypad function to set parameter.
|
83
|
//"SMI numbers on the FT-65 are different but the names in mem are the same.
|
84
|
struct misc {
|
85
|
u8 apo; //SMI 01. 0==off, (1-24) is the number of half-hours.
|
86
|
u8 arts_beep; //SMI 02. 0==off, 1==inrange, 2==always
|
87
|
u8 arts_intv; //SMI 03. 0==25 seconds, 1==15 seconds
|
88
|
u8 battsave; //SMI 32. 0==off, (1-5)==(200,300,500,1000,2000) ms
|
89
|
u8 bclo; //SMI 04. 0==off, 1==on
|
90
|
u8 beep; //SMI 05. (0-2)==(key+scan,key, off)
|
91
|
u8 bell; //SMI 06. (0-5)==(0,1,3,5,8,continuous) bells
|
92
|
u8 cw_id[6]; //SMI 08. callsign (A_Z,0-9) (pad with space if <6)
|
93
|
u8 unknown1[3];
|
94
|
// addr= 2010
|
95
|
u8 dtmf_mode; //SMI 12. 0==manual, 1==auto
|
96
|
u8 dtmf_delay; //SMI 11. (0-4)==(50,250,450,750,1000) ms
|
97
|
u8 dtmf_speed; //SMI 13. (0,1)=(50,100) ms
|
98
|
u8 edg_beep; //SMI 14. 0==off, 1==on
|
99
|
u8 key_lock; //SMI 18. (0-2)==(key,ptt,key+ptt)
|
100
|
u8 lamp; //SMI 15. (0-4)==(5,10,30,continuous,off) secKEY
|
101
|
u8 tx_led; //SMI 17. 0==off, 1==on
|
102
|
u8 bsy_led; //SMI 16. 0==off, 1==on
|
103
|
u8 moni_tcall; //SMI 19. (0-4)==(mon,1750,2100,1000,1450) tcall Hz.
|
104
|
u8 pri_rvt; //SMI 23. 0==off, 1==on
|
105
|
u8 scan_resume; //SMI 34. (0-2)==(busy,hold,time)
|
106
|
u8 rf_squelch; //SMI 28. 0==off, 8==full, (1-7)==(S1-S7)
|
107
|
u8 scan_lamp; //SMI 33 0==off,1==on
|
108
|
u8 unknown2;
|
109
|
u8 use_cwid; //SMI 7. 0==no, 1==yes
|
110
|
u8 compander; // compander on FT_65
|
111
|
// addr 2020
|
112
|
u8 unknown3;
|
113
|
u8 tx_save; //SMI 41. 0==off, 1==on (addr==2021)
|
114
|
u8 vfo_spl; //SMI 42. 0==off, 1==on
|
115
|
u8 vox; //SMI 43. 0==off, 1==on
|
116
|
u8 wfm_rcv; //SMI 44. 0==off, 1==on
|
117
|
u8 unknown4;
|
118
|
u8 wx_alert; //SMI 46. 0==off, 1==0n
|
119
|
u8 tot; //SMI 39. 0-off, (1-30)== (1-30) minutes
|
120
|
u8 pager_tx1; //SMI 21. (0-49)=(1-50) epcs code (i.e., value is code-1)
|
121
|
u8 pager_tx2; //SMI 21 same
|
122
|
u8 pager_rx1; //SMI 23 same
|
123
|
u8 pager_rx2; //SMI 23 same
|
124
|
u8 pager_ack; //SMI 22 same
|
125
|
u8 unknown5[3]; //possibly sql_setting and pgm_vfo_scan on FT-65?
|
126
|
// addr 2030
|
127
|
u8 use_passwd; //SMI 26 0==no, 1==yes
|
128
|
u8 passwd[4]; //SMI 27 ASCII (0-9)
|
129
|
u8 unused2[11]; // pad out to a block boundary
|
130
|
};
|
131
|
|
132
|
struct dtmfset {
|
133
|
u8 digit[16]; //ASCII (*,#,-,0-9,A-D). (dash-filled)
|
134
|
};
|
135
|
|
136
|
// area to be filled with 0xff, or just ignored
|
137
|
struct notused {
|
138
|
u8 unused[16];
|
139
|
};
|
140
|
// areas we are still analyzing
|
141
|
struct unknown {
|
142
|
u8 notknown[16];
|
143
|
};
|
144
|
|
145
|
struct progc {
|
146
|
u8 usage; //P key is (0-2) == unused, use P channel, use parm
|
147
|
u8 submode:2, //if parm!=0 submode 0-3 of mode
|
148
|
parm:6; //if usage == 2: if 0, use m-channel, else mode
|
149
|
};
|
150
|
"""
|
151
|
# Actual memory layout. 0x215 blocks, in 20 groups.
|
152
|
MEM_FORMAT += """
|
153
|
#seekto 0x0000;
|
154
|
struct unknown radiotype; //0000 probably a radio type ID but not sure
|
155
|
struct slot memory[200]; //0010 channel memory array
|
156
|
struct slot pms[20]; //0c90 10 PMS (L,U) slot pairs
|
157
|
struct slot vfo[5]; //0dd0 VFO (A UHF, A VHF, B FM, B UHF, B VHF)
|
158
|
struct slot home[3]; //0e20 Home (FM, VHF, UHF)
|
159
|
struct bitmap enable; //0e50
|
160
|
struct bitmap scan; //0e70
|
161
|
struct notused notused0; //0e90
|
162
|
struct bitmap bankmask[10]; //0ea0
|
163
|
struct notused notused1[2]; //0fe0
|
164
|
struct name names[220]; //1000 220 names in 110 blocks
|
165
|
struct notused notused2[2]; //16e0
|
166
|
struct txfreq txfreqs[220]; //1700 220 freqs in 55 blocks
|
167
|
struct notused notused3[89]; //1a20
|
168
|
struct misc settings; //2000 4-block collection of misc params
|
169
|
struct notused notused4[2]; //2040
|
170
|
struct dtmfset dtmf[9]; //2060 sets 1-9
|
171
|
struct notused notused5; //20f0
|
172
|
//struct progs progkeys; //2100 not a struct. bitwise.py refuses
|
173
|
struct progc progctl[4]; //2100 8 bytes. 1 2-byte struct per P key
|
174
|
u8 pmemndx[4]; //2108 4 bytes, 1 per P key
|
175
|
u8 notused6[4]; //210c fill out the block
|
176
|
struct slot prog[4]; //2110 P key "channel" array
|
177
|
//---------------- end of FT-4 mem?
|
178
|
"""
|
179
|
# The remaining mem is (apparently) not available on the FT4 but is
|
180
|
# reported to be available on the FT-65. Not implemented here yet.
|
181
|
# Possibly, memory-mapped control registers that allow for "live-mode"
|
182
|
# operation instead of "clone-mode" operation.
|
183
|
# 2150 27ff (unused?)
|
184
|
# 2800 285f 6 MRU operation?
|
185
|
# 2860 2fff (unused?)
|
186
|
# 3000 310f 17 (version info, etc?)
|
187
|
# ----------END of memory map
|
188
|
|
189
|
|
190
|
# Begin Serial transfer utilities for the SCU-35 cable.
|
191
|
|
192
|
# The serial transfer protocol was implemented here after snooping the wire.
|
193
|
# After it was implemented, we noticed that it is identical to the protocol
|
194
|
# implemented in th9000.py. A non-echo version is implemented in anytone_ht.py.
|
195
|
#
|
196
|
# The pipe.read and pipe.write functions use bytes, not strings. The serial
|
197
|
# transfer utilities operate only to move data between the memory object and
|
198
|
# the serial port. The code runs on either Python 2 or Python3, so some
|
199
|
# constructs could be better optimized for one or the other, but not both.
|
200
|
|
201
|
|
202
|
def get_mmap_data(radio):
|
203
|
"""
|
204
|
horrible kludge needed until we convert entirely to Python 3 OR we add a
|
205
|
slightly less horrible kludge to the Py2 or Py3 versions of memmap.py.
|
206
|
The minimal change have Py3 code return a bytestring instead of a string.
|
207
|
This is the only function in this module that must explicitly test for the
|
208
|
data string type. It is used only in the do_upload function.
|
209
|
returns the memobj data as a byte-like object.
|
210
|
"""
|
211
|
data = radio.get_mmap().get_packed()
|
212
|
if isinstance(data, bytes):
|
213
|
return data
|
214
|
return bytearray(radio.get_mmap()._data)
|
215
|
|
216
|
|
217
|
def checkSum8(data):
|
218
|
"""
|
219
|
Calculate the 8 bit checksum of buffer
|
220
|
Input: buffer - bytes
|
221
|
returns: integer
|
222
|
"""
|
223
|
return sum(x for x in bytearray(data)) & 0xFF
|
224
|
|
225
|
|
226
|
def variable_len_resp(pipe):
|
227
|
"""
|
228
|
when length of expected reply is not known, read byte at a time
|
229
|
until the ack character is found.
|
230
|
"""
|
231
|
response = b""
|
232
|
i = 0
|
233
|
toolong = 256 # arbitrary
|
234
|
while True:
|
235
|
b = pipe.read(1)
|
236
|
if b == b'\x06':
|
237
|
break
|
238
|
else:
|
239
|
response += b
|
240
|
i += 1
|
241
|
if i > toolong:
|
242
|
LOG.debug("Response too long. got" + util.hexprint(response))
|
243
|
raise errors.RadioError("Response from radio too long.")
|
244
|
return(response)
|
245
|
|
246
|
|
247
|
def sendcmd(pipe, cmd, response_len):
|
248
|
"""
|
249
|
send a command bytelist to radio,receive and return the resulting bytelist.
|
250
|
Input: pipe - serial port object to use
|
251
|
cmd - bytes to send
|
252
|
response_len - number of bytes of expected response,
|
253
|
not including the ACK. (if None, read until ack)
|
254
|
This cable is "two-wire": The TxD and RxD are "or'ed" so we receive
|
255
|
whatever we send and then whatever response the radio sends. We check the
|
256
|
echo and strip it, returning only the radio's response.
|
257
|
We also check and strip the ACK character at the end of the response.
|
258
|
"""
|
259
|
pipe.write(cmd)
|
260
|
echo = pipe.read(len(cmd))
|
261
|
if echo != cmd:
|
262
|
msg = "Bad echo. Sent:" + util.hexprint(cmd) + ", "
|
263
|
msg += "Received:" + util.hexprint(echo)
|
264
|
LOG.debug(msg)
|
265
|
raise errors.RadioError(
|
266
|
"Incorrect echo on serial port. Radio off? Bad cable?")
|
267
|
if response_len is None:
|
268
|
return variable_len_resp(pipe)
|
269
|
if response_len > 0:
|
270
|
response = pipe.read(response_len)
|
271
|
else:
|
272
|
response = b""
|
273
|
ack = pipe.read(1)
|
274
|
if ack != b'\x06':
|
275
|
LOG.debug("missing ack: expected 0x06, got" + util.hexprint(ack))
|
276
|
raise errors.RadioError("Incorrect ACK on serial port.")
|
277
|
return response
|
278
|
|
279
|
|
280
|
def enter_clonemode(radio):
|
281
|
"""
|
282
|
Send the PROGRAM command and check the response. Retry if
|
283
|
needed. After 3 tries, send an "END" and try some more if
|
284
|
it is acknowledged.
|
285
|
"""
|
286
|
for use_end in range(0, 3):
|
287
|
for i in range(0, 3):
|
288
|
try:
|
289
|
if b"QX" == sendcmd(radio.pipe, b"PROGRAM", 2):
|
290
|
return
|
291
|
except:
|
292
|
continue
|
293
|
sendcmd(radio.pipe, b"END", 0)
|
294
|
raise errors.RadioError("expected QX from radio.")
|
295
|
|
296
|
|
297
|
def startcomms(radio, way):
|
298
|
"""
|
299
|
For either upload or download, put the radio into PROGRAM mode
|
300
|
and check the radio's ID. In this preliminary version of the driver,
|
301
|
the exact nature of the ID has been inferred from a single test case.
|
302
|
set up the progress bar
|
303
|
send "PROGRAM" to command the radio into clone mode
|
304
|
read the initial string (version?)
|
305
|
"""
|
306
|
progressbar = chirp_common.Status()
|
307
|
progressbar.msg = "Cloning " + way + " radio"
|
308
|
progressbar.max = radio.numblocks
|
309
|
enter_clonemode(radio)
|
310
|
id_response = sendcmd(radio.pipe, b'\x02', None)
|
311
|
|
312
|
# The last byte of the ID contains info about regional differences
|
313
|
radio.subtype = id_response[len(id_response) - 1]
|
314
|
# Set last byte of the ID zero so that no ID warning is thrown
|
315
|
# Unfortunately, the returned object is an immutable bytes object,
|
316
|
# so we need to convert into a bytearray, modify, and convert back.
|
317
|
ba_id_response = bytearray(id_response)
|
318
|
ba_id_response[len(ba_id_response) - 1] = 0
|
319
|
id_response_mod = bytes(ba_id_response)
|
320
|
|
321
|
if id_response != radio.id_str:
|
322
|
substr0 = radio.id_str[:radio.id_str.find(b'\x00')]
|
323
|
if id_response[:id_response.find(b'\x00')] != substr0:
|
324
|
msg = "ID mismatch. Expected" + util.hexprint(radio.id_str)
|
325
|
msg += ", Received:" + util.hexprint(id_response_mod)
|
326
|
LOG.warning(msg)
|
327
|
raise errors.RadioError("Incorrect ID read from radio.")
|
328
|
else:
|
329
|
msg = "ID suspect. Expected" + util.hexprint(radio.id_str)
|
330
|
msg += ", Received:" + util.hexprint(id_response_mod)
|
331
|
LOG.warning(msg)
|
332
|
return progressbar
|
333
|
|
334
|
|
335
|
def getblock(pipe, addr, image):
|
336
|
"""
|
337
|
read a single 16-byte block from the radio.
|
338
|
send the command and check the response
|
339
|
places the response into the correct offset in the supplied bytearray
|
340
|
returns True if successful, False if error.
|
341
|
"""
|
342
|
cmd = struct.pack(">cHb", b"R", addr, 16)
|
343
|
response = sendcmd(pipe, cmd, 21)
|
344
|
if (response[0] != b"W"[0]) or (response[1:4] != cmd[1:4]):
|
345
|
msg = "Bad response. Sent:\n%s" % util.hexprint(cmd)
|
346
|
msg += "\nReceived:\n%s" % util.hexprint(response)
|
347
|
LOG.debug(msg)
|
348
|
return False
|
349
|
if checkSum8(response[1:20]) != bytearray(response)[20]:
|
350
|
LOG.debug("Bad checksum:\n%s" % util.hexprint(response))
|
351
|
LOG.debug('%r != %r' % (checkSum8(response[1:20]),
|
352
|
bytearray(response)[20]))
|
353
|
return False
|
354
|
image[addr:addr+16] = response[4:20]
|
355
|
return True
|
356
|
|
357
|
|
358
|
def do_download(radio):
|
359
|
"""
|
360
|
Read memory from the radio.
|
361
|
call startcomms to go into program mode and check version
|
362
|
create an mmap
|
363
|
read the memory blocks and place the data into the mmap
|
364
|
send "END"
|
365
|
"""
|
366
|
image = bytearray(radio.get_memsize())
|
367
|
pipe = radio.pipe # Get the serial port connection
|
368
|
progressbar = startcomms(radio, "from")
|
369
|
for blocknum in range(radio.numblocks):
|
370
|
for i in range(0, 3):
|
371
|
if getblock(pipe, 16 * blocknum, image):
|
372
|
break
|
373
|
if i == 2:
|
374
|
raise errors.RadioError(
|
375
|
"read block from radio failed 3 times")
|
376
|
progressbar.cur = blocknum
|
377
|
radio.status_fn(progressbar)
|
378
|
sendcmd(pipe, b"END", 0)
|
379
|
return memmap.MemoryMap(bytes(image))
|
380
|
|
381
|
|
382
|
def putblock(pipe, addr, data):
|
383
|
"""
|
384
|
write a single 16-byte block to the radio
|
385
|
send the command and check the response
|
386
|
"""
|
387
|
chkstr = struct.pack(">Hb", addr, 16) + data
|
388
|
msg = b'W' + chkstr + struct.pack('B', checkSum8(chkstr)) + b'\x06'
|
389
|
sendcmd(pipe, msg, 0)
|
390
|
|
391
|
|
392
|
def do_upload(radio):
|
393
|
"""
|
394
|
Write memory image to radio
|
395
|
call startcomms to go into program mode and check version
|
396
|
write the memory blocks. Skip the first block
|
397
|
send "END"
|
398
|
"""
|
399
|
pipe = radio.pipe # Get the serial port connection
|
400
|
progressbar = startcomms(radio, "to")
|
401
|
data = get_mmap_data(radio)
|
402
|
for _i in range(1, radio.numblocks):
|
403
|
putblock(pipe, 16*_i, data[16*_i:16*(_i+1)])
|
404
|
progressbar.cur = _i
|
405
|
radio.status_fn(progressbar)
|
406
|
sendcmd(pipe, b"END", 0)
|
407
|
return
|
408
|
# End serial transfer utilities
|
409
|
|
410
|
|
411
|
def bit_loc(bitnum):
|
412
|
"""
|
413
|
return the ndx and mask for a bit location
|
414
|
"""
|
415
|
return (bitnum // 8, 1 << (bitnum & 7))
|
416
|
|
417
|
|
418
|
def store_bit(bankmem, bitnum, val):
|
419
|
"""
|
420
|
store a bit in a bankmem. Store 0 or 1 for False or True
|
421
|
"""
|
422
|
ndx, mask = bit_loc(bitnum)
|
423
|
if val:
|
424
|
bankmem.b8[ndx] |= mask
|
425
|
else:
|
426
|
bankmem.b8[ndx] &= ~mask
|
427
|
return
|
428
|
|
429
|
|
430
|
def retrieve_bit(bankmem, bitnum):
|
431
|
"""
|
432
|
return True or False for a bit in a bankmem
|
433
|
"""
|
434
|
ndx, mask = bit_loc(bitnum)
|
435
|
return (bankmem.b8[ndx] & mask) != 0
|
436
|
|
437
|
|
438
|
# A bank is a bitmap of 220 bits. 200 mem slots and 2*10 PMS slots.
|
439
|
# There are 10 banks.
|
440
|
class YaesuSC35GenericBankModel(chirp_common.BankModel):
|
441
|
|
442
|
def get_num_mappings(self):
|
443
|
return 10
|
444
|
|
445
|
def get_mappings(self):
|
446
|
banks = []
|
447
|
for i in range(1, 1 + self.get_num_mappings()):
|
448
|
bank = chirp_common.Bank(self, "%i" % i, "Bank %i" % i)
|
449
|
bank.index = i - 1
|
450
|
banks.append(bank)
|
451
|
return banks
|
452
|
|
453
|
def add_memory_to_mapping(self, memory, bank):
|
454
|
bankmem = self._radio._memobj.bankmask[bank.index]
|
455
|
store_bit(bankmem, memory.number-1, True)
|
456
|
|
457
|
def remove_memory_from_mapping(self, memory, bank):
|
458
|
bankmem = self._radio._memobj.bankmask[bank.index]
|
459
|
if not retrieve_bit(bankmem, memory.number-1):
|
460
|
raise Exception("Memory %i is not in bank %s." %
|
461
|
(memory.number, bank))
|
462
|
store_bit(bankmem, memory.number-1, False)
|
463
|
|
464
|
# return a list of slots in a bank
|
465
|
def get_mapping_memories(self, bank):
|
466
|
memories = []
|
467
|
for i in range(*self._radio.get_features().memory_bounds):
|
468
|
if retrieve_bit(self._radio._memobj.bankmask[bank.index], i - 1):
|
469
|
memories.append(self._radio.get_memory(i))
|
470
|
return memories
|
471
|
|
472
|
# return a list of banks a slot is a member of
|
473
|
def get_memory_mappings(self, memory):
|
474
|
memndx = memory.number - 1
|
475
|
banks = []
|
476
|
for bank in self.get_mappings():
|
477
|
if retrieve_bit(self._radio._memobj.bankmask[bank.index], memndx):
|
478
|
banks.append(bank)
|
479
|
return banks
|
480
|
|
481
|
# the values in these lists must also be in the canonical UI list
|
482
|
# we can re-arrange the order, and we don't need to have all
|
483
|
# the values, but we cannot add our own values here.
|
484
|
|
485
|
DUPLEX = ["+", "", "-", "", "off", "", "split"] # (0,2,4,5) = (+, -, 0, auto)
|
486
|
# The radio implements duplex "auto" as 5. We map to "". It is a convenience
|
487
|
# function in the radio that affects the offset, which sets the duplex value
|
488
|
# according to the frequency in use. Yaesu doesn't entirely adhere to the band
|
489
|
# plans; but worse, they save the value 'auto' instead of + or -. Why Yaesu
|
490
|
# is doing such a thing is beyond me. [AE6YN]
|
491
|
DUPLEX_AUTO_US = [
|
492
|
[145100000, 145495000, 2],
|
493
|
[146000000, 146395000, 0],
|
494
|
[146600000, 146995000, 2],
|
495
|
[147000000, 147395000, 0],
|
496
|
[147600000, 147995000, 2]]
|
497
|
# (There are no automatic duplex values in IARU-2 70CM.)
|
498
|
DUPLEX_AUTO_EU = [
|
499
|
[145600000, 145800000, 2],
|
500
|
[438200000, 439425000, 2]]
|
501
|
|
502
|
SKIPS = ["S", ""]
|
503
|
|
504
|
POWER_LEVELS = [
|
505
|
chirp_common.PowerLevel("High", watts=5.0), # high must be first (0)
|
506
|
chirp_common.PowerLevel("Mid", watts=2.5),
|
507
|
chirp_common.PowerLevel("Low", watts=0.5)]
|
508
|
|
509
|
# these steps encode to 0-9 on all radios, but encoding #2 is disallowed
|
510
|
# on the US versions (FT-4XR)
|
511
|
STEP_CODE = [0, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
|
512
|
US_LEGAL_STEPS = list(STEP_CODE) # copy to pass to UI on US radios
|
513
|
US_LEGAL_STEPS.remove(6.25) # euro radios just use STEP_CODE
|
514
|
|
515
|
# Map the radio image sql_type (0-6) to the CHIRP mem values.
|
516
|
# Yaesu "TSQL" and "DCS" each map to different CHIRP values depending on the
|
517
|
# radio values of the tx and rx tone codes. The table is a list of rows, one
|
518
|
# per Yaesu sql_type (0-5). The code does not use this table when the sql_type
|
519
|
# is 6 (PAGER). Each row is a tuple. Its first member is a list of
|
520
|
# [tmode,cross] or [tmode, cross, suppress]. "Suppress" is used only when
|
521
|
# encoding UI-->radio. When decoding radio-->UI, two of the sql_types each
|
522
|
# result in 5 possibible UI decodings depending on the tx and rx codes, and the
|
523
|
# list in each of these rows has five members. These two row tuples each have
|
524
|
# two additional members to specify which of the radio fields to examine.
|
525
|
# The map from CHIRP UI to radio image types is also built from this table.
|
526
|
RADIO_TMODES = [
|
527
|
([["", None], ], ), # sql_type= 0. off
|
528
|
([["Cross", "->Tone"], ], ), # sql_type= 1. R-TONE
|
529
|
([["Tone", None], ], ), # sql_type= 2. T-TONE
|
530
|
([ # sql_type= 3. TSQL:
|
531
|
["", None], # tx==0, rx==0 : invalid
|
532
|
["TSQL", None], # tx==0
|
533
|
["Tone", None], # rx==0
|
534
|
["Cross", "Tone->Tone"], # tx!=rx
|
535
|
["TSQL", None] # tx==rx
|
536
|
], "tx_ctcss", "rx_ctcss"), # tx and rx fields to check
|
537
|
([["TSQL-R", None], ], ), # sql_type= 4. REV TN
|
538
|
([ # sql_type= 5.DCS:
|
539
|
["", None], # tx==0, rx==0 : invalid
|
540
|
["Cross", "->DTCS", "tx_dcs"], # tx==0. suppress tx
|
541
|
["Cross", "DTCS->", "rx_dcs"], # rx==0. suppress rx
|
542
|
["Cross", "DTCS->DTCS"], # tx!=rx
|
543
|
["DTCS", None] # tx==rx
|
544
|
], "tx_dcs", "rx_dcs"), # tx and rx fields to check
|
545
|
# # sql_type= 6. PAGER is a CHIRP "extra"
|
546
|
]
|
547
|
|
548
|
# Find all legal values for the tmode and cross fields for the UI.
|
549
|
# We build two dictionaries to do the lookups when encoding.
|
550
|
# The reversed range is a kludge: by happenstance, earlier duplicates
|
551
|
# in the above table are the preferred mapping, they override the
|
552
|
# later ones when we process the table backwards.
|
553
|
TONE_DICT = {} # encode sql_type.
|
554
|
CROSS_DICT = {} # encode sql_type.
|
555
|
|
556
|
for sql_type in reversed(range(0, len(RADIO_TMODES))):
|
557
|
sql_type_row = RADIO_TMODES[sql_type]
|
558
|
for decode_row in sql_type_row[0]:
|
559
|
suppress = None
|
560
|
if len(decode_row) == 3:
|
561
|
suppress = decode_row[2]
|
562
|
TONE_DICT[decode_row[0]] = (sql_type, suppress)
|
563
|
if decode_row[1]:
|
564
|
CROSS_DICT[decode_row[1]] = (sql_type, suppress)
|
565
|
|
566
|
# The keys are added to the "VALID" lists using code that puts those lists
|
567
|
# in the same order as the UI's default order instead of the random dict
|
568
|
# order or the arbitrary build order.
|
569
|
VALID_TONE_MODES = [] # list for UI.
|
570
|
VALID_CROSS_MODES = [] # list for UI.
|
571
|
for name in chirp_common.TONE_MODES:
|
572
|
if name in TONE_DICT:
|
573
|
VALID_TONE_MODES += [name]
|
574
|
for name in chirp_common.CROSS_MODES:
|
575
|
if name in CROSS_DICT:
|
576
|
VALID_CROSS_MODES += [name]
|
577
|
|
578
|
|
579
|
DTMF_CHARS = "0123456789ABCD*#- "
|
580
|
CW_ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
581
|
PASSWD_CHARS = "0123456789"
|
582
|
CHARSET = CW_ID_CHARS + "abcdefghijklmnopqrstuvwxyz*+-/@"
|
583
|
PMSNAMES = ["%s%02d" % (c, i) for i in range(1, 11) for c in ('L', 'U')]
|
584
|
|
585
|
# Four separate arrays of special channel mems.
|
586
|
# Each special has unique constraints: band, name yes/no, and pms L/U
|
587
|
# The FT-65 class replaces the "prog" entry in this list.
|
588
|
# The name field must be the name of a slot array in MEM_FORMAT
|
589
|
SPECIALS_FT4 = [
|
590
|
("pms", PMSNAMES),
|
591
|
("vfo", ["VFO A UHF", "VFO A VHF", "VFO B FM", "VFO B VHF", "VFO B UHF"]),
|
592
|
("home", ["HOME FM", "HOME VHF", "HOME UHF"]),
|
593
|
("prog", ["P1", "P2"])
|
594
|
]
|
595
|
SPECIALS_FT65 = SPECIALS_FT4
|
596
|
FT65_PROGS = ("prog", ["P1", "P2", "P3", "P4"])
|
597
|
SPECIALS_FT65[-1] = FT65_PROGS # replace the last entry (P key names)
|
598
|
|
599
|
|
600
|
VALID_BANDS_DUAL = [
|
601
|
(65000000, 108000000), # broadcast FM, receive only
|
602
|
(136000000, 174000000), # VHF
|
603
|
(400000000, 480000000) # UHF
|
604
|
]
|
605
|
|
606
|
VALID_BANDS_VHF = [
|
607
|
(65000000, 108000000), # broadcast FM, receive only
|
608
|
(136000000, 174000000), # VHF
|
609
|
]
|
610
|
|
611
|
# bands for the five VFO and three home channel memories
|
612
|
BAND_ASSIGNMENTS_DUALBAND = [2, 1, 0, 1, 2, 0, 1, 2] # all locations used
|
613
|
BAND_ASSIGNMENTS_MONO_VHF = [1, 1, 0, 1, 1, 0, 1, 1] # UHF locations unused
|
614
|
|
615
|
|
616
|
# None, and 50 Tones. Use this explicit array because the
|
617
|
# one in chirp_common could change and no longer describe our radio
|
618
|
TONE_MAP = [
|
619
|
None, 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5,
|
620
|
85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5,
|
621
|
107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
|
622
|
131.8, 136.5, 141.3, 146.2, 151.4, 156.7,
|
623
|
159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
|
624
|
177.3, 179.9, 183.5, 186.2, 189.9, 192.8,
|
625
|
196.6, 199.5, 203.5, 206.5, 210.7, 218.1,
|
626
|
225.7, 229.1, 233.6, 241.8, 250.3, 254.1
|
627
|
]
|
628
|
|
629
|
# None, and 104 DTCS Codes. Use this explicit array because the
|
630
|
# one in chirp_common could change and no longer describe our radio
|
631
|
DTCS_MAP = [
|
632
|
None, 23, 25, 26, 31, 32, 36, 43, 47, 51, 53, 54,
|
633
|
65, 71, 72, 73, 74, 114, 115, 116, 122, 125, 131,
|
634
|
132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174,
|
635
|
205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252,
|
636
|
255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325,
|
637
|
331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412,
|
638
|
413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464,
|
639
|
465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606,
|
640
|
612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723,
|
641
|
731, 732, 734, 743, 754
|
642
|
]
|
643
|
|
644
|
# The legal PAGER codes are the same as the CTCSS codes, but we
|
645
|
# pass them to the UI as a list of strings
|
646
|
EPCS_CODES = [format(flt) for flt in [0] + TONE_MAP[1:]]
|
647
|
|
648
|
|
649
|
# allow a child class to add a param to its class
|
650
|
# description list. used when a specific radio class has
|
651
|
# a param that is not in the generic list.
|
652
|
def add_paramdesc(desc_list, group, param):
|
653
|
for description in desc_list:
|
654
|
groupname, title, parms = description
|
655
|
if group == groupname:
|
656
|
description[2].append(param)
|
657
|
return
|
658
|
|
659
|
|
660
|
class YaesuSC35GenericRadio(chirp_common.CloneModeRadio,
|
661
|
chirp_common.ExperimentalRadio):
|
662
|
"""
|
663
|
Base class for all Yaesu radios using the SCU-35 programming cable
|
664
|
and its protocol. Classes for sub families extend this class and
|
665
|
are found towards the end of this file.
|
666
|
"""
|
667
|
VENDOR = "Yaesu"
|
668
|
MODEL = "SCU-35Generic" # No radio directly uses the base class
|
669
|
BAUD_RATE = 9600
|
670
|
MAX_MEM_SLOT = 200
|
671
|
NEEDS_COMPAT_SERIAL = False
|
672
|
|
673
|
# These settings are common to all radios in this family.
|
674
|
_valid_chars = chirp_common.CHARSET_ASCII
|
675
|
numblocks = 0x215 # number of 16-byte blocks in the radio
|
676
|
_memsize = 16 * numblocks # used by CHIRP file loader to guess radio type
|
677
|
MAX_MEM_SLOT = 200
|
678
|
|
679
|
# ID string last byte can contain extra info. It is usually 0;
|
680
|
# however, on FT25R/65R sold in asia we have seen that it can be 3,
|
681
|
# which affects the scaler: instead of the default 50000 it's then 25000.
|
682
|
subtype = 0
|
683
|
|
684
|
@classmethod
|
685
|
def get_prompts(cls):
|
686
|
rp = chirp_common.RadioPrompts()
|
687
|
rp.experimental = (
|
688
|
'Tested and mostly works, but may give you issues\n'
|
689
|
'when using lesser common radio variants.\n'
|
690
|
'Proceed at your own risk, and let us know about issues!'
|
691
|
)
|
692
|
|
693
|
rp.pre_download = "".join([
|
694
|
"1. Connect programming cable to MIC jack.\n",
|
695
|
"2. Press OK."
|
696
|
]
|
697
|
)
|
698
|
rp.pre_upload = rp.pre_download
|
699
|
return rp
|
700
|
|
701
|
# identify the features that can be manipulated on this radio.
|
702
|
# mentioned here only when different from defaults in chirp_common.py
|
703
|
def get_features(self):
|
704
|
|
705
|
rf = chirp_common.RadioFeatures()
|
706
|
specials = [name for s in self.class_specials for name in s[1]]
|
707
|
rf.valid_special_chans = specials
|
708
|
rf.memory_bounds = (1, self.MAX_MEM_SLOT)
|
709
|
rf.valid_duplexes = DUPLEX
|
710
|
rf.valid_tmodes = VALID_TONE_MODES
|
711
|
rf.valid_cross_modes = VALID_CROSS_MODES
|
712
|
rf.valid_power_levels = POWER_LEVELS
|
713
|
rf.valid_tuning_steps = self.legal_steps
|
714
|
rf.valid_skips = SKIPS
|
715
|
rf.valid_characters = CHARSET
|
716
|
rf.valid_name_length = self.namelen
|
717
|
rf.valid_modes = ["FM", "NFM"]
|
718
|
rf.valid_bands = self.valid_bands
|
719
|
rf.can_odd_split = True
|
720
|
rf.has_ctone = True
|
721
|
rf.has_rx_dtcs = True
|
722
|
rf.has_dtcs_polarity = False # REV TN reverses the tone, not the dcs
|
723
|
rf.has_cross = True
|
724
|
rf.has_settings = True
|
725
|
|
726
|
return rf
|
727
|
|
728
|
def get_bank_model(self):
|
729
|
return YaesuSC35GenericBankModel(self)
|
730
|
|
731
|
# read and parse the radio memory
|
732
|
def sync_in(self):
|
733
|
try:
|
734
|
self._mmap = do_download(self)
|
735
|
except Exception as e:
|
736
|
raise errors.RadioError("Failed to communicate with radio: %s" % e)
|
737
|
self.process_mmap()
|
738
|
|
739
|
# write the memory image to the radio
|
740
|
def sync_out(self):
|
741
|
try:
|
742
|
do_upload(self)
|
743
|
except Exception as e:
|
744
|
raise errors.RadioError("Failed to communicate with radio: %s" % e)
|
745
|
|
746
|
def process_mmap(self):
|
747
|
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
748
|
|
749
|
# There are about 40 settings and most are handled generically in
|
750
|
# get_settings. get_settings invokes these handlers for the few
|
751
|
# that are more complicated.
|
752
|
|
753
|
# callback for setting byte arrays: DTMF[0-9], passwd, and CW_ID
|
754
|
def apply_str_to_bytearray(self, element, obj):
|
755
|
lng = len(obj)
|
756
|
string = (element.value.get_value() + " ")[:lng]
|
757
|
bytes = bytearray(string, "ascii")
|
758
|
for x in range(0, lng): # memobj cannot iterate, so byte-by-byte
|
759
|
obj[x] = bytes[x]
|
760
|
return
|
761
|
|
762
|
# add a string value to the RadioSettings
|
763
|
def get_string_setting(self, obj, valid_chars, desc1, desc2, group):
|
764
|
content = ''
|
765
|
maxlen = len(obj)
|
766
|
for x in range(0, maxlen):
|
767
|
content += chr(obj[x])
|
768
|
val = RadioSettingValueString(0, maxlen, content, True, valid_chars)
|
769
|
rs = RadioSetting(desc1, desc2, val)
|
770
|
rs.set_apply_callback(self.apply_str_to_bytearray, obj)
|
771
|
group.append(rs)
|
772
|
|
773
|
# called when found in the group_descriptions table to handle string value
|
774
|
def get_strset(self, group, parm):
|
775
|
# parm =(paramname, paramtitle,(handler,[handler params])).
|
776
|
objname, title, fparms = parm
|
777
|
myparms = fparms[1]
|
778
|
obj = getattr(self._memobj.settings, objname)
|
779
|
self.get_string_setting(obj, myparms[0], objname, title, group)
|
780
|
|
781
|
# called when found in the group_descriptions table for DTMF strings
|
782
|
def get_dtmfs(self, group, parm):
|
783
|
objname, title, fparms = parm
|
784
|
for i in range(1, 10):
|
785
|
dtmf_digits = self._memobj.dtmf[i - 1].digit
|
786
|
self.get_string_setting(
|
787
|
dtmf_digits, DTMF_CHARS,
|
788
|
"dtmf_%i" % i, "DTMF Autodialer Memory %i" % i, group)
|
789
|
|
790
|
def apply_P(self, element, pnum, memobj):
|
791
|
memobj.progctl[pnum].usage = element.value
|
792
|
|
793
|
def apply_Pmode(self, element, pnum, memobj):
|
794
|
memobj.progctl[pnum].parm = element.value
|
795
|
|
796
|
def apply_Psubmode(self, element, pnum, memobj):
|
797
|
memobj.progctl[pnum].submode = element.value
|
798
|
|
799
|
def apply_Pmem(self, element, pnum, memobj):
|
800
|
memobj.pmemndx[pnum] = element.value
|
801
|
|
802
|
MEMLIST = ["%d" % i for i in range(1, MAX_MEM_SLOT + 1)] + PMSNAMES
|
803
|
USAGE_LIST = ["unused", "P channel", "mode or M channel"]
|
804
|
|
805
|
# called when found in the group_descriptions table
|
806
|
# returns the settings for the programmable keys (P1-P4)
|
807
|
def get_progs(self, group, parm):
|
808
|
_progctl = self._memobj.progctl
|
809
|
_progndx = self._memobj.pmemndx
|
810
|
|
811
|
def get_prog(i, val_list, valndx, sname, longname, f_apply):
|
812
|
k = str(i + 1)
|
813
|
val = val_list[valndx]
|
814
|
valuelist = RadioSettingValueList(val_list, val)
|
815
|
rs = RadioSetting(sname + k, longname + k, valuelist)
|
816
|
rs.set_apply_callback(f_apply, i, self._memobj)
|
817
|
group.append(rs)
|
818
|
for i in range(0, self.Pkeys):
|
819
|
get_prog(i, self.USAGE_LIST, _progctl[i].usage,
|
820
|
"P", "Programmable key ", self.apply_P)
|
821
|
get_prog(i, self.SETMODES, _progctl[i].parm, "modeP",
|
822
|
"mode for Programmable key ", self.apply_Pmode)
|
823
|
get_prog(i, ["0", "1", "2", "3"], _progctl[i].submode, "submodeP",
|
824
|
"submode for Programmable key ", self.apply_Psubmode)
|
825
|
get_prog(i, self.MEMLIST, _progndx[i], "memP",
|
826
|
"mem for Programmable key ", self.apply_Pmem)
|
827
|
# ------------ End of special settings handlers.
|
828
|
|
829
|
# list of group description tuples: [groupame,group title, [param list]].
|
830
|
# A param is a tuple:
|
831
|
# for a simple param: (paramname, paramtitle,[valuename list])
|
832
|
# for a handler param: (paramname, paramtitle,( handler,[handler params]))
|
833
|
# This is a class variable. subclasses msut create a variable named
|
834
|
# class_group_descs. The FT-4 classes simply equate this, but the
|
835
|
# FT-65 classes must copy and modify this.
|
836
|
group_descriptions = [
|
837
|
("misc", "Miscellaneous Settings", [ # misc
|
838
|
("apo", "Automatic Power Off",
|
839
|
["OFF"] + ["%0.1f" % (x * 0.5) for x in range(1, 24 + 1)]),
|
840
|
("bclo", "Busy Channel Lock-Out", ["OFF", "ON"]),
|
841
|
("beep", "Enable the Beeper", ["KEY+SC", "KEY", "OFF"]),
|
842
|
("bsy_led", "Busy LED", ["ON", "OFF"]),
|
843
|
("edg_beep", "Band Edge Beeper", ["OFF", "ON"]),
|
844
|
("vox", "VOX", ["OFF", "ON"]),
|
845
|
("rf_squelch", "RF Squelch Threshold",
|
846
|
["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6", "S-7", "S-FULL"]),
|
847
|
("tot", "Timeout Timer",
|
848
|
["OFF"] + ["%dMIN" % (x) for x in range(1, 30 + 1)]),
|
849
|
("tx_led", "TX LED", ["OFF", "ON"]),
|
850
|
("use_cwid", "use CW ID", ["NO", "YES"]),
|
851
|
("cw_id", "CW ID Callsign", (get_strset, [CW_ID_CHARS])), # handler
|
852
|
("vfo_spl", "VFO Split", ["OFF", "ON"]),
|
853
|
("wfm_rcv", "Enable Broadband FM", ["OFF", "ON"]),
|
854
|
("passwd", "Password", (get_strset, [PASSWD_CHARS])) # handler
|
855
|
]),
|
856
|
("arts", "ARTS Settings", [ # arts
|
857
|
("arts_beep", "ARTS BEEP", ["OFF", "INRANG", "ALWAYS"]),
|
858
|
("arts_intv", "ARTS Polling Interval", ["25 SEC", "15 SEC"])
|
859
|
]),
|
860
|
("ctcss", "CTCSS/DCS/DTMF Settings", [ # ctcss
|
861
|
("bell", "Bell Repetitions", ["OFF", "1T", "3T", "5T", "8T", "CONT"]),
|
862
|
("dtmf_mode", "DTMF Mode", ["Manual", "Auto"]),
|
863
|
("dtmf_delay", "DTMF Autodialer Delay Time",
|
864
|
["50 MS", "100 MS", "250 MS", "450 MS", "750 MS", "1000 MS"]),
|
865
|
("dtmf_speed", "DTMF Autodialer Sending Speed", ["50 MS", "100 MS"]),
|
866
|
("dtmf", "DTMF Autodialer Memory ", (get_dtmfs, [])) # handler
|
867
|
]),
|
868
|
("switch", "Switch/Knob Settings", [ # switch
|
869
|
("lamp", "Lamp Mode", ["5SEC", "10SEC", "30SEC", "KEY", "OFF"]),
|
870
|
("moni_tcall", "MONI Switch Function",
|
871
|
["MONI", "1750", "2100", "1000", "1450"]),
|
872
|
("key_lock", "Lock Function", ["KEY", "PTT", "KEY+PTT"]),
|
873
|
("Pkeys", "Pkey fields", (get_progs, []))
|
874
|
]),
|
875
|
("scan", "Scan Settings", [ # scan
|
876
|
("scan_resume", "Scan Resume Mode", ["BUSY", "HOLD", "TIME"]),
|
877
|
("pri_rvt", "Priority Revert", ["OFF", "ON"]),
|
878
|
("scan_lamp", "Scan Lamp", ["OFF", "ON"]),
|
879
|
("wx_alert", "Weather Alert Scan", ["OFF", "ON"])
|
880
|
]),
|
881
|
("power", "Power Saver Settings", [ # power
|
882
|
("battsave", "Receive Mode Battery Save Interval",
|
883
|
["OFF", "200 MS", "300 MS", "500 MS", "1 S", "2 S"]),
|
884
|
("tx_save", "Transmitter Battery Saver", ["OFF", "ON"])
|
885
|
]),
|
886
|
("eai", "EAI/EPCS Settings", [ # eai
|
887
|
("pager_tx1", "TX pager frequency 1", EPCS_CODES),
|
888
|
("pager_tx2", "TX pager frequency 2", EPCS_CODES),
|
889
|
("pager_rx1", "RX pager frequency 1", EPCS_CODES),
|
890
|
("pager_rx2", "RX pager frequency 2", EPCS_CODES),
|
891
|
("pager_ack", "Pager answerback", ["NO", "YES"])
|
892
|
])
|
893
|
]
|
894
|
# ----------------end of group_descriptions
|
895
|
|
896
|
# returns the current values of all the settings in the radio memory image,
|
897
|
# in the form of a RadioSettings list. Uses the class_group_descs
|
898
|
# list to create the groups and params. Valuelist scalars are handled
|
899
|
# inline. More complex params are built by calling the special handlers.
|
900
|
def get_settings(self):
|
901
|
_settings = self._memobj.settings
|
902
|
groups = RadioSettings()
|
903
|
for description in self.class_group_descs:
|
904
|
groupname, title, parms = description
|
905
|
group = RadioSettingGroup(groupname, title)
|
906
|
groups.append(group)
|
907
|
for parm in parms:
|
908
|
param, title, opts = parm
|
909
|
try:
|
910
|
if isinstance(opts, list):
|
911
|
# setting is a single value from the list
|
912
|
objval = getattr(_settings, param)
|
913
|
value = opts[objval]
|
914
|
valuelist = RadioSettingValueList(opts, value)
|
915
|
group.append(RadioSetting(param, title, valuelist))
|
916
|
else:
|
917
|
# setting needs special handling. opts[0] is a
|
918
|
# function name
|
919
|
opts[0](self, group, parm)
|
920
|
except Exception as e:
|
921
|
LOG.debug(
|
922
|
"%s: cannot set %s to %s" % (e, param, repr(objval))
|
923
|
)
|
924
|
return groups
|
925
|
# end of get_settings
|
926
|
|
927
|
# modify settings values in the radio memory image
|
928
|
def set_settings(self, uisettings):
|
929
|
_settings = self._memobj.settings
|
930
|
for element in uisettings:
|
931
|
if not isinstance(element, RadioSetting):
|
932
|
self.set_settings(element)
|
933
|
continue
|
934
|
if not element.changed():
|
935
|
continue
|
936
|
|
937
|
try:
|
938
|
name = element.get_name()
|
939
|
value = element.value
|
940
|
|
941
|
if element.has_apply_callback():
|
942
|
LOG.debug("Using apply callback")
|
943
|
element.run_apply_callback()
|
944
|
else:
|
945
|
setattr(_settings, name, value)
|
946
|
|
947
|
LOG.debug("Setting %s: %s" % (name, value))
|
948
|
except:
|
949
|
LOG.debug(element.get_name())
|
950
|
raise
|
951
|
|
952
|
# maps a boolean pair (tx==0,rx==0) to the numbers 0-3
|
953
|
LOOKUP = [[True, True], [True, False], [False, True], [False, False]]
|
954
|
|
955
|
def decode_sql(self, mem, chan):
|
956
|
"""
|
957
|
examine the radio channel fields and determine the correct
|
958
|
CHIRP CSV values for tmode, cross_mode, and sql_override
|
959
|
"""
|
960
|
mem.extra = RadioSettingGroup("Extra", "extra")
|
961
|
extra_modes = ["(None)", "PAGER"]
|
962
|
value = extra_modes[chan.sql_type == 6]
|
963
|
valuelist = RadioSettingValueList(extra_modes, value)
|
964
|
rs = RadioSetting("sql_override", "Squelch override", valuelist)
|
965
|
mem.extra.append(rs)
|
966
|
if chan.sql_type == 6:
|
967
|
return
|
968
|
sql_map = RADIO_TMODES[chan.sql_type]
|
969
|
ndx = 0
|
970
|
if len(sql_map[0]) > 1:
|
971
|
# the sql_type is TSQL or DCS, so there are multiple UI mappings
|
972
|
x = getattr(chan, sql_map[1])
|
973
|
r = getattr(chan, sql_map[2])
|
974
|
ndx = self.LOOKUP.index([x == 0, r == 0])
|
975
|
if ndx == 3 and x == r:
|
976
|
ndx = 4
|
977
|
mem.tmode = sql_map[0][ndx][0]
|
978
|
cross = sql_map[0][ndx][1]
|
979
|
if cross:
|
980
|
mem.cross_mode = cross
|
981
|
if chan.rx_ctcss:
|
982
|
mem.ctone = TONE_MAP[chan.rx_ctcss]
|
983
|
if chan.tx_ctcss:
|
984
|
mem.rtone = TONE_MAP[chan.tx_ctcss]
|
985
|
if chan.tx_dcs:
|
986
|
mem.dtcs = DTCS_MAP[chan.tx_dcs]
|
987
|
if chan.rx_dcs:
|
988
|
mem.rx_dtcs = DTCS_MAP[chan.rx_dcs]
|
989
|
|
990
|
def encode_sql(self, mem, chan):
|
991
|
"""
|
992
|
examine CHIRP's mem.tmode and mem.cross_mode and set the values
|
993
|
for the radio sql_type, dcs codes, and ctcss codes. We set all four
|
994
|
codes, and then zero out a code if needed when Tone or DCS is one-way
|
995
|
"""
|
996
|
chan.tx_ctcss = TONE_MAP.index(mem.rtone)
|
997
|
chan.tx_dcs = DTCS_MAP.index(mem.dtcs)
|
998
|
chan.rx_ctcss = TONE_MAP.index(mem.ctone)
|
999
|
chan.rx_dcs = DTCS_MAP.index(mem.rx_dtcs)
|
1000
|
if mem.tmode == "TSQL":
|
1001
|
chan.tx_ctcss = chan.rx_ctcss # CHIRP uses ctone for TSQL
|
1002
|
if mem.tmode == "DTCS":
|
1003
|
chan.tx_dcs = chan.rx_dcs # CHIRP uses rx_dtcs for DTCS
|
1004
|
# select the correct internal dictionary and key
|
1005
|
mode_dict, key = [
|
1006
|
(TONE_DICT, mem.tmode),
|
1007
|
(CROSS_DICT, mem.cross_mode)
|
1008
|
][mem.tmode == "Cross"]
|
1009
|
# now look up that key in that dictionary.
|
1010
|
chan.sql_type, suppress = mode_dict[key]
|
1011
|
if suppress:
|
1012
|
setattr(chan, suppress, 0)
|
1013
|
for setting in mem.extra:
|
1014
|
if (setting.get_name() == 'sql_override'):
|
1015
|
value = str(setting.value)
|
1016
|
if value == "PAGER":
|
1017
|
chan.sql_type = 6
|
1018
|
return
|
1019
|
|
1020
|
# given a CHIRP memory ref, get the radio memobj for it.
|
1021
|
# A memref is either a number or the name of a special
|
1022
|
# CHIRP will sometimes use numbers (>MAX_SLOTS) for specials
|
1023
|
# returns the obj and several attributes
|
1024
|
def slotloc(self, memref):
|
1025
|
array = None
|
1026
|
num = memref
|
1027
|
sname = memref
|
1028
|
if isinstance(memref, str): # named special?
|
1029
|
num = self.MAX_MEM_SLOT + 1
|
1030
|
for x in self.class_specials:
|
1031
|
try:
|
1032
|
ndx = x[1].index(memref)
|
1033
|
array = x[0]
|
1034
|
break
|
1035
|
except:
|
1036
|
num += len(x[1])
|
1037
|
if array is None:
|
1038
|
LOG.debug("unknown Special %s", memref)
|
1039
|
raise
|
1040
|
num += ndx
|
1041
|
elif memref > self.MAX_MEM_SLOT: # numbered special?
|
1042
|
ndx = memref - (self.MAX_MEM_SLOT + 1)
|
1043
|
for x in self.class_specials:
|
1044
|
if ndx < len(x[1]):
|
1045
|
array = x[0]
|
1046
|
sname = x[1][ndx]
|
1047
|
break
|
1048
|
ndx -= len(x[1])
|
1049
|
if array is None:
|
1050
|
LOG.debug("memref number %d out of range", memref)
|
1051
|
raise
|
1052
|
else: # regular memory slot
|
1053
|
array = "memory"
|
1054
|
ndx = memref - 1
|
1055
|
memloc = getattr(self._memobj, array)[ndx]
|
1056
|
return (memloc, ndx, num, array, sname)
|
1057
|
# end of slotloc
|
1058
|
|
1059
|
# return the raw info for a memory channel
|
1060
|
def get_raw_memory(self, memref):
|
1061
|
memloc, ndx, num, regtype, sname = self.slotloc(memref)
|
1062
|
if regtype == "memory":
|
1063
|
return repr(memloc)
|
1064
|
else:
|
1065
|
return repr(memloc) + repr(self._memobj.names[ndx])
|
1066
|
|
1067
|
# return the info for a memory channel In CHIRP canonical form
|
1068
|
def get_memory(self, memref):
|
1069
|
|
1070
|
def clean_name(obj): # helper func to tidy up the name
|
1071
|
name = ''
|
1072
|
for x in range(0, self.namelen):
|
1073
|
y = obj[x]
|
1074
|
if y == 0:
|
1075
|
break
|
1076
|
if y == 0x7F: # when programmed from VFO
|
1077
|
y = 0x20
|
1078
|
name += chr(y)
|
1079
|
return name.rstrip()
|
1080
|
|
1081
|
def get_duplex(freq): # auto duplex to real duplex
|
1082
|
"""
|
1083
|
Select the duplex direction if duplex == 'auto'.
|
1084
|
0 is +, 2 is -, and 4 is none.
|
1085
|
"""
|
1086
|
return_value = 4 # off, if not in auto range
|
1087
|
for x in self.DUPLEX_AUTO:
|
1088
|
if freq in range(x[0], x[1]):
|
1089
|
return_value = x[2]
|
1090
|
return return_value
|
1091
|
|
1092
|
mem = chirp_common.Memory()
|
1093
|
_mem, ndx, num, regtype, sname = self.slotloc(memref)
|
1094
|
mem.number = num
|
1095
|
freq_offset_factor = self.freq_offset_factor
|
1096
|
# FT-25R/65R Asia version (US is 0)?
|
1097
|
if self.subtype == 3 and freq_offset_factor == 2:
|
1098
|
freq_offset_factor = 1 # 25000 scaler
|
1099
|
|
1100
|
# First, we need to know whether a channel is enabled,
|
1101
|
# then we can process any channel parameters.
|
1102
|
# It was found (at least on an FT-25) that channels might be
|
1103
|
# uninitialized and memory is just completely filled with 0xFF.
|
1104
|
|
1105
|
if regtype == "pms":
|
1106
|
mem.extd_number = sname
|
1107
|
if regtype in ["memory", "pms"]:
|
1108
|
ndx = num - 1
|
1109
|
mem.name = clean_name(self._memobj.names[ndx].chrs)
|
1110
|
mem.empty = not retrieve_bit(self._memobj.enable, ndx)
|
1111
|
mem.skip = SKIPS[retrieve_bit(self._memobj.scan, ndx)]
|
1112
|
else:
|
1113
|
mem.empty = False
|
1114
|
mem.extd_number = sname
|
1115
|
mem.immutable = ["number", "extd_number", "name", "skip"]
|
1116
|
|
1117
|
# So, now if channel is not empty, we can do the evaluation of
|
1118
|
# all parameters. Otherwise we set them to defaults.
|
1119
|
|
1120
|
if mem.empty:
|
1121
|
mem.freq = 0
|
1122
|
mem.offset = 0
|
1123
|
mem.duplex = "off"
|
1124
|
mem.power = POWER_LEVELS[0] # "High"
|
1125
|
mem.mode = "FM"
|
1126
|
mem.tuning_step = 0
|
1127
|
else:
|
1128
|
mem.freq = int(_mem.freq) * 10
|
1129
|
txfreq = int(self._memobj.txfreqs[ndx].freq) * 10
|
1130
|
if _mem.duplex == 5: # auto offset
|
1131
|
mem.duplex = DUPLEX[get_duplex(mem.freq)]
|
1132
|
else:
|
1133
|
mem.duplex = DUPLEX[_mem.duplex]
|
1134
|
if _mem.duplex == 6: # split: offset is tx frequency
|
1135
|
mem.offset = txfreq
|
1136
|
else:
|
1137
|
mem.offset = int(_mem.offset) * 25000 * freq_offset_factor
|
1138
|
self.decode_sql(mem, _mem)
|
1139
|
mem.power = POWER_LEVELS[2 - _mem.tx_pwr]
|
1140
|
mem.mode = ["FM", "NFM"][_mem.tx_width]
|
1141
|
mem.tuning_step = STEP_CODE[_mem.step]
|
1142
|
return mem
|
1143
|
|
1144
|
def enforce_band(self, memloc, freq, mem_num, sname):
|
1145
|
"""
|
1146
|
vfo and home channels are each restricted to a particular band.
|
1147
|
If the frequency is not in the band, use the lower bound
|
1148
|
Raise an exception to cause UI to pop up an error message
|
1149
|
"""
|
1150
|
first_vfo_num = self.MAX_MEM_SLOT + len(PMSNAMES) + 1
|
1151
|
band = self.BAND_ASSIGNMENTS[mem_num - first_vfo_num]
|
1152
|
frange = self.valid_bands[band]
|
1153
|
if freq >= frange[0] and freq <= frange[1]:
|
1154
|
memloc.freq = freq / 10
|
1155
|
return freq
|
1156
|
memloc.freq = frange[0] / 10
|
1157
|
raise Exception("freq out of range for %s" % sname)
|
1158
|
|
1159
|
# modify a radio channel in memobj based on info in CHIRP canonical form
|
1160
|
def set_memory(self, mem):
|
1161
|
_mem, ndx, num, regtype, sname = self.slotloc(mem.number)
|
1162
|
assert(_mem)
|
1163
|
if mem.empty:
|
1164
|
if regtype in ["memory", "pms"]:
|
1165
|
store_bit(self._memobj.enable, ndx, False)
|
1166
|
return
|
1167
|
|
1168
|
txfreq = mem.freq / 10 # really. RX freq is used for TX base
|
1169
|
_mem.freq = txfreq
|
1170
|
self.encode_sql(mem, _mem)
|
1171
|
if mem.power:
|
1172
|
_mem.tx_pwr = 2 - POWER_LEVELS.index(mem.power)
|
1173
|
else:
|
1174
|
_mem.tx_pwr = 0 # set to "High" if CHIRP canonical value is None
|
1175
|
_mem.tx_width = mem.mode == "NFM"
|
1176
|
_mem.step = STEP_CODE.index(mem.tuning_step)
|
1177
|
|
1178
|
freq_offset_factor = self.freq_offset_factor
|
1179
|
# FT-25R/65R Asia version (US version is 0)?
|
1180
|
if self.subtype == 3 and freq_offset_factor == 2:
|
1181
|
freq_offset_factor = 1 # 25000 scaler
|
1182
|
_mem.offset = mem.offset / (25000 * freq_offset_factor)
|
1183
|
duplex = mem.duplex
|
1184
|
if regtype in ["memory", "pms"]:
|
1185
|
ndx = num - 1
|
1186
|
store_bit(self._memobj.enable, ndx, True)
|
1187
|
store_bit(self._memobj.scan, ndx, SKIPS.index(mem.skip))
|
1188
|
nametrim = (mem.name + " ")[:8]
|
1189
|
self._memobj.names[ndx].chrs = bytearray(nametrim, "ascii")
|
1190
|
if mem.duplex == "split":
|
1191
|
txfreq = mem.offset / 10
|
1192
|
self._memobj.txfreqs[num-1].freq = txfreq
|
1193
|
_mem.duplex = DUPLEX.index(duplex)
|
1194
|
if regtype in ["vfo", "home"]:
|
1195
|
self.enforce_band(_mem, mem.freq, num, sname)
|
1196
|
|
1197
|
return
|
1198
|
|
1199
|
|
1200
|
class YaesuFT4GenericRadio(YaesuSC35GenericRadio):
|
1201
|
"""
|
1202
|
FT-4 sub family class. Classes for individual radios extend
|
1203
|
these classes and are found at the end of this file.
|
1204
|
"""
|
1205
|
class_specials = SPECIALS_FT4
|
1206
|
Pkeys = 2 # number of programmable keys
|
1207
|
namelen = 6 # length of the mem name display on the front-panel
|
1208
|
freq_offset_factor = 1 # 25000 * 1
|
1209
|
class_group_descs = YaesuSC35GenericRadio.group_descriptions
|
1210
|
# names for the setmode function for the programmable keys. Mode zero means
|
1211
|
# that the key is programmed for a memory not a setmode.
|
1212
|
SETMODES = [
|
1213
|
"mem", "apo", "ar bep", "ar int", "beclo", # 00-04
|
1214
|
"beep", "bell", "cw id", "cw wrt", "dc vlt", # 05-09
|
1215
|
"dcs cod", "dt dly", "dt set", "dtc spd", "edg.bep", # 10-14
|
1216
|
"lamp", "led.bsy", "led.tx", "lock", "m/t-cl", # 15-19
|
1217
|
"mem.del", "mem.tag", "pag.abk", "pag.cdr", "pag.cdt", # 20-24
|
1218
|
"pri.rvt", "pswd", "pswdwt", "rf sql", "rpt.ars", # 25-29
|
1219
|
"rpt.frq", "rpt.sft", "rxsave", "scn.lmp", "scn.rsm", # 30-34
|
1220
|
"skip", "sql.typ", "step", "tn frq", "tot", # 35-39
|
1221
|
"tx pwr", "tx save", "vfo.spl", "vox", "wfm.rcv", # 40-44
|
1222
|
"w/n.dev", "wx.alert" # 45-46
|
1223
|
]
|
1224
|
|
1225
|
|
1226
|
class YaesuFT65GenericRadio(YaesuSC35GenericRadio):
|
1227
|
"""
|
1228
|
FT-65 sub family class. Classes for individual radios extend
|
1229
|
these classes and are found at the end of this file.
|
1230
|
"""
|
1231
|
class_specials = SPECIALS_FT65
|
1232
|
Pkeys = 4 # number of programmable keys
|
1233
|
namelen = 8 # length of the mem name display on the front-panel
|
1234
|
freq_offset_factor = 2 # 25000 * 2
|
1235
|
# we need a deep copy here because we are adding deeper than the top level.
|
1236
|
class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions)
|
1237
|
add_paramdesc(
|
1238
|
class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"]))
|
1239
|
# names for the setmode function for the programmable keys. Mode zero means
|
1240
|
# that the key is programmed for a memory not a setmode.
|
1241
|
SETMODES = [
|
1242
|
"mem", "apo", "arts", "battsave", "b-ch.l/o", # 00-04
|
1243
|
"beep", "bell", "compander", "ctcss", "cw id", # 05-09
|
1244
|
"dc volt", "dcs code", "dtmf set", "dtmf wrt", "edg bep", # 10-14
|
1245
|
"key lock", "lamp", "ledbsy", "mem del", "mon/t-cl", # 15-19
|
1246
|
"name tag", "pager", "password", "pri.rvt", "repeater", # 20-24
|
1247
|
"resume", "rf.sql", "scn.lamp", "skip", "sql type", # 25-29
|
1248
|
"step", "tot", "tx pwr", "tx save", "vfo.spl", # 30-34
|
1249
|
"vox", "wfm.rcv", "wide/nar", "wx alert", "scramble" # 35-39
|
1250
|
]
|
1251
|
|
1252
|
|
1253
|
# Classes for each individual radio.
|
1254
|
|
1255
|
|
1256
|
@directory.register
|
1257
|
class YaesuFT4XRRadio(YaesuFT4GenericRadio):
|
1258
|
"""
|
1259
|
FT-4X dual band, US version
|
1260
|
"""
|
1261
|
MODEL = "FT-4XR"
|
1262
|
id_str = b'IFT-35R\x00\x00V100\x00\x00'
|
1263
|
valid_bands = VALID_BANDS_DUAL
|
1264
|
DUPLEX_AUTO = DUPLEX_AUTO_US
|
1265
|
legal_steps = US_LEGAL_STEPS
|
1266
|
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
|
1267
|
|
1268
|
|
1269
|
@directory.register
|
1270
|
class YaesuFT4XERadio(YaesuFT4GenericRadio):
|
1271
|
"""
|
1272
|
FT-4X dual band, EU version
|
1273
|
"""
|
1274
|
MODEL = "FT-4XE"
|
1275
|
id_str = b'IFT-35R\x00\x00V100\x00\x00'
|
1276
|
valid_bands = VALID_BANDS_DUAL
|
1277
|
DUPLEX_AUTO = DUPLEX_AUTO_EU
|
1278
|
legal_steps = STEP_CODE
|
1279
|
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
|
1280
|
|
1281
|
|
1282
|
@directory.register
|
1283
|
class YaesuFT4VRRadio(YaesuFT4GenericRadio):
|
1284
|
"""
|
1285
|
FT-4V VHF, US version
|
1286
|
"""
|
1287
|
MODEL = "FT-4VR"
|
1288
|
id_str = b'IFT-15R\x00\x00V100\x00\x00'
|
1289
|
valid_bands = VALID_BANDS_VHF
|
1290
|
DUPLEX_AUTO = DUPLEX_AUTO_US
|
1291
|
legal_steps = US_LEGAL_STEPS
|
1292
|
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
|
1293
|
|
1294
|
|
1295
|
# No image available yet
|
1296
|
# @directory.register
|
1297
|
# class YaesuFT4VERadio(YaesuFT4GenericRadio):
|
1298
|
# """
|
1299
|
# FT-4V VHF, EU version
|
1300
|
# """
|
1301
|
# MODEL = "FT-4VE"
|
1302
|
# id_str = b'IFT-15R\x00\x00V100\x00\x00'
|
1303
|
# valid_bands = VALID_BANDS_VHF
|
1304
|
# DUPLEX_AUTO = DUPLEX_AUTO_EU
|
1305
|
# legal_steps = STEP_CODE
|
1306
|
# BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
|
1307
|
|
1308
|
|
1309
|
@directory.register
|
1310
|
class YaesuFT65RRadio(YaesuFT65GenericRadio):
|
1311
|
"""
|
1312
|
FT-65 dual band, US version
|
1313
|
"""
|
1314
|
MODEL = "FT-65R"
|
1315
|
id_str = b'IH-420\x00\x00\x00V100\x00\x00'
|
1316
|
valid_bands = VALID_BANDS_DUAL
|
1317
|
DUPLEX_AUTO = DUPLEX_AUTO_US
|
1318
|
legal_steps = STEP_CODE
|
1319
|
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
|
1320
|
|
1321
|
|
1322
|
@directory.register
|
1323
|
class YaesuFT65ERadio(YaesuFT65GenericRadio):
|
1324
|
"""
|
1325
|
FT-65 dual band, EU version
|
1326
|
"""
|
1327
|
MODEL = "FT-65E"
|
1328
|
id_str = b'IH-420\x00\x00\x00V100\x00\x00'
|
1329
|
valid_bands = VALID_BANDS_DUAL
|
1330
|
DUPLEX_AUTO = DUPLEX_AUTO_EU
|
1331
|
legal_steps = STEP_CODE
|
1332
|
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
|
1333
|
|
1334
|
|
1335
|
@directory.register
|
1336
|
class YaesuFT25RRadio(YaesuFT65GenericRadio):
|
1337
|
"""
|
1338
|
FT-25 VHF, US or Asia version
|
1339
|
"""
|
1340
|
MODEL = "FT-25R"
|
1341
|
id_str = b'IFT-25R\x00\x00V100\x00\x00'
|
1342
|
valid_bands = VALID_BANDS_VHF
|
1343
|
DUPLEX_AUTO = DUPLEX_AUTO_US
|
1344
|
legal_steps = US_LEGAL_STEPS
|
1345
|
BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
|
1346
|
|
1347
|
|
1348
|
# No image available yet
|
1349
|
# @directory.register
|
1350
|
# class YaesuFT25ERadio(YaesuFT65GenericRadio):
|
1351
|
# """
|
1352
|
# FT-25 VHF, EU version
|
1353
|
# """
|
1354
|
# MODEL = "FT-25E"
|
1355
|
# id_str = b'IFT-25R\x00\x00V100\x00\x00'
|
1356
|
# valid_bands = VALID_BANDS_VHF
|
1357
|
# DUPLEX_AUTO = DUPLEX_AUTO_EU
|
1358
|
# legal_steps = STEP_CODE
|
1359
|
# BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
|