Project

General

Profile

New Model #7387 » ft4.py

Yaesu FT-4X / FT-4V / FT-65 / FT-25 driver - Bernhard Hailer, 01/26/2020 04:26 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 <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-4, FT-25, FT-35, and FT-65. This driver will not work with
23
older 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
DUPLEX = ["+", "", "-", "", "off", "", "split"]  # (0,2,4,5)= (+,-,0, auto)
473
# the radio implements duplex "auto" as 5. We map to "". It is
474
# a convenience function in the radio that affects the offset, which
475
# sets the duplex value according to the frequency in use. Ham band plans
476
# prescribe + or - depending on the location in the spectrum.
477

    
478
SKIPS = ["", "S"]
479

    
480
BASETYPE_FT4 = ["FT-4XR", "FT-4XE"]
481
BASETYPE_FT65 = ["FT-65R", "FT-25R"]
482
POWER_LEVELS = [
483
    chirp_common.PowerLevel("High", watts=5.0),  # high must be first (0)
484
    chirp_common.PowerLevel("Mid", watts=2.5),
485
    chirp_common.PowerLevel("Low", watts=0.5)]
486

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

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

    
526
# Find all legal values for the tmode and cross fields for the UI.
527
# We build two dictionaries to do the lookups when encoding.
528
# The reversed range is a kludge: by happenstance, earlier duplicates
529
# in the above table are the preferred mapping, they override the
530
# later ones when we process the table backwards.
531
TONE_DICT = {}          # encode sql_type.
532
CROSS_DICT = {}         # encode sql_type.
533

    
534
for sql_type in reversed(range(0, len(RADIO_TMODES))):
535
    sql_type_row = RADIO_TMODES[sql_type]
536
    for decode_row in sql_type_row[0]:
537
        suppress = None
538
        if len(decode_row) == 3:
539
            suppress = decode_row[2]
540
        TONE_DICT[decode_row[0]] = (sql_type, suppress)
541
        if decode_row[1]:
542
            CROSS_DICT[decode_row[1]] = (sql_type, suppress)
543

    
544
# The keys are added to the "VALID" lists using code that puts those lists
545
# in the same order as the UI's default order instead of the random dict
546
# order or the arbitrary build order.
547
VALID_TONE_MODES = []   # list for UI.
548
VALID_CROSS_MODES = []  # list for UI.
549
for name in chirp_common.TONE_MODES:
550
    if name in TONE_DICT:
551
        VALID_TONE_MODES += [name]
552
for name in chirp_common.CROSS_MODES:
553
    if name in CROSS_DICT:
554
        VALID_CROSS_MODES += [name]
555

    
556

    
557
DTMF_CHARS = "0123456789ABCD*#- "
558
CW_ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ "
559
PASSWD_CHARS = "0123456789"
560
CHARSET = CW_ID_CHARS + "abcdefghijklmnopqrstuvwxyz*+-/@"
561
PMSNAMES = ["%s%02d" % (c, i) for i in range(1, 11) for c in ('L', 'U')]
562

    
563
# Four separate arrays of special channel mems.
564
# Each special has unique constrants: band, name yes/no, and pms L/U
565
# The FT-65 class replaces the "prog" entry in this list.
566
# The name field must be the name of a slot array in MEM_FORMAT
567
SPECIALS = [
568
    ("pms", PMSNAMES),
569
    ("vfo", ["VFO A UHF", "VFO A VHF", "VFO B FM", "VFO B VHF", "VFO B UHF"]),
570
    ("home", ["HOME FM", "HOME VHF", "HOME UHF"]),
571
    ("prog", ["P1", "P2"])
572
    ]
573
SPECIALS_MONO_VHF = [  # some of these are unused memory
574
    ("pms", PMSNAMES),
575
    ("vfo", ["Unused1", "VFO A VHF", "VFO B FM", "VFO B VHF", "Unused2"]),
576
    ("home", ["HOME FM", "HOME VHF", "Unused"]),
577
    ("prog", ["P1", "P2"])
578
    ]
