Project

General

Profile

Bug #10744 » ft4.py

17d5790b - Dan Smith, 07/30/2023 08: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, 2023 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 unknown1:5,
58
    duplex:3;   //(auto,offset). (0,2,4,5)= (+,-,0, auto)
59
 ul16 offset;   //little-endian binary * scaler, +- per duplex
60
                   //scaler is 25 kHz for FT-4, 50 kHz for FT-65.
61
 u8 unknown2:7,
62
    tx_width:1;  //0=wide, 1=narrow
63
 u8 step;       //STEPS (0-9)=(auto,5,6.25,10,12.5,15,20,25,50,100) kHz
64
 u8 sql_type;   //(0-6)==(off,r-tone,t-tone,tsql,rev tn,dcs,pager)
65
 u8 unused;
66
};
67
// one bit per channel. 220 bits (200 mem+ 2*10 PMS) padded to fill
68
//exactly 2 blocks
69
struct bitmap {
70
u8 b8[28];
71
u8 unused[4];
72
};
73
//name struct occupies half a block (8 bytes)
74
//the code restricts the actual len to 6 for an FT-4
75
struct name {
76
  u8 chrs[8];    //[0-9,A-z,a-z, -] padded with spaces
77
};
78
//txfreq struct occupies 4 bytes (1/4 slot)
79
struct txfreq {
80
 bbcd freq[4];
81
};
82

    
83
//miscellaneous params. One 4-block group.
84
//"SMI": "Set Mode Index" of the FT-4 radio keypad function to set parameter.
85
//"SMI numbers on the FT-65 are different but the names in mem are the same.
86
struct misc {
87
  u8  apo;        //SMI 01. 0==off, (1-24) is the number of half-hours.
88
  u8  arts_beep;  //SMI 02. 0==off, 1==inrange, 2==always
89
  u8  arts_intv;  //SMI 03. 0==25 seconds, 1==15 seconds
90
  u8  battsave;   //SMI 32. 0==off, (1-5)==(200,300,500,1000,2000) ms
91
  u8  bclo;       //SMI 04. 0==off, 1==on
92
  u8  beep;       //SMI 05. (0-2)==(key+scan,key, off)
93
  u8  bell;       //SMI 06. (0-5)==(0,1,3,5,8,continuous) bells
94
  u8  cw_id[6];   //SMI 08. callsign (A_Z,0-9) (pad with space if <6)
95
  u8  unknown1[3];
96
  // addr= 2010
97
  u8  dtmf_mode;  //SMI 12. 0==manual, 1==auto
98
  u8  dtmf_delay; //SMI 11. (0-4)==(50,250,450,750,1000) ms
99
  u8  dtmf_speed; //SMI 13. (0,1)=(50,100) ms
100
  u8  edg_beep;   //SMI 14. 0==off, 1==on
101
  u8  key_lock;   //SMI 18. (0-2)==(key,ptt,key+ptt)
102
  u8  lamp;       //SMI 15. (0-4)==(5,10,30,continuous,off) secKEY
103
  u8  tx_led;     //SMI 17. 0==off, 1==on
104
  u8  bsy_led;    //SMI 16. 0==off, 1==on
105
  u8  moni_tcall; //SMI 19. (0-4)==(mon,1750,2100,1000,1450) tcall Hz.
106
  u8  pri_rvt;    //SMI 23. 0==off, 1==on
107
  u8  scan_resume; //SMI 34. (0-2)==(busy,hold,time)
108
  u8  rf_squelch;  //SMI 28. 0==off, 8==full, (1-7)==(S1-S7)
109
  u8  scan_lamp;  //SMI 33  0==off,1==on
110
  u8  unknown2;
111
  u8  use_cwid;   //SMI 7. 0==no, 1==yes
112
  u8  compander;  // compander on FT_65
113
  // addr 2020
114
  u8  unknown3;
115
  u8  tx_save;    //SMI 41. 0==off, 1==on (addr==2021)
116
  u8  vfo_spl;    //SMI 42. 0==off, 1==on
117
  u8  vox;        //SMI 43. 0==off, 1==on
118
  u8  wfm_rcv;    //SMI 44. 0==off, 1==on
119
  u8  unknown4;
120
  u8  wx_alert;   //SMI 46. 0==off, 1==0n
121
  u8  tot;        //SMI 39. 0-off, (1-30)== (1-30) minutes
122
  u8  pager_tx1;  //SMI 21. (0-49)=(1-50) epcs code (i.e., value is code-1)
123
  u8  pager_tx2;  //SMI 21   same
124
  u8  pager_rx1;  //SMI 23   same
125
  u8  pager_rx2;  //SMI 23   same
126
  u8  pager_ack;  //SMI 22   same
127
  u8  unknown5[3];  //possibly sql_setting and pgm_vfo_scan on FT-65?
128
  // addr 2030
129
  u8  use_passwd; //SMI 26 0==no, 1==yes
130
  u8  passwd[4];  //SMI 27 ASCII (0-9)
131
  u8  unused2[11]; //  pad out to a block boundary
132
};
133

    
134
struct dtmfset {
135
 u8 digit[16];    //ASCII (*,#,-,0-9,A-D). (dash-filled)
136
};
137

    
138
// area to be filled with 0xff, or just ignored
139
struct notused {
140
  u8 unused[16];
141
};
142
// areas we are still analyzing
143
struct unknown {
144
  u8 notknown[16];
145
};
146

    
147
struct progc {
148
  u8 usage;          //P key is (0-2) == unused, use P channel, use parm
149
  u8 submode:2,      //if parm!=0 submode 0-3 of mode
150
     parm:6;         //if usage == 2: if 0, use m-channel, else  mode
151
};
152
"""
153
# Actual memory layout. 0x215 blocks, in 20 groups.
154
MEM_FORMAT += """
155
#seekto 0x0000;
156
struct unknown radiotype;     //0000 probably a radio type ID but not sure
157
struct slot    memory[200];   //0010 channel memory array
158
struct slot    pms[20];       //0c90 10 PMS (L,U) slot pairs
159
struct slot    vfo[5];        //0dd0 VFO (A UHF, A VHF, B FM, B  UHF, B VHF)
160
struct slot    home[3];       //0e20 Home (FM, VHF, UHF)
161
struct bitmap  enable;        //0e50
162
struct bitmap  scan;          //0e70
163
struct notused notused0;      //0e90
164
struct bitmap  bankmask[10];  //0ea0
165
struct notused notused1[2];   //0fe0
166
struct name  names[220];      //1000 220 names in 110 blocks
167
struct notused notused2[2];   //16e0
168
struct txfreq  txfreqs[220];  //1700 220 freqs in 55 blocks
169
struct notused notused3[89];  //1a20
170
struct misc  settings;        //2000  4-block collection of misc params
171
struct notused notused4[2];   //2040
172
struct dtmfset dtmf[9];       //2060  sets 1-9
173
struct notused notused5;      //20f0
174
//struct progs progkeys;      //2100  not a struct. bitwise.py refuses
175
struct progc  progctl[4];        //2100 8 bytes. 1 2-byte struct per P key
176
u8 pmemndx[4];                   //2108 4 bytes, 1 per P key
177
u8 notused6[4];                  //210c fill out the block
178
struct slot  prog[4];         //2110  P key "channel" array
179
//---------------- end of FT-4 mem?
180
"""
181
# The remaining mem is (apparently) not available on the FT4 but is
182
# reported to be available on the FT-65. Not implemented here yet.
183
# Possibly, memory-mapped control registers that  allow for "live-mode"
184
# operation instead of "clone-mode" operation.
185
# 2150 27ff                   (unused?)
186
# 2800 285f       6           MRU operation?
187
# 2860 2fff                   (unused?)
188
# 3000 310f       17          (version info, etc?)
189
# ----------END of memory map
190

    
191

    
192
# Begin Serial transfer utilities for the SCU-35 cable.
193

    
194
# The serial transfer protocol was implemented here after snooping the wire.
195
# After it was implemented, we noticed that it is identical to the protocol
196
# implemented in th9000.py. A non-echo version is implemented in anytone_ht.py.
197
#
198
# The pipe.read and pipe.write functions use bytes, not strings. The serial
199
# transfer utilities operate only to move data between the memory object and
200
# the serial port. The code runs on either Python 2 or Python3, so some
201
# constructs could be better optimized for one or the other, but not both.
202

    
203

    
204
def get_mmap_data(radio):
205
    """
