ft4(with_6.25_step_for_ft-65r).py

Jim Unroe, 02/03/2021 10:45 am

Download (51.1 kB)

 
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
    if id_response != radio.id_str:
312
        substr0 = radio.id_str[:radio.id_str.find('\x00')]
313
        if id_response[:id_response.find('\x00')] != substr0:
314
            msg = "ID mismatch. Expected" + util.hexprint(radio.id_str)
315
            msg += ", Received:" + util.hexprint(id_response)
316
            LOG.warning(msg)
317
            raise errors.RadioError("Incorrect ID read from radio.")
318
        else:
319
            msg = "ID suspect. Expected" + util.hexprint(radio.id_str)
320
            msg += ", Received:" + util.hexprint(id_response)
321
            LOG.warning(msg)
322
    return progressbar
323

    
324

    
325
def getblock(pipe, addr, image):
326
    """
327
    read a single 16-byte block from the radio.
328
    send the command and check the response
329
    places the response into the correct offset in the supplied bytearray
330
    returns True if successful, False if error.
331
    """
332
    cmd = struct.pack(">cHb", b"R", addr, 16)
333
    response = sendcmd(pipe, cmd, 21)
334
    if (response[0] != b"W"[0]) or (response[1:4] != cmd[1:4]):
335
        msg = "Bad response. Sent:" + util.hexprint(cmd) + ", "
336
        msg += b"Received:" + util.hexprint(response)
337
        LOG.debug(msg)
338
        return False
339
    if checkSum8(response[1:20]) != bytearray(response)[20]:
340
        LOG.debug(b"Bad checksum: " + util.hexprint(response))
341
        return False
342
    image[addr:addr+16] = response[4:20]
343
    return True
344

    
345

    
346
def do_download(radio):
347
    """
348
    Read memory from the radio.
349
      call startcomms to go into program mode and check version
350
      create an mmap
351
      read the memory blocks and place the data into the mmap
352
      send "END"
353
    """
354
    image = bytearray(radio.get_memsize())
355
    pipe = radio.pipe  # Get the serial port connection
356
    progressbar = startcomms(radio, "from")
357
    for blocknum in range(radio.numblocks):
358
        for i in range(0, 3):
359
            if getblock(pipe, 16 * blocknum, image):
360
                break
361
            if i == 2:
362
                raise errors.RadioError(
363
                   "read block from radio failed 3 times")
364
        progressbar.cur = blocknum
365
        radio.status_fn(progressbar)
366
    sendcmd(pipe, b"END", 0)
367
    return memmap.MemoryMap(bytes(image))
368

    
369

    
370
def putblock(pipe, addr, data):
371
    """
372
    write a single 16-byte block to the radio
373
    send the command and check the response
374
    """
375
    chkstr = struct.pack(">Hb",  addr, 16) + data
376
    msg = b'W' + chkstr + struct.pack('B', checkSum8(chkstr)) + b'\x06'
377
    sendcmd(pipe, msg, 0)
378

    
379

    
380
def do_upload(radio):
381
    """
382
    Write memory image to radio
383
      call startcomms to go into program mode and check version
384
      write the memory blocks. Skip the first block
385
      send "END"
386
    """
387
    pipe = radio.pipe  # Get the serial port connection
388
    progressbar = startcomms(radio, "to")
389
    data = get_mmap_data(radio)
390
    for _i in range(1, radio.numblocks):
391
        putblock(pipe, 16*_i, data[16*_i:16*(_i+1)])
392
        progressbar.cur = _i
393
        radio.status_fn(progressbar)
394
    sendcmd(pipe, b"END", 0)
395
    return
396
# End serial transfer utilities
397

    
398

    
399
def bit_loc(bitnum):
400
    """
401
    return the ndx and mask for a bit location
402
    """
