Project

General

Profile

Bug #10286 » ft4.py

61ff219d - Dan Smith, 01/20/2023 10:38 PM

 
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(b'\x00')]
313
        if id_response[:id_response.find(b'\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:\n%s" % util.hexprint(cmd)
336
        msg += "\nReceived:\n%s" % util.hexprint(response)
337
        LOG.debug(msg)
338
        return False
339
    if checkSum8(response[1:20]) != bytearray(response)[20]:
340
        LOG.debug("Bad checksum:\n%s" % util.hexprint(response))
341
        LOG.debug('%r != %r' % (checkSum8(response[1:20]),
342
                                bytearray(response)[20]))
343
        return False
344
    image[addr:addr+16] = response[4:20]
345
    return True
346

    
347

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

    
371

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

    
381

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

    
400

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

    
407

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

    
419

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

    
427

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

    
432
    def get_num_mappings(self):
433
        return 10
434

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

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

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

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

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

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

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

    
492
SKIPS = ["S", ""]
493

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

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

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

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

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

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

    
568

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

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

    
589

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

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

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

    
605

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

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

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

    
638

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

    
649

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

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

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

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

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

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

    
710
        return rf
711

    
712
    def get_bank_model(self):
713
        return YaesuSC35GenericBankModel(self)
714

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

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

    
730
    def process_mmap(self):
731
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
732

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
921
            try:
922
                name = element.get_name()
923
                value = element.value
924

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1173
        return
1174

    
1175

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

    
1201

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

    
1228

    
1229
# Classes for each individual radio.
1230

    
1231

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

    
1244

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

    
1257

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

    
1270

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

    
1284

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

    
1297

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

    
1311

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

    
1324

    
1325
@directory.register
1326
class YaesuFT25ERadio(YaesuFT65GenericRadio):
1327
    """
1328
    FT-25 VHF, EU version
1329
    """
1330
    MODEL = "FT-25E"
1331
    id_str = b'IFT-25R\x00\x00V100\x00\x03'
1332
    valid_bands = VALID_BANDS_VHF
1333
    DUPLEX_AUTO = DUPLEX_AUTO_EU
1334
    legal_steps = STEP_CODE
1335
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1336
    freq_offset_scale = 25000
(6-6/15)