579

    
580
FT4X_SPECIALS = list(SPECIALS)               # a shallow copy works here
581

    
582
FT4V_SPECIALS = list(SPECIALS_MONO_VHF)      # a shallow copy works here
583

    
584
FT65_SPECIALS = list(SPECIALS)               # a shallow copy works here
585
FT65_PROGS = ("prog", ["P1", "P2", "P3", "P4"])
586
FT65_SPECIALS[-1] = FT65_PROGS    # replace the last entry (P key names)
587

    
588
FT25_SPECIALS = list(SPECIALS_MONO_VHF)      # a shallow copy works here
589
FT25_PROGS = ("prog", ["P1", "P2", "P3", "P4"])
590
FT25_SPECIALS[-1] = FT25_PROGS    # replace the last entry (P key names)
591

    
592
# bands for the vfos and homes
593
BAND_ASSIGNMENTS_DUALBAND = [2, 1, 0, 1, 2, 0, 1, 2]
594
BAND_ASSIGNMENTS_MONO_VHF = [1, 1, 0, 1, 1, 0, 1, 1]
595

    
596
# names for the setmode function for the programmable keys. Mode zero means
597
# that the key is programmed for a memory not a setmode.
598
SETMODES_FT4 = [
599
    "mem", "apo", "ar bep", "ar int", "beclo",              # 00-04
600
    "beep", "bell", "cw id", "cw wrt", "dc vlt",            # 05-09
601
    "dcs cod", "dt dly", "dt set", "dtc spd", "edg.bep",    # 10-14
602
    "lamp", "led.bsy", "led.tx", "lock", "m/t-cl",          # 15-19
603
    "mem.del", "mem.tag", "pag.abk", "pag.cdr", "pag.cdt",  # 20-24
604
    "pri.rvt", "pswd", "pswdwt", "rf sql", "rpt.ars",       # 25-29
605
    "rpt.frq", "rpt.sft", "rxsave", "scn.lmp", "scn.rsm",   # 30-34
606
    "skip", "sql.typ", "step", "tn frq", "tot",             # 35-39
607
    "tx pwr", "tx save", "vfo.spl", "vox", "wfm.rcv",       # 40-44
608
    "w/n.dev", "wx.alert"                                   # 45-46
609
    ]
610
SETMODES_FT65 = [
611
    "mem", "apo", "arts", "battsave", "b-ch.l/o",              # 00-04
612
    "beep", "bell", "compander", "ctcss", "cw id",             # 05-09
613
    "dc volt", "dcs code", "dtmf set", "dtmf wrt", "edg bep",  # 10-14
614
    "key lock", "lamp", "ledbsy", "mem del", "mon/t-cl",       # 15-19
615
    "name tag", "pager", "password", "pri.rvt", "repeater",    # 20-24
616
    "resume", "rf.sql", "scn.lamp", "skip", "sql type",        # 25-29
617
    "step", "tot", "tx pwr", "tx save", "vfo.spl",             # 30-34
618
    "vox", "wfm.rcv", "wide/nar", "wx alert", "scramble"       # 35-39
619
    ]
620

    
621

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

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

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

    
654

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

    
665

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

    
679
    @classmethod
680
    def get_prompts(cls):
681
        rp = chirp_common.RadioPrompts()
682
        rp.experimental = (
683
            'Tested only by the developer and only on a single radio.\n'
684
            'Proceed at your own risk!'
685
            )
686

    
687
        rp.pre_download = "".join([
688
            "1. Connect programming cable to MIC jack.\n",
689
            "2. Press OK."
690
            ]
691
            )
692
        rp.pre_upload = rp.pre_download
693
        return rp
694

    
695
    # identify the features that can be manipulated on this radio.
696
    # mentioned here only when different from defaults in chirp_common.py
697
    def get_features(self):
698

    
699
        rf = chirp_common.RadioFeatures()
700
        specials = [name for s in self.class_specials for name in s[1]]
701
        rf.valid_special_chans = specials
702
        rf.memory_bounds = (1, self.MAX_MEM_SLOT)
703
        rf.valid_duplexes = DUPLEX
704
        rf.valid_tmodes = VALID_TONE_MODES
705
        rf.valid_cross_modes = VALID_CROSS_MODES
706
        rf.valid_power_levels = POWER_LEVELS
707
        rf.valid_tuning_steps = self.legal_steps
708
        rf.valid_skips = SKIPS
709
        rf.valid_characters = CHARSET
710
        rf.valid_name_length = self.namelen
711
        rf.valid_modes = ["FM", "NFM"]