206
    horrible kludge needed until we convert entirely to Python 3 OR we add a
207
    slightly less horrible kludge to the Py2 or Py3 versions of memmap.py.
208
    The minimal change have Py3 code return a bytestring instead of a string.
209
    This is the only function in this module that must explicitly test for the
210
    data string type. It is used only in the do_upload function.
211
    returns the memobj data as a byte-like object.
212
    """
213
    data = radio.get_mmap().get_packed()
214
    if isinstance(data, bytes):
215
        return data
216
    return bytearray(radio.get_mmap()._data)
217

    
218

    
219
def checkSum8(data):
220
    """
221
    Calculate the 8 bit checksum of buffer
222
    Input: buffer   - bytes
223
    returns: integer
224
    """
225
    return sum(x for x in bytearray(data)) & 0xFF
226

    
227

    
228
def variable_len_resp(pipe):
229
    """
230
    when length of expected reply is not known, read byte at a time
231
    until the ack character is found.
232
    """
233
    response = b""
234
    i = 0
235
    toolong = 256        # arbitrary
236
    while True:
237
        b = pipe.read(1)
238
        if b == b'\x06':
239
            break
240
        else:
241
            response += b
242
            i += 1
243
            if i > toolong:
244
                LOG.debug("Response too long. got" + util.hexprint(response))
245
                raise errors.RadioError("Response from radio too long.")
246
    return response
247

    
248

    
249
def sendcmd(pipe, cmd, response_len):
250
    """
251
    send a command bytelist to radio,receive and return the resulting bytelist.
252
    Input: pipe         - serial port object to use
253
           cmd          - bytes to send
254
           response_len - number of bytes of expected response,
255
                          not including the ACK. (if None, read until ack)
256
    This cable is "two-wire": The TxD and RxD are "or'ed" so we receive
257
    whatever we send and then whatever response the radio sends. We check the
258
    echo and strip it, returning only the radio's response.
259
    We also check and strip the ACK character at the end of the response.
260
    """
261
    pipe.write(cmd)
262
    echo = pipe.read(len(cmd))
263
    if echo != cmd:
264
        msg = "Bad echo. Sent:" + util.hexprint(cmd) + ", "
265
        msg += "Received:" + util.hexprint(echo)
266
        LOG.debug(msg)
267
        raise errors.RadioError(
268
            "Incorrect echo on serial port. Radio off? Bad cable?")
269
    if response_len is None:
270
        return variable_len_resp(pipe)
271
    if response_len > 0:
272
        response = pipe.read(response_len)
273
    else:
274
        response = b""
275
    ack = pipe.read(1)
276
    if ack != b'\x06':
277
        LOG.debug("missing ack: expected 0x06, got" + util.hexprint(ack))
278
        raise errors.RadioError("Incorrect ACK on serial port.")
279
    return response
280

    
281

    
282
def enter_clonemode(radio):
283
    """
284
    Send the PROGRAM command and check the response. Retry if
285
    needed. After 3 tries, send an "END" and try some more if
286
    it is acknowledged.
287
    """
288
    for use_end in range(0, 3):
289
        for i in range(0, 3):
290
            try:
291
                if b"QX" == sendcmd(radio.pipe, b"PROGRAM", 2):
292
                    return
293
            except Exception:
294
                continue
295
        sendcmd(radio.pipe, b"END", 0)
296
    raise errors.RadioError("expected QX from radio.")
297

    
298

    
299
def startcomms(radio, way):
300
    """
301
    For either upload or download, put the radio into PROGRAM mode
302
    and check the radio's ID. In this preliminary version of the driver,
303
    the exact nature of the ID has been inferred from a single test case.
304
        set up the progress bar
305
        send "PROGRAM" to command the radio into clone mode
306
        read the initial string (version?)
307
    """
308
    progressbar = chirp_common.Status()
309
    progressbar.msg = "Cloning " + way + " radio"
310
    progressbar.max = radio.numblocks
311
    enter_clonemode(radio)
312
    id_response = sendcmd(radio.pipe, b'\x02', None)
313

    
314
    # The last byte of the ID contains info about regional differences
315
    radio.subtype = id_response[-1]
316
    # Set last byte of the ID zero so that no ID warning is thrown
317
    # Unfortunately, the returned object is an immutable bytes object,
318
    # so we need to convert into a bytearray, modify, and convert back.
319
    ba_id_response = bytearray(id_response)
320
    ba_id_response[len(ba_id_response) - 1] = 0
321
    id_response_mod = bytes(ba_id_response)
322

    
323
    if id_response != radio.id_str:
324
        substr0 = radio.id_str[:radio.id_str.find(b'\x00')]
325
        if id_response[:id_response.find(b'\x00')] != substr0:
326
            msg = "ID mismatch. Expected" + util.hexprint(radio.id_str)
327
            msg += ", Received:" + util.hexprint(id_response_mod)
328
            LOG.warning(msg)
329
            raise errors.RadioError("Incorrect ID read from radio.")
330
        else:
331
            msg = "ID suspect. Expected" + util.hexprint(radio.id_str)
332
            msg += ", Received:" + util.hexprint(id_response_mod)
333
            LOG.warning(msg)
334
    return progressbar
335

    
336

    
337
def getblock(pipe, addr, image):
338
    """
339
    read a single 16-byte block from the radio.
340
    send the command and check the response
341
    places the response into the correct offset in the supplied bytearray
342
    returns True if successful, False if error.
343
    """
344
    cmd = struct.pack(">cHb", b"R", addr, 16)
345
    response = sendcmd(pipe, cmd, 21)
346
    if (response[0] != b"W"[0]) or (response[1:4] != cmd[1:4]):
347
        msg = "Bad response. Sent:\n%s" % util.hexprint(cmd)
348
        msg += "\nReceived:\n%s" % util.hexprint(response)
349
        LOG.debug(msg)
350
        return False
351
    if checkSum8(response[1:20]) != bytearray(response)[20]:
352
        LOG.debug("Bad checksum:\n%s" % util.hexprint(response))
353
        LOG.debug('%r != %r' % (checkSum8(response[1:20]),
354
                                bytearray(response)[20]))
355
        return False
356
    image[addr:addr+16] = response[4:20]
357
    return True
358

    
359

    
360
def do_download(radio):
361
    """
362
    Read memory from the radio.
363
      call startcomms to go into program mode and check version
364
      create an mmap
365
      read the memory blocks and place the data into the mmap
366
      send "END"
367
    """
368
    image = bytearray(radio.get_memsize())
369
    pipe = radio.pipe  # Get the serial port connection
370
    progressbar = startcomms(radio, "from")
371
    for blocknum in range(radio.numblocks):
372
        for i in range(0, 3):
373
            if getblock(pipe, 16 * blocknum, image):
374
                break
375
            if i == 2:
376
                raise errors.RadioError(
377
                   "read block from radio failed 3 times")
378
        progressbar.cur = blocknum
379
        radio.status_fn(progressbar)
380
    sendcmd(pipe, b"END", 0)
381
    return memmap.MemoryMapBytes(bytes(image))
382

    
383

    
384
def putblock(pipe, addr, data):
385
    """
386
    write a single 16-byte block to the radio
387
    send the command and check the response
388
    """
389
    chkstr = struct.pack(">Hb",  addr, 16) + data
390
    msg = b'W' + chkstr + struct.pack('B', checkSum8(chkstr)) + b'\x06'
391
    sendcmd(pipe, msg, 0)
392

    
393

    
394
def do_upload(radio):
395
    """
396
    Write memory image to radio
397
      call startcomms to go into program mode and check version
398
      write the memory blocks. Skip the first block
399
      send "END"
400
    """
401
    pipe = radio.pipe  # Get the serial port connection
402
    progressbar = startcomms(radio, "to")
403
    data = get_mmap_data(radio)
404
    for _i in range(1, radio.numblocks):
405
        putblock(pipe, 16*_i, data[16*_i:16*(_i+1)])
406
        progressbar.cur = _i
407
        radio.status_fn(progressbar)
408
    sendcmd(pipe, b"END", 0)
409
    return
410
# End serial transfer utilities
411

    
412

    
413
def bit_loc(bitnum):
414
    """
415
    return the ndx and mask for a bit location
416
    """
417
    return (bitnum // 8, 1 << (bitnum & 7))
418

    
419

    
420
def store_bit(bankmem, bitnum, val):
421
    """
422
    store a bit in a bankmem. Store 0 or 1 for False or True
423
    """
424
    ndx, mask = bit_loc(bitnum)
425
    if val:
426
        bankmem.b8[ndx] |= mask
427
    else:
428
        bankmem.b8[ndx] &= ~mask
429
    return
430

    
431

    
432
def retrieve_bit(bankmem, bitnum):
433
    """
434
    return True or False for a bit in a bankmem
435
    """
436
    ndx, mask = bit_loc(bitnum)
437
    return (bankmem.b8[ndx] & mask) != 0
438

    
439

    
440
# A bank is a bitmap of 220 bits. 200 mem slots and 2*10 PMS slots.
441
# There are 10 banks.
442
class YaesuSC35GenericBankModel(chirp_common.BankModel):
443

    
444
    def get_num_mappings(self):
445
        return 10
446

    
447
    def get_mappings(self):
448
        banks = []
449
        for i in range(1, 1 + self.get_num_mappings()):
450
            bank = chirp_common.Bank(self, "%i" % i, "Bank %i" % i)
451
            bank.index = i - 1
452
            banks.append(bank)
453
        return banks
454

    
455
    def add_memory_to_mapping(self, memory, bank):
456
        bankmem = self._radio._memobj.bankmask[bank.index]
457
        store_bit(bankmem, memory.number-1, True)
458

    
459
    def remove_memory_from_mapping(self, memory, bank):
460
        bankmem = self._radio._memobj.bankmask[bank.index]
461
        if not retrieve_bit(bankmem, memory.number-1):
462
            raise Exception("Memory %i is not in bank %s." %
463
                            (memory.number, bank))
464
        store_bit(bankmem, memory.number-1, False)
465

    
466
    # return a list of slots in a bank
467
    def get_mapping_memories(self, bank):
468
        memories = []
469
        for i in range(*self._radio.get_features().memory_bounds):
470
            if retrieve_bit(self._radio._memobj.bankmask[bank.index], i - 1):
471
                memories.append(self._radio.get_memory(i))
472
        return memories
473

    
474
    # return a list of banks a slot is a member of
475
    def get_memory_mappings(self, memory):
476
        memndx = memory.number - 1
477
        banks = []
478
        for bank in self.get_mappings():
479
            if retrieve_bit(self._radio._memobj.bankmask[bank.index], memndx):
480
                banks.append(bank)
481
        return banks
482

    
483

    
484
# the values in these lists must also be in the canonical UI list
485
# we can re-arrange the order, and we don't need to have all
486
# the values, but we cannot add our own values here.
487

    
488
DUPLEX = ["+", "", "-", "", "off", "", "split"]  # (0,2,4,5) = (+, -, 0, auto)
489
# The radio implements duplex "auto" as 5. We map to "". It is a convenience
490
# function in the radio that affects the offset, which sets the duplex value
491
# according to the frequency in use. Yaesu doesn't entirely adhere to the band
492
# plans; but worse, they save the value 'auto' instead of + or -. Why Yaesu
493
# is doing such a thing is beyond me. [AE6YN]
494
DUPLEX_AUTO_US = [
495
    [145100000, 145495000, 2],
496
    [146000000, 146395000, 0],
497
    [146600000, 146995000, 2],
498
    [147000000, 147395000, 0],
499
    [147600000, 147995000, 2]]
500
# (There are no automatic duplex values in IARU-2 70CM.)
501
DUPLEX_AUTO_EU = [
502
    [145600000, 145800000, 2],
503
    [438200000, 439425000, 2]]
504

    
505
SKIPS = ["S", ""]
506

    
507
POWER_LEVELS = [
508
    chirp_common.PowerLevel("High", watts=5.0),  # high must be first (0)
509
    chirp_common.PowerLevel("Mid", watts=2.5),
510
    chirp_common.PowerLevel("Low", watts=0.5)]
511

    
512
# these steps encode to 0-9 on all radios, but encoding #2 is disallowed
513
# on the US versions (FT-4XR)
514
STEP_CODE = [0, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
515
US_LEGAL_STEPS = list(STEP_CODE)  # copy to pass to UI on US radios
516
US_LEGAL_STEPS.remove(6.25)       # euro radios just use STEP_CODE
517

    
518
# Map the radio image sql_type (0-6) to the CHIRP mem values.
519
# Yaesu "TSQL" and "DCS" each map to different CHIRP values depending on the
520
# radio values of the tx and rx tone codes. The table is a list of rows, one
521
# per Yaesu sql_type (0-5). The code does not use this table when the sql_type
522
# is 6 (PAGER). Each row is a tuple. Its first member is a list of
523
# [tmode,cross] or [tmode, cross, suppress]. "Suppress" is used only when
524
# encoding UI-->radio. When decoding radio-->UI, two of the sql_types each
525
# result in 5 possible UI decodings depending on the tx and rx codes, and the
526
# list in each of these rows has five members. These two row tuples each have
527
# two additional members to specify which of the radio fields to examine.
528
# The map from CHIRP UI to radio image types is also built from this table.
529
RADIO_TMODES = [
530
        ([["", None], ], ),            # sql_type= 0. off
531
        ([["Cross", "->Tone"], ], ),   # sql_type= 1. R-TONE
532
        ([["Tone", None], ], ),        # sql_type= 2. T-TONE
533
        ([                             # sql_type= 3. TSQL:
534
          ["", None],                       # tx==0, rx==0 : invalid
535
          ["TSQL", None],                   # tx==0
536
          ["Tone", None],                   # rx==0
537
          ["Cross", "Tone->Tone"],          # tx!=rx
538
          ["TSQL", None]                    # tx==rx
539
         ], "tx_ctcss", "rx_ctcss"),     # tx and rx fields to check
540
        ([["TSQL-R", None], ], ),      # sql_type= 4. REV TN
541
        ([                             # sql_type= 5.DCS:
542
          ["", None],                       # tx==0, rx==0 : invalid
543
          ["Cross", "->DTCS", "tx_dcs"],    # tx==0. suppress tx
544
          ["Cross", "DTCS->", "rx_dcs"],    # rx==0. suppress rx
545
          ["Cross", "DTCS->DTCS"],          # tx!=rx
546
          ["DTCS", None]                    # tx==rx
547
         ], "tx_dcs", "rx_dcs"),         # tx and rx fields to check
548
        #                              # sql_type= 6. PAGER is a CHIRP "extra"
549
        ]
550

    
551
# Find all legal values for the tmode and cross fields for the UI.
552
# We build two dictionaries to do the lookups when encoding.
553
# The reversed range is a kludge: by happenstance, earlier duplicates
554
# in the above table are the preferred mapping, they override the
555
# later ones when we process the table backwards.
556
TONE_DICT = {}          # encode sql_type.
557
CROSS_DICT = {}         # encode sql_type.
558

    
559
for sql_type in reversed(range(0, len(RADIO_TMODES))):
560
    sql_type_row = RADIO_TMODES[sql_type]
561
    for decode_row in sql_type_row[0]:
562
        suppress = None
563
        if len(decode_row) == 3:
564
            suppress = decode_row[2]
565
        TONE_DICT[decode_row[0]] = (sql_type, suppress)
566
        if decode_row[1]:
567
            CROSS_DICT[decode_row[1]] = (sql_type, suppress)
568

    
569
# The keys are added to the "VALID" lists using code that puts those lists
570
# in the same order as the UI's default order instead of the random dict
571
# order or the arbitrary build order.
572
VALID_TONE_MODES = []   # list for UI.
573
VALID_CROSS_MODES = []  # list for UI.
574
for name in chirp_common.TONE_MODES:
575
    if name in TONE_DICT:
576
        VALID_TONE_MODES += [name]
577
for name in chirp_common.CROSS_MODES:
578
    if name in CROSS_DICT:
579
        VALID_CROSS_MODES += [name]
580

    
581

    
582
DTMF_CHARS = "0123456789ABCD*#- "
583
CW_ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ "
584
PASSWD_CHARS = "0123456789"
585
CHARSET = CW_ID_CHARS + "abcdefghijklmnopqrstuvwxyz*+-/@"
586
PMSNAMES = ["%s%02d" % (c, i) for i in range(1, 11) for c in ('L', 'U')]
587

    
588
# Four separate arrays of special channel mems.
589
# Each special has unique constraints: band, name yes/no, and pms L/U
590
# The FT-65 class replaces the "prog" entry in this list.
591
# The name field must be the name of a slot array in MEM_FORMAT
592
SPECIALS_FT4 = [
593
    ("pms", PMSNAMES),
594
    ("vfo", ["VFO A UHF", "VFO A VHF", "VFO B FM", "VFO B VHF", "VFO B UHF"]),
595
    ("home", ["HOME FM", "HOME VHF", "HOME UHF"]),
596
    ("prog", ["P1", "P2"])
597
    ]
598
SPECIALS_FT65 = SPECIALS_FT4
599
FT65_PROGS = ("prog", ["P1", "P2", "P3", "P4"])
600
SPECIALS_FT65[-1] = FT65_PROGS    # replace the last entry (P key names)
601

    
602

    
603
VALID_BANDS_DUAL = [
604
    (65000000, 108000000),     # broadcast FM, receive only
605
    (136000000, 174000000),    # VHF
606
    (400000000, 480000000)     # UHF
607
    ]
608

    
609
VALID_BANDS_VHF = [
610
    (65000000, 108000000),     # broadcast FM, receive only
611
    (136000000, 174000000),    # VHF
612
    ]
613

    
614
# bands for the five VFO and three home channel memories
615
BAND_ASSIGNMENTS_DUALBAND = [2, 1, 0, 1, 2, 0, 1, 2]  # all locations used
616
BAND_ASSIGNMENTS_MONO_VHF = [1, 1, 0, 1, 1, 0, 1, 1]  # UHF locations unused
617

    
618

    
619
# None, and 50 Tones. Use this explicit array because the
620
# one in chirp_common could change and no longer describe our radio
621
TONE_MAP = [
622
    None, 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5,
623
    85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5,
624
    107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
625
    131.8, 136.5, 141.3, 146.2, 151.4, 156.7,
626
    159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
627
    177.3, 179.9, 183.5, 186.2, 189.9, 192.8,
628
    196.6, 199.5, 203.5, 206.5, 210.7, 218.1,
629
    225.7, 229.1, 233.6, 241.8, 250.3, 254.1
630
    ]
631

    
632
# None, and 104 DTCS Codes. Use this explicit array because the
633
# one in chirp_common could change and no longer describe our radio
634
DTCS_MAP = [
635
    None, 23,  25,  26,  31,  32,  36,  43,  47,  51,  53,  54,
636
    65,  71,  72,  73,  74,  114, 115, 116, 122, 125, 131,
637
    132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174,
638
    205, 212, 223, 225, 226, 243, 244, 245, 246, 251, 252,
639
    255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325,
640
    331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412,
641
    413, 423, 431, 432, 445, 446, 452, 454, 455, 462, 464,
642
    465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606,
643
    612, 624, 627, 631, 632, 654, 662, 664, 703, 712, 723,
644
    731, 732, 734, 743, 754
645
    ]
646

    
647
# The legal PAGER codes are the same as the CTCSS codes, but we
648
# pass them to the UI as a list of strings
649
EPCS_CODES = [format(flt) for flt in [0] + TONE_MAP[1:]]
650

    
651

    
652
# allow a child class to add a param to its class
653
# description list. used when a specific radio class has
654
# a param that is not in the generic list.
655
def add_paramdesc(desc_list, group, param):
656
    for description in desc_list:
657
        groupname, title, parms = description
658
        if group == groupname:
659
            description[2].append(param)
660
            return
661

    
662

    
663
class YaesuSC35GenericRadio(chirp_common.CloneModeRadio,
664
                            chirp_common.ExperimentalRadio):
665
    """