403
    return (bitnum // 8, 1 << (bitnum & 7))
404

    
405

    
406
def store_bit(bankmem, bitnum, val):
407
    """
408
    store a bit in a bankmem. Store 0 or 1 for False or True
409
    """
410
    ndx, mask = bit_loc(bitnum)
411
    if val:
412
        bankmem.b8[ndx] |= mask
413
    else:
414
        bankmem.b8[ndx] &= ~mask
415
    return
416

    
417

    
418
def retrieve_bit(bankmem, bitnum):
419
    """
420
    return True or False for a bit in a bankmem
421
    """
422
    ndx, mask = bit_loc(bitnum)
423
    return (bankmem.b8[ndx] & mask) != 0
424

    
425

    
426
# A bank is a bitmap of 220 bits. 200 mem slots and 2*10 PMS slots.
427
# There are 10 banks.
428
class YaesuSC35GenericBankModel(chirp_common.BankModel):
429

    
430
    def get_num_mappings(self):
431
        return 10
432

    
433
    def get_mappings(self):
434
        banks = []
435
        for i in range(1, 1 + self.get_num_mappings()):
436
            bank = chirp_common.Bank(self, "%i" % i, "Bank %i" % i)
437
            bank.index = i - 1
438
            banks.append(bank)
439
        return banks
440

    
441
    def add_memory_to_mapping(self, memory, bank):
442
        bankmem = self._radio._memobj.bankmask[bank.index]
443
        store_bit(bankmem, memory.number-1, True)
444

    
445
    def remove_memory_from_mapping(self, memory, bank):
446
        bankmem = self._radio._memobj.bankmask[bank.index]
447
        if not retrieve_bit(bankmem, memory.number-1):
448
            raise Exception("Memory %i is not in bank %s." %
449
                            (memory.number, bank))
450
        store_bit(bankmem, memory.number-1, False)
451

    
452
    # return a list of slots in a bank
453
    def get_mapping_memories(self, bank):
454
        memories = []
455
        for i in range(*self._radio.get_features().memory_bounds):
456
            if retrieve_bit(self._radio._memobj.bankmask[bank.index], i - 1):
457
                memories.append(self._radio.get_memory(i))
458
        return memories
459

    
460
    # return a list of banks a slot is a member of
461
    def get_memory_mappings(self, memory):
462
        memndx = memory.number - 1
463
        banks = []
464
        for bank in self.get_mappings():
465
            if retrieve_bit(self._radio._memobj.bankmask[bank.index], memndx):
466
                banks.append(bank)
467
        return banks
468

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

    
473
DUPLEX = ["+", "", "-", "", "off", "", "split"]  # (0,2,4,5) = (+, -, 0, auto)
474
# The radio implements duplex "auto" as 5. We map to "". It is a convenience
475
# function in the radio that affects the offset, which sets the duplex value
476
# according to the frequency in use. Yaesu doesn't entirely adhere to the band
477
# plans; but worse, they save the value 'auto' instead of + or -. Why Yaesu
478
# is doing such a thing is beyond me. [AE6YN]
479
DUPLEX_AUTO_US = [
480
    [145100000, 145495000, 2],
481
    [146000000, 146395000, 0],
482
    [146600000, 146995000, 2],
483
    [147000000, 147395000, 0],
484
    [147600000, 147995000, 2]]
485
# (There are no automatic duplex values in IARU-2 70CM.)
486
DUPLEX_AUTO_EU = [
487
    [145600000, 145800000, 2],
488
    [438200000, 439425000, 2]]
489

    
490
SKIPS = ["S", ""]
491

    
492
POWER_LEVELS = [
493
    chirp_common.PowerLevel("High", watts=5.0),  # high must be first (0)
494
    chirp_common.PowerLevel("Mid", watts=2.5),
495
    chirp_common.PowerLevel("Low", watts=0.5)]
496

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

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

    
536
# Find all legal values for the tmode and cross fields for the UI.
537
# We build two dictionaries to do the lookups when encoding.
538
# The reversed range is a kludge: by happenstance, earlier duplicates
539
# in the above table are the preferred mapping, they override the
540
# later ones when we process the table backwards.
541
TONE_DICT = {}          # encode sql_type.
542
CROSS_DICT = {}         # encode sql_type.
543

    
544
for sql_type in reversed(range(0, len(RADIO_TMODES))):
545
    sql_type_row = RADIO_TMODES[sql_type]
546
    for decode_row in sql_type_row[0]:
547
        suppress = None
548
        if len(decode_row) == 3:
549
            suppress = decode_row[2]
550
        TONE_DICT[decode_row[0]] = (sql_type, suppress)
551
        if decode_row[1]:
552
            CROSS_DICT[decode_row[1]] = (sql_type, suppress)
553

    
554
# The keys are added to the "VALID" lists using code that puts those lists
555
# in the same order as the UI's default order instead of the random dict
556
# order or the arbitrary build order.
557
VALID_TONE_MODES = []   # list for UI.
558
VALID_CROSS_MODES = []  # list for UI.
559
for name in chirp_common.TONE_MODES:
560
    if name in TONE_DICT:
561
        VALID_TONE_MODES += [name]
562
for name in chirp_common.CROSS_MODES:
563
    if name in CROSS_DICT:
564
        VALID_CROSS_MODES += [name]
565

    
566

    
567
DTMF_CHARS = "0123456789ABCD*#- "
568
CW_ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ "
569
PASSWD_CHARS = "0123456789"
570
CHARSET = CW_ID_CHARS + "abcdefghijklmnopqrstuvwxyz*+-/@"
571
PMSNAMES = ["%s%02d" % (c, i) for i in range(1, 11) for c in ('L', 'U')]
572

    
573
# Four separate arrays of special channel mems.
574
# Each special has unique constrants: band, name yes/no, and pms L/U
575
# The FT-65 class replaces the "prog" entry in this list.
576
# The name field must be the name of a slot array in MEM_FORMAT
577
SPECIALS_FT4 = [
578
    ("pms", PMSNAMES),
579
    ("vfo", ["VFO A UHF", "VFO A VHF", "VFO B FM", "VFO B VHF", "VFO B UHF"]),
580
    ("home", ["HOME FM", "HOME VHF", "HOME UHF"]),
581
    ("prog", ["P1", "P2"])
582
    ]
583
SPECIALS_FT65 = SPECIALS_FT4
584
FT65_PROGS = ("prog", ["P1", "P2", "P3", "P4"])
585
SPECIALS_FT65[-1] = FT65_PROGS    # replace the last entry (P key names)
586

    
587

    
588
VALID_BANDS_DUAL = [
589
    (65000000, 108000000),     # broadcast FM, receive only
590
    (136000000, 174000000),    # VHF
591
    (400000000, 480000000)     # UHF
592
    ]
593

    
594
VALID_BANDS_VHF = [
595
    (65000000, 108000000),     # broadcast FM, receive only
596
    (136000000, 174000000),    # VHF
597
    ]
598

    
599
# bands for the five VFO and three home channel memories
600
BAND_ASSIGNMENTS_DUALBAND = [2, 1, 0, 1, 2, 0, 1, 2]  # all locations used
601
BAND_ASSIGNMENTS_MONO_VHF = [1, 1, 0, 1, 1, 0, 1, 1]  # UHF locations unused
602

    
603

    
604
# None, and 50 Tones. Use this explicit array because the
605
# one in chirp_common could change and no longer describe our radio
606
TONE_MAP = [
607
    None, 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5,
608
    85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5,
609
    107.2, 110.9, 114.8, 118.8, 123.0, 127.3,
610
    131.8, 136.5, 141.3, 146.2, 151.4, 156.7,
611
    159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
612
    177.3, 179.9, 183.5, 186.2, 189.9, 192.8,
613
    196.6, 199.5, 203.5, 206.5, 210.7, 218.1,
614
    225.7, 229.1, 233.6, 241.8, 250.3, 254.1
615
    ]
616

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

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

    
636

    
637
# allow a child class to add a param to its class
638
# description list. used when a specific radio class has
639
# a param that is not in the generic list.
640
def add_paramdesc(desc_list, group, param):
641
    for description in desc_list:
642
        groupname, title, parms = description
643
        if group == groupname:
644
            description[2].append(param)
645
            return
646

    
647

    
648
class YaesuSC35GenericRadio(chirp_common.CloneModeRadio,
649
                            chirp_common.ExperimentalRadio):
650
    """
651
    Base class for all Yaesu radios using the SCU-35 programming cable
652
    and its protocol. Classes for sub families extend this class and
653
    are found towards the end of this file.
654
    """
655
    VENDOR = "Yaesu"
656
    MODEL = "SCU-35Generic"  # No radio directly uses the base class
657
    BAUD_RATE = 9600
658
    MAX_MEM_SLOT = 200
659
    NEEDS_COMPAT_SERIAL = False
660

    
661
    # These settings are common to all radios in this family.
662
    _valid_chars = chirp_common.CHARSET_ASCII
663
    numblocks = 0x215           # number of 16-byte blocks in the radio
664
    _memsize = 16 * numblocks   # used by CHIRP file loader to guess radio type
665
    MAX_MEM_SLOT = 200
666

    
667
    @classmethod
668
    def get_prompts(cls):
669
        rp = chirp_common.RadioPrompts()
670
        rp.experimental = (
671
            'Tested only by the developer and only on a single radio.\n'
672
            'Proceed at your own risk!'
673
            )
674

    
675
        rp.pre_download = "".join([
676
            "1. Connect programming cable to MIC jack.\n",
677
            "2. Press OK."
678
            ]
679
            )
680
        rp.pre_upload = rp.pre_download
681
        return rp
682

    
683
    # identify the features that can be manipulated on this radio.
684
    # mentioned here only when different from defaults in chirp_common.py
685
    def get_features(self):
686

    
687
        rf = chirp_common.RadioFeatures()
688
        specials = [name for s in self.class_specials for name in s[1]]
689
        rf.valid_special_chans = specials
690
        rf.memory_bounds = (1, self.MAX_MEM_SLOT)
691
        rf.valid_duplexes = DUPLEX
692
        rf.valid_tmodes = VALID_TONE_MODES
693
        rf.valid_cross_modes = VALID_CROSS_MODES
694
        rf.valid_power_levels = POWER_LEVELS
695
        rf.valid_tuning_steps = self.legal_steps
696
        rf.valid_skips = SKIPS
697
        rf.valid_characters = CHARSET
698
        rf.valid_name_length = self.namelen
699
        rf.valid_modes = ["FM", "NFM"]
700
        rf.valid_bands = self.valid_bands
701
        rf.can_odd_split = True
702
        rf.has_ctone = True
703
        rf.has_rx_dtcs = True
704
        rf.has_dtcs_polarity = False    # REV TN reverses the tone, not the dcs
705
        rf.has_cross = True
706
        rf.has_settings = True
707

    
708
        return rf
709

    
710
    def get_bank_model(self):
711
        return YaesuSC35GenericBankModel(self)
712

    
713
    # read and parse the radio memory
714
    def sync_in(self):
715
        try:
716
            self._mmap = do_download(self)
717
        except Exception as e:
718
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
719
        self.process_mmap()
720

    
721
    # write the memory image to the radio
722
    def sync_out(self):
723
        try:
724
            do_upload(self)
725
        except Exception as e:
726
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
727

    
728
    def process_mmap(self):
729
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
730

    
731
    # There are about 40 settings and most are handled generically in
732
    # get_settings. get_settings invokes these handlers for the few
733
    # that are more complicated.
734

    
735
    # callback for setting  byte arrays: DTMF[0-9], passwd, and CW_ID
736
    def apply_str_to_bytearray(self, element, obj):
737
        lng = len(obj)
738
        strng = (element.value.get_value() + "                ")[:lng]
739
        bytes = bytearray(strng, "ascii")
740
        for x in range(0, lng):    # memobj cannot iterate, so byte-by-byte
741
            obj[x] = bytes[x]
742
        return
743

    
744
    # add a string value to the RadioSettings
745
    def get_string_setting(self, obj, valid_chars, desc1, desc2, group):
746
        content = ''
747
        maxlen = len(obj)
748
        for x in range(0, maxlen):
749
            content += chr(obj[x])
750
        val = RadioSettingValueString(0, maxlen, content, True, valid_chars)
751
        rs = RadioSetting(desc1, desc2, val)
752
        rs.set_apply_callback(self.apply_str_to_bytearray, obj)
753
        group.append(rs)
754

    
755
    # called when found in the group_descriptions table to handle string value
756
    def get_strset(self, group, parm):
757
        #   parm =(paramname, paramtitle,(handler,[handler params])).
758
        objname, title, fparms = parm
759
        myparms = fparms[1]
760
        obj = getattr(self._memobj.settings,  objname)
761
        self.get_string_setting(obj, myparms[0], objname, title, group)
762

    
763
    # called when found in the group_descriptions table for DTMF strings
764
    def get_dtmfs(self, group, parm):
765
        objname, title, fparms = parm
766
        for i in range(1, 10):
767
            dtmf_digits = self._memobj.dtmf[i - 1].digit
768
            self.get_string_setting(
769
                dtmf_digits, DTMF_CHARS,
770
                "dtmf_%i" % i, "DTMF Autodialer Memory %i" % i, group)
771

    
772
    def apply_P(self, element, pnum, memobj):
773
        memobj.progctl[pnum].usage = element.value
774

    
775
    def apply_Pmode(self, element, pnum, memobj):
776
        memobj.progctl[pnum].parm = element.value
777

    
778
    def apply_Psubmode(self, element, pnum, memobj):
779
        memobj.progctl[pnum].submode = element.value
780

    
781
    def apply_Pmem(self, element, pnum, memobj):
782
        memobj.pmemndx[pnum] = element.value
783

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

    
787
    # called when found in the group_descriptions table
788
    # returns the settings for the programmable keys (P1-P4)
789
    def get_progs(self, group, parm):
790
        _progctl = self._memobj.progctl
791
        _progndx = self._memobj.pmemndx
792

    
793
        def get_prog(i, val_list, valndx, sname, longname, f_apply):
794
            k = str(i + 1)
795
            val = val_list[valndx]
796
            valuelist = RadioSettingValueList(val_list, val)
797
            rs = RadioSetting(sname + k, longname + k, valuelist)
798
            rs.set_apply_callback(f_apply, i, self._memobj)
799
            group.append(rs)
800
        for i in range(0, self.Pkeys):
801
            get_prog(i, self.USAGE_LIST,  _progctl[i].usage,
802
                     "P", "Programmable key ",  self.apply_P)
803
            get_prog(i, self.SETMODES, _progctl[i].parm, "modeP",
804
                     "mode for Programmable key ",  self.apply_Pmode)
805
            get_prog(i, ["0", "1", "2", "3"], _progctl[i].submode, "submodeP",
806
                     "submode for Programmable key ",  self.apply_Psubmode)
807
            get_prog(i, self.MEMLIST, _progndx[i], "memP",
808
                     "mem for Programmable key ",  self.apply_Pmem)
809
    # ------------ End of special settings handlers.
810

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

    
878
    # returns the current values of all the settings in the radio memory image,
879
    # in the form of a RadioSettings list. Uses the class_group_descs
880
    # list to create the groups and params. Valuelist scalars are handled
881
    # inline. More complex params are built by calling the special handlers.
882
    def get_settings(self):
883
        _settings = self._memobj.settings
884
        groups = RadioSettings()
885
        for description in self.class_group_descs:
886
            groupname, title, parms = description
887
            group = RadioSettingGroup(groupname, title)
888
            groups.append(group)
889
            for parm in parms:
890
                param, title, opts = parm
891
                try:
892
                    if isinstance(opts, list):
893
                        # setting is a single value from the list
894
                        objval = getattr(_settings, param)
895
                        value = opts[objval]
896
                        valuelist = RadioSettingValueList(opts, value)
897
                        group.append(RadioSetting(param, title, valuelist))
898
                    else:
899
                        # setting  needs special handling. opts[0] is a
900
                        # function name
901
                        opts[0](self,  group, parm)
902
                except Exception as e:
903
                    LOG.debug(
904
                        "%s: cannot set %s to %s" % (e, param, repr(objval))
905
                        )
906
        return groups
907
        # end of get_settings
908

    
909
    # modify settings values in the radio memory image
910
    def set_settings(self, uisettings):
911
        _settings = self._memobj.settings
912
        for element in uisettings:
913
            if not isinstance(element, RadioSetting):
914
                self.set_settings(element)
915
                continue
916
            if not element.changed():
917
                continue
918

    
919
            try:
920
                name = element.get_name()
921
                value = element.value
922

    
923
                if element.has_apply_callback():
924
                    LOG.debug("Using apply callback")
925
                    element.run_apply_callback()
926
                else:
927
                    setattr(_settings, name, value)
928

    
929
                LOG.debug("Setting %s: %s" % (name, value))
930
            except:
931
                LOG.debug(element.get_name())
932
                raise
933

    
934
    # maps a boolean pair (tx==0,rx==0) to the numbers 0-3
935
    LOOKUP = [[True, True], [True, False], [False, True], [False, False]]
936

    
937
    def decode_sql(self, mem, chan):
938
        """
939
        examine the radio channel fields and determine the correct
940
        CHIRP CSV values for tmode, cross_mode, and sql_override
941
        """
942
        mem.extra = RadioSettingGroup("Extra", "extra")
943
        extra_modes = ["(None)", "PAGER"]
944
        value = extra_modes[chan.sql_type == 6]
945
        valuelist = RadioSettingValueList(extra_modes, value)
946
        rs = RadioSetting("sql_override", "Squelch override", valuelist)
947
        mem.extra.append(rs)
948
        if chan.sql_type == 6:
949
            return
950
        sql_map = RADIO_TMODES[chan.sql_type]
951
        ndx = 0
952
        if len(sql_map[0]) > 1:
953
            # the sql_type is TSQL or DCS, so there are multiple UI mappings
954
            x = getattr(chan, sql_map[1])
955
            r = getattr(chan, sql_map[2])
956
            ndx = self.LOOKUP.index([x == 0, r == 0])
957
            if ndx == 3 and x == r:
958
                ndx = 4
959
        mem.tmode = sql_map[0][ndx][0]
960
        cross = sql_map[0][ndx][1]
961
        if cross:
962
            mem.cross_mode = cross
963
        if chan.rx_ctcss:
964
            mem.ctone = TONE_MAP[chan.rx_ctcss]
965
        if chan.tx_ctcss:
966
            mem.rtone = TONE_MAP[chan.tx_ctcss]
967
        if chan.tx_dcs:
968
            mem.dtcs = DTCS_MAP[chan.tx_dcs]
969
        if chan.rx_dcs:
970
            mem.rx_dtcs = DTCS_MAP[chan.rx_dcs]
971

    
972
    def encode_sql(self, mem, chan):
973
        """
974
        examine CHIRP's mem.tmode and mem.cross_mode and set the values
975
        for the radio sql_type, dcs codes, and ctcss codes. We set all four
976
        codes, and then zero out a code if needed when Tone or DCS is one-way
977
        """
978
        chan.tx_ctcss = TONE_MAP.index(mem.rtone)
979
        chan.tx_dcs = DTCS_MAP.index(mem.dtcs)
980
        chan.rx_ctcss = TONE_MAP.index(mem.ctone)
981
        chan.rx_dcs = DTCS_MAP.index(mem.rx_dtcs)
982
        if mem.tmode == "TSQL":
983
            chan.tx_ctcss = chan.rx_ctcss  # CHIRP uses ctone for TSQL
984
        if mem.tmode == "DTCS":
985
            chan.tx_dcs = chan.rx_dcs     # CHIRP uses rx_dtcs for DTCS
986
        # select the correct internal dictionary and key
987
        mode_dict, key = [
988
            (TONE_DICT, mem.tmode),
989
            (CROSS_DICT, mem.cross_mode)
990
            ][mem.tmode == "Cross"]
991
        # now look up that key in that dictionary.
992
        chan.sql_type, suppress = mode_dict[key]
993
        if suppress:
994
            setattr(chan, suppress, 0)
995
        for setting in mem.extra:
996
            if (setting.get_name() == 'sql_override'):
997
                value = str(setting.value)
998
                if value == "PAGER":
999
                    chan.sql_type = 6
1000
        return
1001

    
1002
    # given a CHIRP memory ref, get the radio memobj for it.
1003
    # A memref is either a number or the name of a special
1004
    # CHIRP will sometimes use numbers (>MAX_SLOTS) for specials
1005
    # returns the obj and several attributes
1006
    def slotloc(self, memref):
1007
        array = None
1008
        num = memref
1009
        sname = memref
1010
        if isinstance(memref, str):   # named special?
1011
            num = self.MAX_MEM_SLOT + 1
1012
            for x in self.class_specials:
1013
                try:
1014
                    ndx = x[1].index(memref)
1015
                    array = x[0]
1016
                    break
1017
                except:
1018
                    num += len(x[1])
1019
            if array is None:
1020
                LOG.debug("unknown Special %s", memref)
1021
                raise
1022
            num += ndx
1023
        elif memref > self.MAX_MEM_SLOT:    # numbered special?
1024
            ndx = memref - (self.MAX_MEM_SLOT + 1)
1025
            for x in self.class_specials:
1026
                if ndx < len(x[1]):
1027
                    array = x[0]
1028
                    sname = x[1][ndx]
1029
                    break
1030
                ndx -= len(x[1])
1031
            if array is None:
1032
                LOG.debug("memref number %d out of range", memref)
1033
                raise
1034
        else:                    # regular memory slot
1035
            array = "memory"
1036
            ndx = memref - 1
1037
        memloc = getattr(self._memobj, array)[ndx]
1038
        return (memloc, ndx, num, array, sname)
1039
        # end of slotloc
1040

    
1041
    # return the raw info for a memory channel
1042
    def get_raw_memory(self, memref):
1043
        memloc, ndx, num, regtype, sname = self.slotloc(memref)
1044
        if regtype == "memory":
1045
            return repr(memloc)
1046
        else:
1047
            return repr(memloc) + repr(self._memobj.names[ndx])
1048

    
1049
    # return the info for a memory channel In CHIRP canonical form
1050
    def get_memory(self, memref):
1051

    
1052
        def clean_name(obj):     # helper func to tidy up the name
1053
            name = ''
1054
            for x in range(0, self.namelen):
1055
                y = obj[x]
1056
                if y == 0:
1057
                    break
1058
                if y == 0x7F:    # when programmed from VFO
1059
                    y = 0x20
1060
                name += chr(y)
1061
            return name.rstrip()
1062

    
1063
        def get_duplex(freq):    # auto duplex to real duplex
1064
            """
1065
            Select the duplex direction if duplex == 'auto'.
1066
            0 is +, 2 is -, and 4 is none.
1067
            """
1068
            return_value = 4     # off, if not in auto range
1069
            for x in self.DUPLEX_AUTO:
1070
                if freq in range(x[0], x[1]):
1071
                    return_value = x[2]
1072
            return return_value
1073

    
1074
        mem = chirp_common.Memory()
1075
        _mem, ndx, num, regtype, sname = self.slotloc(memref)
1076
        mem.number = num
1077

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

    
1083
        if regtype == "pms":
1084
            mem.extd_number = sname
1085
        if regtype in ["memory", "pms"]:
1086
            ndx = num - 1
1087
            mem.name = clean_name(self._memobj.names[ndx].chrs)
1088
            mem.empty = not retrieve_bit(self._memobj.enable, ndx)
1089
            mem.skip = SKIPS[retrieve_bit(self._memobj.scan, ndx)]
1090
        else:
1091
            mem.empty = False
1092
            mem.extd_number = sname
1093
            mem.immutable = ["number", "extd_number", "name", "skip"]
1094

    
1095
        # So, now if channel is not empty, we can do the evaluation of
1096
        # all parameters. Otherwise we set them to defaults.
1097

    
1098
        if mem.empty:
1099
            mem.freq = 0
1100
            mem.offset = 0
1101
            mem.duplex = "off"
1102
            mem.power = POWER_LEVELS[0]  # "High"
1103
            mem.mode = "FM"
1104
            mem.tuning_step = 0
1105
        else:
1106
            mem.freq = int(_mem.freq) * 10
1107
            txfreq = int(self._memobj.txfreqs[ndx].freq) * 10
1108
            if _mem.duplex == 5:  # auto offset
1109
                mem.duplex = DUPLEX[get_duplex(mem.freq)]
1110
            else:
1111
                mem.duplex = DUPLEX[_mem.duplex]
1112
            if _mem.duplex == 6:  # split: offset is tx frequency
1113
                mem.offset = txfreq
1114
            else:
1115
                mem.offset = int(_mem.offset) * self.freq_offset_scale
1116
            self.decode_sql(mem, _mem)
1117
            mem.power = POWER_LEVELS[2 - _mem.tx_pwr]
1118
            mem.mode = ["FM", "NFM"][_mem.tx_width]
1119
            mem.tuning_step = STEP_CODE[_mem.step]
1120
        return mem
1121

    
1122
    def enforce_band(self, memloc, freq, mem_num, sname):
1123
        """
1124
        vfo and home channels are each restricted to a particular band.
1125
        If the frequency is not in the band, use the lower bound
1126
        Raise an exception to cause UI to pop up an error message
1127
        """
1128
        first_vfo_num = self.MAX_MEM_SLOT + len(PMSNAMES) + 1
1129
        band = BAND_ASSIGNMENTS[mem_num - first_vfo_num]
1130
        frange = self.valid_bands[band]
1131
        if freq >= frange[0] and freq <= frange[1]:
1132
            memloc.freq = freq / 10
1133
            return freq
1134
        memloc.freq = frange[0] / 10
1135
        raise Exception("freq out of range for %s" % sname)
1136

    
1137
    # modify a radio channel in memobj based on info in CHIRP canonical form
1138
    def set_memory(self, mem):
1139
        _mem, ndx, num, regtype, sname = self.slotloc(mem.number)
1140
        assert(_mem)
1141
        if mem.empty:
1142
            if regtype in ["memory", "pms"]:
1143
                store_bit(self._memobj.enable, ndx, False)
1144
                return
1145

    
1146
        txfreq = mem.freq / 10     # really. RX freq is used for TX base
1147
        _mem.freq = txfreq
1148
        self.encode_sql(mem, _mem)
1149
        if mem.power:
1150
            _mem.tx_pwr = 2 - POWER_LEVELS.index(mem.power)
1151
        else:
1152
            _mem.tx_pwr = 0  # set to "High" if CHIRP canonical value is None
1153
        _mem.tx_width = mem.mode == "NFM"
1154
        _mem.step = STEP_CODE.index(mem.tuning_step)
1155

    
1156
        _mem.offset = mem.offset / self.freq_offset_scale
1157
        duplex = mem.duplex
1158
        if regtype in ["memory", "pms"]:
1159
            ndx = num - 1
1160
            store_bit(self._memobj.enable, ndx, True)
1161
            store_bit(self._memobj.scan, ndx, SKIPS.index(mem.skip))
1162
            nametrim = (mem.name + "        ")[:8]
1163
            self._memobj.names[ndx].chrs = bytearray(nametrim, "ascii")
1164
            if mem.duplex == "split":
1165
                txfreq = mem.offset / 10
1166
            self._memobj.txfreqs[num-1].freq = txfreq
1167
        _mem.duplex = DUPLEX.index(duplex)
1168
        if regtype in ["vfo", "home"]:
1169
            self.enforce_band(_mem, mem.freq, num, sname)
1170

    
1171
        return
1172

    
1173

    
1174
class YaesuFT4GenericRadio(YaesuSC35GenericRadio):
1175
    """
1176
    FT-4 sub family class. Classes for individual radios extend
1177
    these classes and are found at the end of this file.
1178
    """
1179
    class_specials = SPECIALS_FT4
1180
    Pkeys = 2     # number of programmable keys
1181
    namelen = 6   # length of the mem name display on the front-panel
1182
    freq_offset_scale = 25000
1183
    class_group_descs = YaesuSC35GenericRadio.group_descriptions
1184
    # names for the setmode function for the programmable keys. Mode zero means
1185
    # that the key is programmed for a memory not a setmode.
1186
    SETMODES = [
1187
        "mem", "apo", "ar bep", "ar int", "beclo",              # 00-04
1188
        "beep", "bell", "cw id", "cw wrt", "dc vlt",            # 05-09
1189
        "dcs cod", "dt dly", "dt set", "dtc spd", "edg.bep",    # 10-14
1190
        "lamp", "led.bsy", "led.tx", "lock", "m/t-cl",          # 15-19
1191
        "mem.del", "mem.tag", "pag.abk", "pag.cdr", "pag.cdt",  # 20-24
1192
        "pri.rvt", "pswd", "pswdwt", "rf sql", "rpt.ars",       # 25-29
1193
        "rpt.frq", "rpt.sft", "rxsave", "scn.lmp", "scn.rsm",   # 30-34
1194
        "skip", "sql.typ", "step", "tn frq", "tot",             # 35-39
1195
        "tx pwr", "tx save", "vfo.spl", "vox", "wfm.rcv",       # 40-44
1196
        "w/n.dev", "wx.alert"                                   # 45-46
1197
        ]
1198

    
1199

    
1200
class YaesuFT65GenericRadio(YaesuSC35GenericRadio):
1201
    """
1202
    FT-65 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_FT65
1206
    Pkeys = 4     # number of programmable keys
1207
    namelen = 8   # length of the mem name display on the front-panel
1208
    freq_offset_scale = 50000
1209
    # we need a deep copy here because we are adding deeper than the top level.
1210
    class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions)
1211
    add_paramdesc(
1212
        class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"]))
1213
    # names for the setmode function for the programmable keys. Mode zero means
1214
    # that the key is programmed for a memory not a setmode.
1215
    SETMODES = [
1216
        "mem", "apo", "arts", "battsave", "b-ch.l/o",              # 00-04
1217
        "beep", "bell", "compander", "ctcss", "cw id",             # 05-09
1218
        "dc volt", "dcs code", "dtmf set", "dtmf wrt", "edg bep",  # 10-14
1219
        "key lock", "lamp", "ledbsy", "mem del", "mon/t-cl",       # 15-19
1220
        "name tag", "pager", "password", "pri.rvt", "repeater",    # 20-24
1221
        "resume", "rf.sql", "scn.lamp", "skip", "sql type",        # 25-29
1222
        "step", "tot", "tx pwr", "tx save", "vfo.spl",             # 30-34
1223
        "vox", "wfm.rcv", "wide/nar", "wx alert", "scramble"       # 35-39
1224
        ]
1225

    
1226

    
1227
# Classes for each individual radio.
1228

    
1229

    
1230
@directory.register
1231
class YaesuFT4XRRadio(YaesuFT4GenericRadio):
1232
    """
1233
    FT-4X dual band, US version
1234
    """
1235
    MODEL = "FT-4XR"
1236
    id_str = b'IFT-35R\x00\x00V100\x00\x00'
1237
    valid_bands = VALID_BANDS_DUAL
1238
    DUPLEX_AUTO = DUPLEX_AUTO_US
1239
    legal_steps = US_LEGAL_STEPS
1240
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1241

    
1242

    
1243
@directory.register
1244
class YaesuFT4XERadio(YaesuFT4GenericRadio):
1245
    """
1246
    FT-4X dual band, EU version
1247
    """
1248
    MODEL = "FT-4XE"
1249
    id_str = b'IFT-35R\x00\x00V100\x00\x00'
1250
    valid_bands = VALID_BANDS_DUAL
1251
    DUPLEX_AUTO = DUPLEX_AUTO_EU
1252
    legal_steps = STEP_CODE
1253
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1254

    
1255

    
1256
@directory.register
1257
class YaesuFT4VRRadio(YaesuFT4GenericRadio):
1258
    """
1259
    FT-4V VHF, US version
1260
    """
1261
    MODEL = "FT-4VR"
1262
    id_str = b'IFT-15R\x00\x00V100\x00\x00'
1263
    valid_bands = VALID_BANDS_VHF
1264
    DUPLEX_AUTO = DUPLEX_AUTO_US
1265
    legal_steps = US_LEGAL_STEPS
1266
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1267

    
1268

    
1269
# No image available yet
1270
# @directory.register
1271
# class YaesuFT4VERadio(YaesuFT4GenericRadio):
1272
#    """
1273
#    FT-4V VHF, EU version
1274
#    """
1275
#    MODEL = "FT-4VE"
1276
#    id_str = b'IFT-15R\x00\x00V100\x00\x00'
1277
#    valid_bands = VALID_BANDS_VHF
1278
#    DUPLEX_AUTO = DUPLEX_AUTO_EU
1279
#    legal_steps = STEP_CODE
1280
#    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1281

    
1282

    
1283
@directory.register
1284
class YaesuFT65RRadio(YaesuFT65GenericRadio):
1285
    """
1286
    FT-65 dual band, US version
1287
    """
1288
    MODEL = "FT-65R"
1289
    id_str = b'IH-420\x00\x00\x00V100\x00\x00'
1290
    valid_bands = VALID_BANDS_DUAL
1291
    DUPLEX_AUTO = DUPLEX_AUTO_US
1292
    legal_steps = STEP_CODE
1293
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1294

    
1295

    
1296
@directory.register
1297
class YaesuFT65ERadio(YaesuFT65GenericRadio):
1298
    """
1299
    FT-65 dual band, EU version
1300
    """
1301
    MODEL = "FT-65E"
1302
    id_str = b'IH-420\x00\x00\x00V100\x00\x00'
1303
    valid_bands = VALID_BANDS_DUAL
1304
    DUPLEX_AUTO = DUPLEX_AUTO_EU
1305
    legal_steps = STEP_CODE
1306
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1307

    
1308

    
1309
@directory.register
1310
class YaesuFT25RRadio(YaesuFT65GenericRadio):
1311
    """
1312
    FT-25 VHF, US version
1313
    """
1314
    MODEL = "FT-25R"
1315
    id_str = b'IFT-25R\x00\x00V100\x00\x00'
1316
    valid_bands = VALID_BANDS_VHF
1317
    DUPLEX_AUTO = DUPLEX_AUTO_US
1318
    legal_steps = US_LEGAL_STEPS
1319
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1320

    
1321

    
1322
# No image available yet
1323
# @directory.register
1324
# class YaesuFT25ERadio(YaesuFT65GenericRadio):
1325
#    """
1326
#    FT-25 VHF, EU version
1327
#    """
1328
#    MODEL = "FT-25E"
1329
#    id_str = b'IFT-25R\x00\x00V100\x00\x00'
1330
#    valid_bands = VALID_BANDS_VHF
1331
#    DUPLEX_AUTO = DUPLEX_AUTO_EU
1332
#    legal_steps = STEP_CODE
1333
#    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF