Project

General

Profile

Bug #10286 » ft4.py

Bernhard Hailer, 02/15/2023 01:23 AM

 
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
(14-14/15)