666
    Base class for all Yaesu radios using the SCU-35 programming cable
667
    and its protocol. Classes for sub families extend this class and
668
    are found towards the end of this file.
669
    """
670
    VENDOR = "Yaesu"
671
    MODEL = "SCU-35Generic"  # No radio directly uses the base class
672
    BAUD_RATE = 9600
673
    MAX_MEM_SLOT = 200
674
    NEEDS_COMPAT_SERIAL = False
675

    
676
    # These settings are common to all radios in this family.
677
    _valid_chars = chirp_common.CHARSET_ASCII
678
    numblocks = 0x215           # number of 16-byte blocks in the radio
679
    _memsize = 16 * numblocks   # used by CHIRP file loader to guess radio type
680
    MAX_MEM_SLOT = 200
681

    
682
    # ID string last byte can contain extra info. It is usually 0;
683
    # however, on FT25R/65R sold in Asia we have seen that it can be 3,
684
    # which affects the scaler: instead of the default 50000 it's then 25000.
685
    @property
686
    def subtype(self):
687
        return self.metadata.get('ft4_subtype', 0)
688

    
689
    @subtype.setter
690
    def subtype(self, value):
691
        self.metadata = {'ft4_subtype': int(value)}
692

    
693
    @classmethod
694
    def get_prompts(cls):
695
        rp = chirp_common.RadioPrompts()
696
        rp.experimental = (
697
            'Tested and mostly works, but may give you issues\n'
698
            'when using lesser common radio variants.\n'
699
            'Proceed at your own risk, and let us know about issues!'
700
            )
701

    
702
        rp.pre_download = "".join([
703
            "1. Connect programming cable to MIC jack.\n",
704
            "2. Press OK."
705
            ]
706
            )
707
        rp.pre_upload = rp.pre_download
708
        return rp
709

    
710
    # identify the features that can be manipulated on this radio.
711
    # mentioned here only when different from defaults in chirp_common.py
712
    def get_features(self):
713

    
714
        rf = chirp_common.RadioFeatures()
715
        specials = [name for s in self.class_specials for name in s[1]]
716
        rf.valid_special_chans = specials
717
        rf.memory_bounds = (1, self.MAX_MEM_SLOT)
718
        rf.valid_duplexes = DUPLEX
719
        rf.valid_tmodes = VALID_TONE_MODES
720
        rf.valid_cross_modes = VALID_CROSS_MODES
721
        rf.valid_power_levels = POWER_LEVELS
722
        rf.valid_tuning_steps = self.legal_steps
723
        rf.valid_skips = SKIPS
724
        rf.valid_characters = CHARSET
725
        rf.valid_name_length = self.namelen
726
        rf.valid_modes = ["FM", "NFM"]
727
        rf.valid_bands = self.valid_bands
728
        rf.can_odd_split = True
729
        rf.has_ctone = True
730
        rf.has_rx_dtcs = True
731
        rf.has_dtcs_polarity = False    # REV TN reverses the tone, not the dcs
732
        rf.has_cross = True
733
        rf.has_settings = True
734

    
735
        return rf
736

    
737
    def get_bank_model(self):
738
        return YaesuSC35GenericBankModel(self)
739

    
740
    # read and parse the radio memory
741
    def sync_in(self):
742
        try:
743
            self._mmap = do_download(self)
744
        except Exception as e:
745
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
746
        self.process_mmap()
747

    
748
    # write the memory image to the radio
749
    def sync_out(self):
750
        try:
751
            do_upload(self)
752
        except Exception as e:
753
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
754

    
755
    def process_mmap(self):
756
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
757

    
758
    # There are about 40 settings and most are handled generically in
759
    # get_settings. get_settings invokes these handlers for the few
760
    # that are more complicated.
761

    
762
    # callback for setting  byte arrays: DTMF[0-9], passwd, and CW_ID
763
    def apply_str_to_bytearray(self, element, obj):
764
        lng = len(obj)
765
        string = (element.value.get_value() + "                ")[:lng]
766
        bytes = bytearray(string, "ascii")
767
        for x in range(0, lng):    # memobj cannot iterate, so byte-by-byte
768
            obj[x] = bytes[x]
769
        return
770

    
771
    # add a string value to the RadioSettings
772
    def get_string_setting(self, obj, valid_chars, desc1, desc2, group):
773
        content = ''
774
        maxlen = len(obj)
775
        for x in range(0, maxlen):
776
            content += chr(obj[x])
777
        val = RadioSettingValueString(0, maxlen, content, True, valid_chars)
778
        rs = RadioSetting(desc1, desc2, val)
779
        rs.set_apply_callback(self.apply_str_to_bytearray, obj)
780
        group.append(rs)
781

    
782
    # called when found in the group_descriptions table to handle string value
783
    def get_strset(self, group, parm):
784
        #   parm =(paramname, paramtitle,(handler,[handler params])).
785
        objname, title, fparms = parm
786
        myparms = fparms[1]
787
        obj = getattr(self._memobj.settings,  objname)
788
        self.get_string_setting(obj, myparms[0], objname, title, group)
789

    
790
    # called when found in the group_descriptions table for DTMF strings
791
    def get_dtmfs(self, group, parm):
792
        objname, title, fparms = parm
793
        for i in range(1, 10):
794
            dtmf_digits = self._memobj.dtmf[i - 1].digit
795
            self.get_string_setting(
796
                dtmf_digits, DTMF_CHARS,
797
                "dtmf_%i" % i, "DTMF Autodialer Memory %i" % i, group)
798

    
799
    def apply_P(self, element, pnum, memobj):
800
        memobj.progctl[pnum].usage = element.value
801

    
802
    def apply_Pmode(self, element, pnum, memobj):
803
        memobj.progctl[pnum].parm = element.value
804

    
805
    def apply_Psubmode(self, element, pnum, memobj):
806
        memobj.progctl[pnum].submode = element.value
807

    
808
    def apply_Pmem(self, element, pnum, memobj):
809
        memobj.pmemndx[pnum] = element.value
810

    
811
    MEMLIST = ["%d" % i for i in range(1, MAX_MEM_SLOT + 1)] + PMSNAMES
812
    USAGE_LIST = ["unused", "P channel", "mode or M channel"]
813

    
814
    # called when found in the group_descriptions table
815
    # returns the settings for the programmable keys (P1-P4)
816
    def get_progs(self, group, parm):
817
        _progctl = self._memobj.progctl
818
        _progndx = self._memobj.pmemndx
819

    
820
        def get_prog(i, val_list, valndx, sname, longname, f_apply):
821
            k = str(i + 1)
822
            val = val_list[valndx]
823
            valuelist = RadioSettingValueList(val_list, val)
824
            rs = RadioSetting(sname + k, longname + k, valuelist)
825
            rs.set_apply_callback(f_apply, i, self._memobj)
826
            group.append(rs)
827
        for i in range(0, self.Pkeys):
828
            get_prog(i, self.USAGE_LIST,  _progctl[i].usage,
829
                     "P", "Programmable key ",  self.apply_P)
830
            get_prog(i, self.SETMODES, _progctl[i].parm, "modeP",
831
                     "mode for Programmable key ",  self.apply_Pmode)
832
            get_prog(i, ["0", "1", "2", "3"], _progctl[i].submode, "submodeP",
833
                     "submode for Programmable key ",  self.apply_Psubmode)
834
            get_prog(i, self.MEMLIST, _progndx[i], "memP",
835
                     "mem for Programmable key ",  self.apply_Pmem)
836
    # ------------ End of special settings handlers.
837

    
838
    # list of group description tuples: [groupame,group title, [param list]].
839
    # A param is a tuple:
840
    #  for a simple param: (paramname, paramtitle,[valuename list])
841
    #  for a handler param: (paramname, paramtitle,( handler,[handler params]))
842
    # This is a class variable. subclasses must create a variable named
843
    # class_group_descs. The FT-4 classes simply equate this, but the
844
    # FT-65 classes must copy and modify this.
845
    group_descriptions = [
846
        ("misc", "Miscellaneous Settings", [    # misc
847
         ("apo", "Automatic Power Off",
848
          ["OFF"] + ["%0.1f" % (x * 0.5) for x in range(1, 24 + 1)]),
849
         ("bclo", "Busy Channel Lock-Out", ["OFF", "ON"]),
850
         ("beep", "Enable the Beeper", ["KEY+SC", "KEY", "OFF"]),
851
         ("bsy_led", "Busy LED", ["ON", "OFF"]),
852
         ("edg_beep", "Band Edge Beeper", ["OFF", "ON"]),
853
         ("vox", "VOX", ["OFF", "ON"]),
854
         ("rf_squelch", "RF Squelch Threshold",
855
          ["OFF", "S-1", "S-2", "S-3", "S-4", "S-5", "S-6", "S-7", "S-FULL"]),
856
         ("tot", "Timeout Timer",
857
          ["OFF"] + ["%dMIN" % (x) for x in range(1, 30 + 1)]),
858
         ("tx_led", "TX LED", ["OFF", "ON"]),
859
         ("use_cwid", "use CW ID", ["NO", "YES"]),
860
         ("cw_id",  "CW ID Callsign",  (get_strset, [CW_ID_CHARS])),  # handler
861
         ("vfo_spl", "VFO Split", ["OFF", "ON"]),
862
         ("wfm_rcv", "Enable Broadband FM", ["OFF", "ON"]),
863
         ("passwd", "Password",  (get_strset, [PASSWD_CHARS]))  # handler
864
         ]),
865
        ("arts", "ARTS Settings", [  # arts
866
         ("arts_beep", "ARTS BEEP", ["OFF", "INRANG", "ALWAYS"]),
867
         ("arts_intv", "ARTS Polling Interval", ["25 SEC", "15 SEC"])
868
         ]),
869
        ("ctcss", "CTCSS/DCS/DTMF Settings", [  # ctcss
870
         ("bell", "Bell Repetitions", ["OFF", "1T", "3T", "5T", "8T", "CONT"]),
871
         ("dtmf_mode", "DTMF Mode", ["Manual", "Auto"]),
872
         ("dtmf_delay", "DTMF Autodialer Delay Time",
873
          ["50 MS", "100 MS", "250 MS", "450 MS", "750 MS", "1000 MS"]),
874
         ("dtmf_speed", "DTMF Autodialer Sending Speed", ["50 MS", "100 MS"]),
875
         ("dtmf", "DTMF Autodialer Memory ",  (get_dtmfs, []))  # handler
876
         ]),
877
        ("switch", "Switch/Knob Settings", [  # switch
878
         ("lamp", "Lamp Mode", ["5SEC", "10SEC", "30SEC", "KEY", "OFF"]),
879
         ("moni_tcall", "MONI Switch Function",
880
          ["MONI", "1750", "2100", "1000", "1450"]),
881
         ("key_lock", "Lock Function", ["KEY", "PTT", "KEY+PTT"]),
882
         ("Pkeys", "Pkey fields", (get_progs, []))
883
         ]),
884
        ("scan", "Scan Settings", [   # scan
885
         ("scan_resume", "Scan Resume Mode", ["BUSY", "HOLD", "TIME"]),
886
         ("pri_rvt", "Priority Revert", ["OFF", "ON"]),
887
         ("scan_lamp", "Scan Lamp", ["OFF", "ON"]),
888
         ("wx_alert", "Weather Alert Scan", ["OFF", "ON"])
889
         ]),
890
        ("power", "Power Saver Settings", [  # power
891
         ("battsave", "Receive Mode Battery Save Interval",
892
          ["OFF", "200 MS", "300 MS", "500 MS", "1 S", "2 S"]),
893
         ("tx_save", "Transmitter Battery Saver", ["OFF", "ON"])
894
         ]),
895
        ("eai", "EAI/EPCS Settings", [  # eai
896
         ("pager_tx1", "TX pager frequency 1", EPCS_CODES),
897
         ("pager_tx2", "TX pager frequency 2", EPCS_CODES),
898
         ("pager_rx1", "RX pager frequency 1", EPCS_CODES),
899
         ("pager_rx2", "RX pager frequency 2", EPCS_CODES),
900
         ("pager_ack", "Pager answerback", ["NO", "YES"])
901
         ])
902
        ]
903
    # ----------------end of group_descriptions
904

    
905
    # returns the current values of all the settings in the radio memory image,
906
    # in the form of a RadioSettings list. Uses the class_group_descs
907
    # list to create the groups and params. Valuelist scalars are handled
908
    # inline. More complex params are built by calling the special handlers.
909
    def get_settings(self):
910
        _settings = self._memobj.settings
911
        groups = RadioSettings()
912
        for description in self.class_group_descs:
913
            groupname, title, parms = description
914
            group = RadioSettingGroup(groupname, title)
915
            groups.append(group)
916
            for parm in parms:
917
                param, title, opts = parm
918
                try:
919
                    if isinstance(opts, list):
920
                        # setting is a single value from the list
921
                        objval = getattr(_settings, param)
922
                        value = opts[objval]
923
                        valuelist = RadioSettingValueList(opts, value)
924
                        group.append(RadioSetting(param, title, valuelist))
925
                    else:
926
                        # setting  needs special handling. opts[0] is a
927
                        # function name
928
                        opts[0](self,  group, parm)
929
                except Exception as e:
930
                    LOG.debug(
931
                        "%s: cannot set %s to %s" % (e, param, repr(objval))
932
                        )
933
        return groups
934
        # end of get_settings
935

    
936
    # modify settings values in the radio memory image
937
    def set_settings(self, uisettings):
938
        _settings = self._memobj.settings
939
        for element in uisettings:
940
            if not isinstance(element, RadioSetting):
941
                self.set_settings(element)
942
                continue
943
            if not element.changed():
944
                continue
945

    
946
            try:
947
                name = element.get_name()
948
                value = element.value
949

    
950
                if element.has_apply_callback():
951
                    LOG.debug("Using apply callback")
952
                    element.run_apply_callback()
953
                else:
954
                    setattr(_settings, name, value)
955

    
956
                LOG.debug("Setting %s: %s" % (name, value))
957
            except Exception:
958
                LOG.debug(element.get_name())
959
                raise
960

    
961
    # maps a boolean pair (tx==0,rx==0) to the numbers 0-3
962
    LOOKUP = [[True, True], [True, False], [False, True], [False, False]]
963

    
964
    def decode_sql(self, mem, chan):
965
        """