712
        rf.valid_bands = self.valid_bands
713
        rf.can_odd_split = True
714
        rf.has_ctone = True
715
        rf.has_rx_dtcs = True
716
        rf.has_dtcs_polarity = False    # REV TN reverses the tone, not the dcs
717
        rf.has_cross = True
718
        rf.has_settings = True
719
        return rf
720

    
721
    def get_bank_model(self):
722
        return YaesuSC35GenericBankModel(self)
723

    
724
    # read and parse the radio memory
725
    def sync_in(self):
726
        try:
727
            self._mmap = do_download(self)
728
        except Exception as e:
729
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
730
        self.process_mmap()
731

    
732
    # write the memory image to the radio
733
    def sync_out(self):
734
        try:
735
            do_upload(self)
736
        except Exception as e:
737
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
738

    
739
    def process_mmap(self):
740
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
741

    
742
    # There are about 40 settings and most are handled generically in
743
    # get_settings. get_settings invokes these handlers for the few
744
    # that are more complicated.
745

    
746
    # callback for setting  byte arrays: DTMF[0-9], passwd, and CW_ID
747
    def apply_str_to_bytearray(self, element, obj):
748
        lng = len(obj)
749
        strng = (element.value.get_value() + "                ")[:lng]
750
        bytes = bytearray(strng, "ascii")
751
        for x in range(0, lng):    # memobj cannot iterate, so byte-by-byte
752
            obj[x] = bytes[x]
753
        return
754

    
755
    # add a string value to the RadioSettings
756
    def get_string_setting(self, obj, valid_chars, desc1, desc2, group):
757
        content = ''
758
        maxlen = len(obj)
759
        for x in range(0, maxlen):
760
            content += chr(obj[x])
761
        val = RadioSettingValueString(0, maxlen, content, True, valid_chars)
762
        rs = RadioSetting(desc1, desc2, val)
763
        rs.set_apply_callback(self.apply_str_to_bytearray, obj)
764
        group.append(rs)
765

    
766
    # called when found in the group_descriptions table to handle string value
767
    def get_strset(self, group, parm):
768
        #   parm =(paramname, paramtitle,(handler,[handler params])).
769
        objname, title, fparms = parm
770
        myparms = fparms[1]
771
        obj = getattr(self._memobj.settings,  objname)
772
        self.get_string_setting(obj, myparms[0], objname, title, group)
773

    
774
    # called when found in the group_descriptions table for DTMF strings
775
    def get_dtmfs(self, group, parm):
776
        objname, title, fparms = parm
777
        for i in range(1, 10):
778
            dtmf_digits = self._memobj.dtmf[i - 1].digit
779
            self.get_string_setting(
780
                dtmf_digits, DTMF_CHARS,
781
                "dtmf_%i" % i, "DTMF Autodialer Memory %i" % i, group)
782

    
783
    def apply_P(self, element, pnum, memobj):
784
        memobj.progctl[pnum].usage = element.value
785

    
786
    def apply_Pmode(self, element, pnum, memobj):
787
        memobj.progctl[pnum].parm = element.value
788

    
789
    def apply_Psubmode(self, element, pnum, memobj):
790
        memobj.progctl[pnum].submode = element.value
791

    
792
    def apply_Pmem(self, element, pnum, memobj):
793
        memobj.pmemndx[pnum] = element.value
794

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

    
798
    # called when found in the group_descriptions table
799
    # returns the settings for the programmable keys (P1-P4)
800
    def get_progs(self, group, parm):
801
        _progctl = self._memobj.progctl
802
        _progndx = self._memobj.pmemndx
803

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

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

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

    
920
    # modify settings values in the radio memory image
921
    def set_settings(self, uisettings):
922
        _settings = self._memobj.settings
923
        for element in uisettings:
924
            if not isinstance(element, RadioSetting):
925
                self.set_settings(element)
926
                continue
927
            if not element.changed():
928
                continue
929

    
930
            try:
931
                name = element.get_name()
932
                value = element.value
933

    
934
                if element.has_apply_callback():
935
                    LOG.debug("Using apply callback")
936
                    element.run_apply_callback()
937
                else:
938
                    setattr(_settings, name, value)
939

    
940
                LOG.debug("Setting %s: %s" % (name, value))
941
            except:
942
                LOG.debug(element.get_name())