966
        examine the radio channel fields and determine the correct
967
        CHIRP CSV values for tmode, cross_mode, and sql_override
968
        """
969
        mem.extra = RadioSettingGroup("Extra", "extra")
970
        extra_modes = ["(None)", "PAGER"]
971
        value = extra_modes[chan.sql_type == 6]
972
        valuelist = RadioSettingValueList(extra_modes, value)
973
        rs = RadioSetting("sql_override", "Squelch override", valuelist)
974
        mem.extra.append(rs)
975
        if chan.sql_type == 6:
976
            return
977
        sql_map = RADIO_TMODES[chan.sql_type]
978
        ndx = 0
979
        if len(sql_map[0]) > 1:
980
            # the sql_type is TSQL or DCS, so there are multiple UI mappings
981
            x = getattr(chan, sql_map[1])
982
            r = getattr(chan, sql_map[2])
983
            ndx = self.LOOKUP.index([x == 0, r == 0])
984
            if ndx == 3 and x == r:
985
                ndx = 4
986
        mem.tmode = sql_map[0][ndx][0]
987
        cross = sql_map[0][ndx][1]
988
        if cross:
989
            mem.cross_mode = cross
990
        if chan.rx_ctcss:
991
            mem.ctone = TONE_MAP[chan.rx_ctcss]
992
        if chan.tx_ctcss:
993
            mem.rtone = TONE_MAP[chan.tx_ctcss]
994
        if chan.tx_dcs:
995
            mem.dtcs = DTCS_MAP[chan.tx_dcs]
996
        if chan.rx_dcs:
997
            mem.rx_dtcs = DTCS_MAP[chan.rx_dcs]
998

    
999
    def encode_sql(self, mem, chan):
1000
        """
1001
        examine CHIRP's mem.tmode and mem.cross_mode and set the values
1002
        for the radio sql_type, dcs codes, and ctcss codes. We set all four
1003
        codes, and then zero out a code if needed when Tone or DCS is one-way
1004
        """
1005
        chan.tx_ctcss = TONE_MAP.index(mem.rtone)
1006
        chan.tx_dcs = DTCS_MAP.index(mem.dtcs)
1007
        chan.rx_ctcss = TONE_MAP.index(mem.ctone)
1008
        chan.rx_dcs = DTCS_MAP.index(mem.rx_dtcs)
1009
        if mem.tmode == "TSQL":
1010
            chan.tx_ctcss = chan.rx_ctcss  # CHIRP uses ctone for TSQL
1011
        if mem.tmode == "DTCS":
1012
            chan.tx_dcs = chan.rx_dcs     # CHIRP uses rx_dtcs for DTCS
1013
        # select the correct internal dictionary and key
1014
        mode_dict, key = [
1015
            (TONE_DICT, mem.tmode),
1016
            (CROSS_DICT, mem.cross_mode)
1017
            ][mem.tmode == "Cross"]
1018
        # now look up that key in that dictionary.
1019
        chan.sql_type, suppress = mode_dict[key]
1020
        if suppress:
1021
            setattr(chan, suppress, 0)
1022
        for setting in mem.extra:
1023
            if (setting.get_name() == 'sql_override'):
1024
                value = str(setting.value)
1025
                if value == "PAGER":
1026
                    chan.sql_type = 6
1027
        return
1028

    
1029
    # given a CHIRP memory ref, get the radio memobj for it.
1030
    # A memref is either a number or the name of a special
1031
    # CHIRP will sometimes use numbers (>MAX_SLOTS) for specials
1032
    # returns the obj and several attributes
1033
    def slotloc(self, memref):
1034
        array = None
1035
        num = memref
1036
        sname = memref
1037
        if isinstance(memref, str):   # named special?
1038
            num = self.MAX_MEM_SLOT + 1
1039
            for x in self.class_specials:
1040
                try:
1041
                    ndx = x[1].index(memref)
1042
                    array = x[0]
1043
                    break
1044
                except Exception:
1045
                    num += len(x[1])
1046
            if array is None:
1047
                LOG.debug("unknown Special %s", memref)
1048
                raise
1049
            num += ndx
1050
        elif memref > self.MAX_MEM_SLOT:    # numbered special?
1051
            ndx = memref - (self.MAX_MEM_SLOT + 1)
1052
            for x in self.class_specials:
1053
                if ndx < len(x[1]):
1054
                    array = x[0]
1055
                    sname = x[1][ndx]
1056
                    break
1057
                ndx -= len(x[1])
1058
            if array is None:
1059
                LOG.debug("memref number %d out of range", memref)
1060
                raise
1061
        else:                    # regular memory slot
1062
            array = "memory"
1063
            ndx = memref - 1
1064
        memloc = getattr(self._memobj, array)[ndx]
1065
        return (memloc, ndx, num, array, sname)
1066
        # end of slotloc
1067

    
1068
    # return the raw info for a memory channel
1069
    def get_raw_memory(self, memref):
1070
        memloc, ndx, num, regtype, sname = self.slotloc(memref)
1071
        if regtype == "memory":
1072
            return repr(memloc)
1073
        else:
1074
            return repr(memloc) + repr(self._memobj.names[ndx])
1075

    
1076
    # return the info for a memory channel In CHIRP canonical form
1077
    def get_memory(self, memref):
1078

    
1079
        def clean_name(obj):     # helper func to tidy up the name
1080
            name = ''
1081
            for x in range(0, self.namelen):
1082
                y = obj[x]
1083
                if y == 0:
1084
                    break
1085
                if y == 0x7F:    # when programmed from VFO
1086
                    y = 0x20
1087
                name += chr(y)
1088
            return name.rstrip()
1089

    
1090
        def get_duplex(freq):    # auto duplex to real duplex
1091
            """
1092
            Select the duplex direction if duplex == 'auto'.
1093
            0 is +, 2 is -, and 4 is none.
1094
            """
1095
            return_value = 4     # off, if not in auto range
1096
            for x in self.DUPLEX_AUTO:
1097
                if freq in range(x[0], x[1]):
1098
                    return_value = x[2]
1099
            return return_value
1100

    
1101
        mem = chirp_common.Memory()
1102
        _mem, ndx, num, regtype, sname = self.slotloc(memref)
1103
        mem.number = num
1104
        freq_offset_factor = self.freq_offset_factor
1105
        # FT-25R/65R Asia version (US is 0)?
1106
        if self.subtype == 3 and freq_offset_factor == 2:
1107
            freq_offset_factor = 1  # 25000 scaler
1108

    
1109
        # First, we need to know whether a channel is enabled,
1110
        # then we can process any channel parameters.
1111
        # It was found (at least on an FT-25) that channels might be
1112
        # uninitialized and memory is just completely filled with 0xFF.
1113

    
1114
        if regtype == "pms":
1115
            mem.extd_number = sname
1116
        if regtype in ["memory", "pms"]:
1117
            ndx = num - 1
1118
            mem.name = clean_name(self._memobj.names[ndx].chrs)
1119
            mem.empty = not retrieve_bit(self._memobj.enable, ndx)
1120
            mem.skip = SKIPS[retrieve_bit(self._memobj.scan, ndx)]
1121
        else:
1122
            mem.empty = False
1123
            mem.extd_number = sname
1124
            mem.immutable = ["number", "extd_number", "name", "skip"]
1125

    
1126
        # So, now if channel is not empty, we can do the evaluation of
1127
        # all parameters. Otherwise we set them to defaults.
1128

    
1129
        if mem.empty:
1130
            mem.freq = 0
1131
            mem.offset = 0
1132
            mem.duplex = "off"
1133
            mem.power = POWER_LEVELS[0]  # "High"
1134
            mem.mode = "FM"
1135
            mem.tuning_step = 0
1136
        else:
1137
            mem.freq = int(_mem.freq) * 10
1138
            txfreq = int(self._memobj.txfreqs[ndx].freq) * 10
1139
            if _mem.duplex == 5:  # auto offset
1140
                mem.duplex = DUPLEX[get_duplex(mem.freq)]
1141
            else:
1142
                mem.duplex = DUPLEX[_mem.duplex]
1143
            if _mem.duplex == 6:  # split: offset is tx frequency
1144
                mem.offset = txfreq
1145
            else:
1146
                mem.offset = int(_mem.offset) * 25000 * freq_offset_factor
1147
            self.decode_sql(mem, _mem)
1148
            mem.power = POWER_LEVELS[2 - _mem.tx_pwr]
1149
            mem.mode = ["FM", "NFM"][_mem.tx_width]
1150
            mem.tuning_step = STEP_CODE[_mem.step]
1151
        return mem
1152

    
1153
    def enforce_band(self, memloc, freq, mem_num, sname):
1154
        """
1155
        vfo and home channels are each restricted to a particular band.
1156
        If the frequency is not in the band, use the lower bound
1157
        Raise an exception to cause UI to pop up an error message
1158
        """
1159
        first_vfo_num = self.MAX_MEM_SLOT + len(PMSNAMES) + 1
1160
        band = self.BAND_ASSIGNMENTS[mem_num - first_vfo_num]
1161
        frange = self.valid_bands[band]
1162
        if freq >= frange[0] and freq <= frange[1]:
1163
            memloc.freq = freq / 10
1164
            return freq
1165
        memloc.freq = frange[0] / 10
1166
        raise Exception("freq out of range for %s" % sname)
1167

    
1168
    # modify a radio channel in memobj based on info in CHIRP canonical form
1169
    def set_memory(self, mem):
1170
        _mem, ndx, num, regtype, sname = self.slotloc(mem.number)
1171
        assert _mem
1172
        if mem.empty:
1173
            if regtype in ["memory", "pms"]:
1174
                store_bit(self._memobj.enable, ndx, False)
1175
                return
1176

    
1177
        txfreq = mem.freq / 10     # really. RX freq is used for TX base
1178
        _mem.freq = txfreq
1179
        self.encode_sql(mem, _mem)
1180
        if mem.power:
1181
            _mem.tx_pwr = 2 - POWER_LEVELS.index(mem.power)
1182
        else:
1183
            _mem.tx_pwr = 0  # set to "High" if CHIRP canonical value is None
1184
        _mem.tx_width = mem.mode == "NFM"
1185
        _mem.step = STEP_CODE.index(mem.tuning_step)
1186

    
1187
        freq_offset_factor = self.freq_offset_factor
1188
        # FT-25R/65R Asia version (US version is 0)?
1189
        if self.subtype == 3 and freq_offset_factor == 2:
1190
            freq_offset_factor = 1  # 25000 scaler
1191
        _mem.offset = mem.offset / (25000 * freq_offset_factor)
1192
        duplex = mem.duplex
1193
        if regtype in ["memory", "pms"]:
1194
            ndx = num - 1
1195
            store_bit(self._memobj.enable, ndx, True)
1196
            store_bit(self._memobj.scan, ndx, SKIPS.index(mem.skip))
1197
            nametrim = (mem.name + "        ")[:8]
1198
            self._memobj.names[ndx].chrs = bytearray(nametrim, "ascii")
1199
            if mem.duplex == "split":
1200
                txfreq = mem.offset / 10
1201
            self._memobj.txfreqs[num-1].freq = txfreq
1202
        _mem.duplex = DUPLEX.index(duplex)
1203
        if regtype in ["vfo", "home"]:
1204
            self.enforce_band(_mem, mem.freq, num, sname)
1205

    
1206
        return
1207

    
1208

    
1209
class YaesuFT4GenericRadio(YaesuSC35GenericRadio):
1210
    """
1211
    FT-4 sub family class. Classes for individual radios extend
1212
    these classes and are found at the end of this file.
1213
    """
1214
    class_specials = SPECIALS_FT4
1215
    Pkeys = 2     # number of programmable keys
1216
    namelen = 6   # length of the mem name display on the front-panel
1217
    freq_offset_factor = 1  # 25000 * 1
1218
    class_group_descs = YaesuSC35GenericRadio.group_descriptions
1219
    # names for the setmode function for the programmable keys. Mode zero means
1220
    # that the key is programmed for a memory not a setmode.
1221
    SETMODES = [
1222
        "mem", "apo", "ar bep", "ar int", "beclo",              # 00-04
1223
        "beep", "bell", "cw id", "cw wrt", "dc vlt",            # 05-09
1224
        "dcs cod", "dt dly", "dt set", "dtc spd", "edg.bep",    # 10-14
1225
        "lamp", "led.bsy", "led.tx", "lock", "m/t-cl",          # 15-19
1226
        "mem.del", "mem.tag", "pag.abk", "pag.cdr", "pag.cdt",  # 20-24
1227
        "pri.rvt", "pswd", "pswdwt", "rf sql", "rpt.ars",       # 25-29
1228
        "rpt.frq", "rpt.sft", "rxsave", "scn.lmp", "scn.rsm",   # 30-34
1229
        "skip", "sql.typ", "step", "tn frq", "tot",             # 35-39
1230
        "tx pwr", "tx save", "vfo.spl", "vox", "wfm.rcv",       # 40-44
1231
        "w/n.dev", "wx.alert"                                   # 45-46
1232
        ]
1233

    
1234

    
1235
class YaesuFT65GenericRadio(YaesuSC35GenericRadio):
1236
    """
1237
    FT-65 sub family class. Classes for individual radios extend
1238
    these classes and are found at the end of this file.
1239
    """
1240
    class_specials = SPECIALS_FT65
1241
    Pkeys = 4     # number of programmable keys
1242
    namelen = 8   # length of the mem name display on the front-panel
1243
    freq_offset_factor = 2  # 25000 * 2
1244
    # we need a deep copy here because we are adding deeper than the top level.
1245
    class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions)
1246
    add_paramdesc(
1247
        class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"]))
1248
    # names for the setmode function for the programmable keys. Mode zero means
1249
    # that the key is programmed for a memory not a setmode.
1250
    SETMODES = [
1251
        "mem", "apo", "arts", "battsave", "b-ch.l/o",              # 00-04
1252
        "beep", "bell", "compander", "ctcss", "cw id",             # 05-09
1253
        "dc volt", "dcs code", "dtmf set", "dtmf wrt", "edg bep",  # 10-14
1254
        "key lock", "lamp", "ledbsy", "mem del", "mon/t-cl",       # 15-19
1255
        "name tag", "pager", "password", "pri.rvt", "repeater",    # 20-24
1256
        "resume", "rf.sql", "scn.lamp", "skip", "sql type",        # 25-29
1257
        "step", "tot", "tx pwr", "tx save", "vfo.spl",             # 30-34
1258
        "vox", "wfm.rcv", "wide/nar", "wx alert", "scramble"       # 35-39
1259
        ]
1260

    
1261

    
1262
# Classes for each individual radio.
1263

    
1264

    
1265
@directory.register
1266
class YaesuFT4XRRadio(YaesuFT4GenericRadio):
1267
    """
1268
    FT-4X dual band, US version
1269
    """
1270
    MODEL = "FT-4XR"
1271
    id_str = b'IFT-35R\x00\x00V100\x00\x00'
1272
    valid_bands = VALID_BANDS_DUAL
1273
    DUPLEX_AUTO = DUPLEX_AUTO_US
1274
    legal_steps = US_LEGAL_STEPS
1275
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1276

    
1277

    
1278
@directory.register
1279
class YaesuFT4XERadio(YaesuFT4GenericRadio):
1280
    """
1281
    FT-4X dual band, EU version
1282
    """
1283
    MODEL = "FT-4XE"
1284
    id_str = b'IFT-35R\x00\x00V100\x00\x00'
1285
    valid_bands = VALID_BANDS_DUAL
1286
    DUPLEX_AUTO = DUPLEX_AUTO_EU
1287
    legal_steps = STEP_CODE
1288
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1289

    
1290

    
1291
@directory.register
1292
class YaesuFT4VRRadio(YaesuFT4GenericRadio):
1293
    """
1294
    FT-4V VHF, US version
1295
    """
1296
    MODEL = "FT-4VR"
1297
    id_str = b'IFT-15R\x00\x00V100\x00\x00'
1298
    valid_bands = VALID_BANDS_VHF
1299
    DUPLEX_AUTO = DUPLEX_AUTO_US
1300
    legal_steps = US_LEGAL_STEPS
1301
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1302

    
1303

    
1304
# No image available yet
1305
# @directory.register
1306
# class YaesuFT4VERadio(YaesuFT4GenericRadio):
1307
#    """
1308
#    FT-4V VHF, EU version
1309
#    """
1310
#    MODEL = "FT-4VE"
1311
#    id_str = b'IFT-15R\x00\x00V100\x00\x00'
1312
#    valid_bands = VALID_BANDS_VHF
1313
#    DUPLEX_AUTO = DUPLEX_AUTO_EU
1314
#    legal_steps = STEP_CODE
1315
#    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1316

    
1317

    
1318
@directory.register
1319
class YaesuFT65RRadio(YaesuFT65GenericRadio):
1320
    """
1321
    FT-65 dual band, US/Asia version
1322
    """
1323
    MODEL = "FT-65R"
1324
    id_str = b'IH-420\x00\x00\x00V100\x00\x00'
1325
    valid_bands = VALID_BANDS_DUAL
1326
    DUPLEX_AUTO = DUPLEX_AUTO_US
1327
    legal_steps = STEP_CODE
1328
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1329

    
1330

    
1331
@directory.register
1332
class YaesuFT65ERadio(YaesuFT65GenericRadio):
1333
    """
1334
    FT-65 dual band, EU version
1335
    """
1336
    MODEL = "FT-65E"
1337
    id_str = b'IH-420\x00\x00\x00V100\x00\x00'
1338
    valid_bands = VALID_BANDS_DUAL
1339
    DUPLEX_AUTO = DUPLEX_AUTO_EU
1340
    legal_steps = STEP_CODE
1341
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1342

    
1343

    
1344
@directory.register
1345
class YaesuFT25RRadio(YaesuFT65GenericRadio):
1346
    """
1347
    FT-25 VHF, US/Asia version
1348
    """
1349
    MODEL = "FT-25R"
1350
    id_str = b'IFT-25R\x00\x00V100\x00\x00'
1351
    valid_bands = VALID_BANDS_VHF
1352
    DUPLEX_AUTO = DUPLEX_AUTO_US
1353
    legal_steps = US_LEGAL_STEPS
1354
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1355

    
1356

    
1357
# No image available yet
1358
# @directory.register
1359
# class YaesuFT25ERadio(YaesuFT65GenericRadio):
1360
#    """
1361
#    FT-25 VHF, EU version
1362
#    """
1363
#    MODEL = "FT-25E"
1364
#    id_str = b'IFT-25R\x00\x00V100\x00\x00'
1365
#    valid_bands = VALID_BANDS_VHF
1366
#    DUPLEX_AUTO = DUPLEX_AUTO_EU
1367
#    legal_steps = STEP_CODE
1368
#    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
(7-7/9)