943
                raise
944

    
945
    # maps a boolean pair (tx==0,rx==0) to the numbers 0-3
946
    LOOKUP = [[True, True], [True, False], [False, True], [False, False]]
947

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

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

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

    
1052
    # return the raw info for a memory channel
1053
    def get_raw_memory(self, memref):
1054
        memloc, ndx, num, regtype, sname = self.slotloc(memref)
1055
        if regtype == "memory":
1056
            return repr(memloc)
1057
        else:
1058
            return repr(memloc) + repr(self._memobj.names[ndx])
1059

    
1060
    # return the info for a memory channel In CHIRP canonical form
1061
    def get_memory(self, memref):
1062

    
1063
        def clean_name(obj):     # helper func to tidy up the name
1064
            name = ''
1065
            for x in range(0, self.namelen):
1066
                y = obj[x]
1067
                if y == 0:
1068
                    break
1069
                if y == 0x7F:    # when programmed from VFO
1070
                    y = 0x20
1071
                name += chr(y)
1072
            return name.rstrip()
1073

    
1074
        def get_duplex(freq):     # auto duplex to real duplex
1075
            return_value = 4      # off
1076
            # 2M frequencies (according to ARRL)
1077
            if freq in range(145200000, 145500000):
1078
                return_value = 2  # negative
1079
            if freq in range(146610000, 146970000):
1080
                return_value = 2  # negative
1081
            if freq in range(147600000, 147990000):
1082
                return_value = 0  # positive
1083
            # ARRL band plan for 70cm does define repeater
1084
            # frequency ranges, but it allows local entities
1085
            # to make them input or output channels.
1086
            return return_value
1087

    
1088
        mem = chirp_common.Memory()
1089
        _mem, ndx, num, regtype, sname = self.slotloc(memref)
1090
        mem.number = num
1091

    
1092
        """
1093
        First, we need to know whether a channel is enabled,
1094
        then we can process any channel parameters.
1095
        It was found (at least on an FT-25) that channels might be
1096
        uninitialized and memory is just completely filled with 0xFF.
1097
        """
1098
        if regtype == "pms":
1099
            mem.extd_number = sname
1100

    
1101
        if regtype in ["memory", "pms"]:
1102
            ndx = num - 1
1103
            mem.name = clean_name(self._memobj.names[ndx].chrs)
1104
            mem.empty = not retrieve_bit(self._memobj.enable, ndx)
1105
            mem.skip = SKIPS[retrieve_bit(self._memobj.scan, ndx)]
1106
        else:
1107
            mem.empty = False
1108
            mem.extd_number = sname
1109
            mem.immutable = ["number", "extd_number", "name", "skip"]
1110

    
1111
        """
1112
        So, now if channel is not empty, we can do the evaluation of
1113
        all parameters. Otherwise we set them to defaults.
1114
        """
1115
        if mem.empty:
1116
            mem.freq = 0
1117
            mem.offset = 0
1118
            mem.duplex = "off"
1119
            mem.power = POWER_LEVELS[0]  # "High"
1120
            mem.mode = "FM"
1121
            mem.tuning_step = 0
1122
        else:
1123
            mem.freq = int(_mem.freq) * 10
1124
            txfreq = int(self._memobj.txfreqs[ndx].freq) * 10
1125
            if (txfreq != 0) and (txfreq != mem.freq):
1126
                mem.duplex = "split"
1127
                mem.offset = txfreq
1128
            else:
1129
                mem.offset = int(_mem.offset) * self.freq_offset_scale
1130
                if _mem.duplex == 5:  # auto offset
1131
                    mem.duplex = DUPLEX[get_duplex(mem.freq)]
1132
                else:
1133
                    mem.duplex = DUPLEX[_mem.duplex]
1134
            self.decode_sql(mem, _mem)
1135
            mem.power = POWER_LEVELS[2 - _mem.tx_pwr]
1136
            mem.mode = ["FM", "NFM"][_mem.tx_width]
1137
            mem.tuning_step = STEP_CODE[_mem.step]
1138
        return mem
1139

    
1140
    def enforce_band(self, memloc, freq, mem_num, sname):
1141
        """
1142
        vfo and home channels are each restricted to a particular band.
1143
        If the frequency is not in the band, use the lower bound
1144
        Raise an exception to cause UI to pop up an error message
1145
        """
1146
        first_vfo_num = self.MAX_MEM_SLOT + len(PMSNAMES) + 1
1147
        band = BAND_ASSIGNMENTS[mem_num - first_vfo_num]
1148
        frange = self.valid_bands[band]
1149
        if freq >= frange[0] and freq <= frange[1]:
1150
            memloc.freq = freq / 10
1151
            return freq
1152
        memloc.freq = frange[0] / 10
1153
        raise Exception("freq out of range for %s" % sname)
1154

    
1155
    # modify a radio channel in memobj based on info in CHIRP canonical form
1156
    def set_memory(self, mem):
1157
        _mem, ndx, num, regtype, sname = self.slotloc(mem.number)
1158
        assert(_mem)
1159
        if mem.empty:
1160
            if regtype in ["memory", "pms"]:
1161
                store_bit(self._memobj.enable, ndx, False)
1162
                return
1163

    
1164
        txfreq = mem.freq / 10     # really. RX freq is used for TX base
1165
        _mem.freq = txfreq
1166
        self.encode_sql(mem, _mem)
1167
        if mem.power:
1168
            _mem.tx_pwr = 2 - POWER_LEVELS.index(mem.power)
1169
        else:
1170
            _mem.tx_pwr = 0  # set to "High" if CHIRP canonical value is None
1171
        _mem.tx_width = mem.mode == "NFM"
1172
        _mem.step = STEP_CODE.index(mem.tuning_step)
1173

    
1174
        _mem.offset = mem.offset / self.freq_offset_scale
1175
        duplex = mem.duplex
1176
        if regtype in ["memory", "pms"]:
1177
            ndx = num - 1
1178
            store_bit(self._memobj.enable, ndx, True)
1179
            store_bit(self._memobj.scan, ndx, SKIPS.index(mem.skip))
1180
            nametrim = (mem.name + "        ")[:8]
1181
            self._memobj.names[ndx].chrs = bytearray(nametrim, "ascii")
1182
            if mem.duplex == "split":
1183
                txfreq = mem.offset / 10
1184
                duplex = "off"    # radio ignores when tx != rx
1185
            self._memobj.txfreqs[num-1].freq = txfreq
1186
        _mem.duplex = DUPLEX.index(duplex)
1187
        if regtype in ["vfo", "home"]:
1188
            self.enforce_band(_mem, mem.freq, num, sname)
1189

    
1190
        return
1191

    
1192

    
1193
@directory.register
1194
class YaesuFT4Radio(YaesuSC35GenericRadio):
1195
    MODEL = "FT-4XR"
1196
    _basetype = BASETYPE_FT4
1197
    valid_bands = [
1198
        (65000000, 108000000),     # broadcast FM, receive only
1199
        (144000000, 148000000),    # VHF, US version, TX and RX
1200
        (430000000, 450000000)     # UHF, US version, TX and RX
1201
                                   # VHF, RX (136000000, 174000000)
1202
                                   # UHF, RX (400000000, 480000000)
1203
        ]
1204
    _valid_chars = chirp_common.CHARSET_ASCII
1205
    numblocks = 0x215           # number of 16-byte blocks in the radio
1206
    _memsize = 16 * numblocks   # used by CHIRP file loader to guess radio type
1207
    MAX_MEM_SLOT = 200
1208
    Pkeys = 2     # number of programmable keys on the FT-4
1209
    namelen = 6   # length of the mem name display on the FT-4 front-panel
1210
    id_str = b'IFT-35R\x00\x00V100\x00\x00'
1211
    freq_offset_scale = 25000
1212
    legal_steps = US_LEGAL_STEPS
1213
    class_group_descs = YaesuSC35GenericRadio.group_descriptions
1214
    class_specials = FT4X_SPECIALS
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 = SETMODES_FT4
1218
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1219

    
1220

    
1221
@directory.register
1222
class YaesuFT4Radio(YaesuSC35GenericRadio):
1223
    MODEL = "FT-4VR"
1224
    _basetype = BASETYPE_FT4
1225
    valid_bands = [
1226
        (65000000, 108000000),     # broadcast FM, receive only
1227
        (144000000, 148000000),    # VHF, US version, TX and RX
1228
                                   # VHF, RX (136000000, 174000000)
1229
        ]
1230
    _valid_chars = chirp_common.CHARSET_ASCII
1231
    numblocks = 0x215           # number of 16-byte blocks in the radio
1232
    _memsize = 16 * numblocks   # used by CHIRP file loader to guess radio type
1233
    MAX_MEM_SLOT = 200
1234
    Pkeys = 2     # number of programmable keys on the FT-4V
1235
    namelen = 6   # length of the mem name display on the FT-4V front-panel
1236
    id_str = b'IFT-15R\x00\x00V100\x00\x00'
1237
    freq_offset_scale = 25000
1238
    legal_steps = US_LEGAL_STEPS
1239
    class_group_descs = YaesuSC35GenericRadio.group_descriptions
1240
    class_specials = FT4V_SPECIALS
1241
    # names for the setmode function for the programmable keys. Mode zero means
1242
    # that the key is programmed for a memory not a setmode.
1243
    SETMODES = SETMODES_FT4
1244
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
1245

    
1246

    
1247
@directory.register
1248
class YaesuFT65Radio(YaesuSC35GenericRadio):
1249
    MODEL = "FT-65R"
1250
    _basetype = BASETYPE_FT65
1251
    valid_bands = [
1252
        (65000000, 108000000),     # broadcast FM, receive only
1253
        (144000000, 148000000),    # VHF, US version, TX and RX
1254
        (430000000, 450000000)     # UHF, US version, TX and RX
1255
                                   # VHF, RX (136000000, 174000000)
1256
                                   # UHF, RX (400000000, 480000000)
1257
        ]
1258
    _valid_chars = chirp_common.CHARSET_ASCII
1259
    numblocks = 0x215      # number of 16-byte blocks in the radio
1260
    _memsize = 16 * numblocks   # used by CHIRP file loader to guess radio type
1261
    MAX_MEM_SLOT = 200
1262
    Pkeys = 4     # number of programmable keys on the FT-65
1263
    namelen = 8   # length of the mem name display on the FT-65 front panel
1264
    id_str = b'IH-420\x00\x00\x00V100\x00\x00'
1265
    freq_offset_scale = 50000
1266
    legal_steps = US_LEGAL_STEPS
1267
    # we need a deep copy here because we are adding deeper than the top level.
1268
    class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions)
1269
    add_paramdesc(
1270
        class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"]))
1271
    class_specials = FT65_SPECIALS
1272
    # names for the setmode function for the programmable keys. Mode zero means
1273
    # that the key is programmed for a memory not a setmode.
1274
    SETMODES = SETMODES_FT65
1275
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_DUALBAND
1276

    
1277

    
1278
@directory.register
1279
class YaesuFT25Radio(YaesuSC35GenericRadio):
1280
    MODEL = "FT-25R"
1281
    _basetype = BASETYPE_FT65
1282
    valid_bands = [
1283
        (65000000, 108000000),     # broadcast FM, receive only
1284
        (144000000, 148000000),    # VHF, US version, TX and RX
1285
                                   # VHF, RX (136000000, 174000000)
1286
        ]
1287
    _valid_chars = chirp_common.CHARSET_ASCII
1288
    numblocks = 0x215      # number of 16-byte blocks in the radio
1289
    _memsize = 16 * numblocks   # used by CHIRP file loader to guess radio type
1290
    MAX_MEM_SLOT = 200
1291
    Pkeys = 4     # number of programmable keys on the FT-65
1292
    namelen = 8   # length of the mem name display on the FT-65 front panel
1293
    id_str = b'IFT-25R\x00\x00V100\x00\x00'
1294
    freq_offset_scale = 50000
1295
    legal_steps = US_LEGAL_STEPS
1296
    # we need a deep copy here because we are adding deeper than the top level.
1297
    class_group_descs = copy.deepcopy(YaesuSC35GenericRadio.group_descriptions)
1298
    add_paramdesc(
1299
        class_group_descs, "misc", ("compander", "Compander", ["OFF", "ON"]))
1300
    class_specials = FT25_SPECIALS
1301
    # names for the setmode function for the programmable keys. Mode zero means
1302
    # that the key is programmed for a memory not a setmode.
1303
    SETMODES = SETMODES_FT65
1304
    BAND_ASSIGNMENTS = BAND_ASSIGNMENTS_MONO_VHF
(3-3/4)