Project

General

Profile

Bug #2501 » ft2900eu.py

Driver for European FT-2900 models - Mikael Grön, 04/15/2017 05:07 AM

 
1
# Copyright 2011 Dan Smith <dsmith@danplanet.com>
2
#
3
# FT-2900-specific modifications by Richard Cochran, <ag6qr@sonic.net>
4
# Initial work on settings by Chris Fosnight, <chris.fosnight@gmail.com>
5
#
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18

    
19
import time
20
import os
21
import logging
22

    
23
from chirp import util, memmap, chirp_common, bitwise, directory, errors
24
from chirp.drivers.yaesu_clone import YaesuCloneModeRadio
25
from chirp.settings import RadioSetting, RadioSettingGroup, \
26
    RadioSettingValueList, RadioSettingValueString, RadioSettings
27

    
28
from textwrap import dedent
29

    
30
LOG = logging.getLogger(__name__)
31

    
32

    
33
def _send(s, data):
34
    s.write(data)
35
    echo = s.read(len(data))
36
    if data != echo:
37
        raise Exception("Failed to read echo")
38
    LOG.debug("got echo\n%s\n" % util.hexprint(echo))
39

    
40
ACK = "\x06"
41
INITIAL_CHECKSUM = 0
42

    
43

    
44
def _download(radio):
45

    
46
    blankChunk = ""
47
    for _i in range(0, 32):
48
        blankChunk += "\xff"
49

    
50
    LOG.debug("in _download\n")
51

    
52
    data = ""
53
    for _i in range(0, 20):
54
        data = radio.pipe.read(20)
55
        LOG.debug("Header:\n%s" % util.hexprint(data))
56
        LOG.debug("len(header) = %s\n" % len(data))
57
	LOG.debug("IDBLOCK:\n%s" % util.hexprint(radio.IDBLOCK))
58
        LOG.debug("len(IDBLOCK) = %s\n" % len(radio.IDBLOCK))
59
	LOG.debug("----------------------------------------\n")
60
        if data == radio.IDBLOCK:
61
            break
62

    
63
    if data != radio.IDBLOCK:
64
        raise Exception("Failed to read header")
65

    
66
    _send(radio.pipe, ACK)
67

    
68
    # initialize data, the big var that holds all memory
69
    data = ""
70

    
71
    _blockNum = 0
72

    
73
    while len(data) < radio._block_sizes[1]:
74
        _blockNum += 1
75
        time.sleep(0.03)
76
        chunk = radio.pipe.read(32)
77
        LOG.debug("Block %i " % (_blockNum))
78
        if chunk == blankChunk:
79
            LOG.debug("blank chunk\n")
80
        else:
81
            LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
82
        if len(chunk) != 32:
83
            LOG.debug("len chunk is %i\n" % (len(chunk)))
84
            raise Exception("Failed to get full data block")
85
            break
86
        else:
87
            data += chunk
88

    
89
        if radio.status_fn:
90
            status = chirp_common.Status()
91
            status.max = radio._block_sizes[1]
92
            status.cur = len(data)
93
            status.msg = "Cloning from radio"
94
            radio.status_fn(status)
95

    
96
    LOG.debug("Total: %i" % len(data))
97

    
98
    # radio should send us one final termination byte, containing
99
    # checksum
100
    chunk = radio.pipe.read(32)
101
    if len(chunk) != 1:
102
        LOG.debug("len(chunk) is %i\n" % len(chunk))
103
        raise Exception("radio sent extra unknown data")
104
    LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
105

    
106
    # compute checksum
107
    cs = INITIAL_CHECKSUM
108
    for byte in radio.IDBLOCK:
109
        cs += ord(byte)
110
    for byte in data:
111
        cs += ord(byte)
112
    LOG.debug("calculated checksum is %x\n" % (cs & 0xff))
113
    LOG.debug("Radio sent checksum is %x\n" % ord(chunk[0]))
114

    
115
    if (cs & 0xff) != ord(chunk[0]):
116
        raise Exception("Failed checksum on read.")
117

    
118
    # for debugging purposes, dump the channels, in hex.
119
    for _i in range(0, 200):
120
        _startData = 1892 + 20 * _i
121
        chunk = data[_startData:_startData + 20]
122
        LOG.debug("channel %i:\n%s" % (_i, util.hexprint(chunk)))
123

    
124
    return memmap.MemoryMap(data)
125

    
126

    
127
def _upload(radio):
128
    for _i in range(0, 10):
129
        data = radio.pipe.read(256)
130
        if not data:
131
            break
132
        LOG.debug("What is this garbage?\n%s" % util.hexprint(data))
133
        raise Exception("Radio sent unrecognized data")
134

    
135
    _send(radio.pipe, radio.IDBLOCK)
136
    time.sleep(.2)
137
    ack = radio.pipe.read(300)
138
    LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack)))
139
    if ack != ACK:
140
        raise Exception("Radio did not ack ID. Check cable, verify"
141
                        " radio is not locked.\n"
142
                        " (press & Hold red \"*L\" button to unlock"
143
                        " radio if needed)")
144

    
145
    block = 0
146
    cs = INITIAL_CHECKSUM
147
    for byte in radio.IDBLOCK:
148
        cs += ord(byte)
149

    
150
    while block < (radio.get_memsize() / 32):
151
        data = radio.get_mmap()[block * 32:(block + 1) * 32]
152

    
153
        LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data)))
154

    
155
        _send(radio.pipe, data)
156
        time.sleep(0.03)
157

    
158
        for byte in data:
159
            cs += ord(byte)
160

    
161
        if radio.status_fn:
162
            status = chirp_common.Status()
163
            status.max = radio._block_sizes[1]
164
            status.cur = block * 32
165
            status.msg = "Cloning to radio"
166
            radio.status_fn(status)
167
        block += 1
168

    
169
    _send(radio.pipe, chr(cs & 0xFF))
170

    
171
MEM_FORMAT = """
172
#seekto 0x0080;
173
struct {
174
    u8  apo;
175
    u8  arts_beep;
176
    u8  bell;
177
    u8  dimmer;
178
    u8  cw_id_string[16];
179
    u8  cw_trng;
180
    u8  x95;
181
    u8  x96;
182
    u8  x97;
183
    u8  int_cd;
184
    u8  int_set;
185
    u8  x9A;
186
    u8  x9B;
187
    u8  lock;
188
    u8  x9D;
189
    u8  mic_gain;
190
    u8  open_msg;
191
    u8  openMsg_Text[6];
192
    u8  rf_sql;
193
    u8  unk:6,
194
        pag_abk:1,
195
        unk:1;
196
    u8  pag_cdr_1;
197
    u8  pag_cdr_2;
198
    u8  pag_cdt_1;
199
    u8  pag_cdt_2;
200
    u8  prog_p1;
201
    u8  xAD;
202
    u8  prog_p2;
203
    u8  xAF;
204
    u8  prog_p3;
205
    u8  xB1;
206
    u8  prog_p4;
207
    u8  xB3;
208
    u8  resume;
209
    u8  tot;
210
    u8  unk:1,
211
        cw_id:1,
212
        unk:1,
213
        ts_speed:1,
214
        ars:1,
215
        unk:2,
216
        dtmf_mode:1;
217
    u8  unk:1,
218
        ts_mut:1
219
        wires_auto:1,
220
        busy_lockout:1,
221
        edge_beep:1,
222
        unk:3;
223
    u8  unk:2,
224
        s_search:1,
225
        unk:2,
226
        cw_trng_units:1,
227
        unk:2;
228
    u8  dtmf_speed:1,
229
        unk:2,
230
        arts_interval:1,
231
        unk:1,
232
        inverted_dcs:1,
233
        unk:1,
234
        mw_mode:1;
235
    u8  unk:2,
236
        wires_mode:1,
237
        wx_alert:1,
238
        unk:1,
239
        wx_vol_max:1,
240
        revert:1,
241
        unk:1;
242
    u8  vfo_scan;
243
    u8  scan_mode;
244
    u8  dtmf_delay;
245
    u8  beep;
246
    u8  xBF;
247
} settings;
248

    
249
#seekto 0x00d0;
250
    u8  passwd[4];
251
    u8  mbs;
252

    
253
#seekto 0x00c0;
254
struct {
255
  u16 in_use;
256
} bank_used[8];
257

    
258
#seekto 0x00ef;
259
  u8 currentTone;
260

    
261
#seekto 0x00f0;
262
  u8 curChannelMem[20];
263

    
264
#seekto 0x1e0;
265
struct {
266
  u8 dtmf_string[16];
267
} dtmf_strings[10];
268

    
269
#seekto 0x0127;
270
  u8 curChannelNum;
271

    
272
#seekto 0x012a;
273
  u8 banksoff1;
274

    
275
#seekto 0x15f;
276
  u8 checksum1;
277

    
278
#seekto 0x16f;
279
  u8 curentTone2;
280

    
281
#seekto 0x1aa;
282
  u16 banksoff2;
283

    
284
#seekto 0x1df;
285
  u8 checksum2;
286

    
287
#seekto 0x0360;
288
struct{
289
  u8 name[6];
290
} bank_names[8];
291

    
292

    
293
#seekto 0x03c4;
294
struct{
295
  u16 channels[50];
296
} banks[8];
297

    
298
#seekto 0x06e4;
299
struct {
300
  u8 even_pskip:1,
301
     even_skip:1,
302
     even_valid:1,
303
     even_masked:1,
304
     odd_pskip:1,
305
     odd_skip:1,
306
     odd_valid:1,
307
     odd_masked:1;
308
} flags[225];
309

    
310
#seekto 0x0764;
311
struct {
312
  u8 unknown0:2,
313
     isnarrow:1,
314
     unknown1:5;
315
  u8 unknown2:2,
316
     duplex:2,
317
     unknown3:1,
318
     step:3;
319
  bbcd freq[3];
320
  u8 power:2,
321
     unknown4:3,
322
     tmode:3;
323
  u8 name[6];
324
  bbcd offset[3];
325
  u8 ctonesplitflag:1,
326
     ctone:7;
327
  u8 rx_dtcssplitflag:1,
328
     rx_dtcs:7;
329
  u8 unknown5;
330
  u8 rtonesplitflag:1,
331
     rtone:7;
332
  u8 dtcssplitflag:1,
333
     dtcs:7;
334
} memory[200];
335

    
336
"""
337

    
338
MODES = ["FM", "NFM"]
339
TMODES = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"]
340
CROSS_MODES = ["DTCS->", "Tone->DTCS", "DTCS->Tone",
341
               "Tone->Tone", "DTCS->DTCS"]
342
DUPLEX = ["", "-", "+", "split"]
343
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=75),
344
                chirp_common.PowerLevel("Low3", watts=30),
345
                chirp_common.PowerLevel("Low2", watts=10),
346
                chirp_common.PowerLevel("Low1", watts=5),
347
                ]
348

    
349
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ +-/?C[] _"
350
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0, 100.0]
351

    
352

    
353
def _decode_tone(radiotone):
354
    try:
355
        chirptone = chirp_common.TONES[radiotone]
356
    except IndexError:
357
        chirptone = 100
358
        LOG.debug("found invalid radio tone: %i\n" % radiotone)
359
    return chirptone
360

    
361

    
362
def _decode_dtcs(radiodtcs):
363
    try:
364
        chirpdtcs = chirp_common.DTCS_CODES[radiodtcs]
365
    except IndexError:
366
        chirpdtcs = 23
367
        LOG.debug("found invalid radio dtcs code: %i\n" % radiodtcs)
368
    return chirpdtcs
369

    
370

    
371
def _decode_name(mem):
372
    name = ""
373
    for i in mem:
374
        if (i & 0x7F) == 0x7F:
375
            break
376
        try:
377
            name += CHARSET[i & 0x7F]
378
        except IndexError:
379
            LOG.debug("Unknown char index: %x " % (i))
380
    name = name.strip()
381
    return name
382

    
383

    
384
def _encode_name(mem):
385
    if(mem.strip() == ""):
386
        return [0xff] * 6
387

    
388
    name = [None] * 6
389
    for i in range(0, 6):
390
        try:
391
            name[i] = CHARSET.index(mem[i])
392
        except IndexError:
393
            name[i] = CHARSET.index(" ")
394

    
395
    name[0] = name[0] | 0x80
396
    return name
397

    
398

    
399
def _wipe_memory(mem):
400
    mem.set_raw("\xff" * (mem.size() / 8))
401

    
402

    
403
class FT2900Bank(chirp_common.NamedBank):
404

    
405
    def get_name(self):
406
        _bank = self._model._radio._memobj.bank_names[self.index]
407
        name = ""
408
        for i in _bank.name:
409
            if i == 0xff:
410
                break
411
            name += CHARSET[i & 0x7f]
412

    
413
        return name.rstrip()
414

    
415
    def set_name(self, name):
416
        name = name.upper().ljust(6)[:6]
417
        _bank = self._model._radio._memobj.bank_names[self.index]
418
        _bank.name = [CHARSET.index(x) for x in name.ljust(6)[:6]]
419

    
420

    
421
class FT2900BankModel(chirp_common.BankModel):
422

    
423
    def get_num_mappings(self):
424
        return 8
425

    
426
    def get_mappings(self):
427
        banks = self._radio._memobj.banks
428
        bank_mappings = []
429
        for index, _bank in enumerate(banks):
430
            bank = FT2900Bank(self, "%i" % index, "b%i" % (index + 1))
431
            bank.index = index
432
            bank_mappings.append(bank)
433

    
434
        return bank_mappings
435

    
436
    def _get_channel_numbers_in_bank(self, bank):
437
        _bank_used = self._radio._memobj.bank_used[bank.index]
438
        if _bank_used.in_use == 0xffff:
439
            return set()
440

    
441
        _members = self._radio._memobj.banks[bank.index]
442
        return set([int(ch) for ch in _members.channels if ch != 0xffff])
443

    
444
    def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
445
        _members = self._radio._memobj.banks[bank.index]
446
        if len(channels_in_bank) > len(_members.channels):
447
            raise Exception("More than %i entries in bank %d" %
448
                            (len(_members.channels), bank.index))
449

    
450
        empty = 0
451
        for index, channel_number in enumerate(sorted(channels_in_bank)):
452
            _members.channels[index] = channel_number
453
            empty = index + 1
454
        for index in range(empty, len(_members.channels)):
455
            _members.channels[index] = 0xffff
456

    
457
        _bank_used = self._radio._memobj.bank_used[bank.index]
458
        if empty == 0:
459
            _bank_used.in_use = 0xffff
460
        else:
461
            _bank_used.in_use = empty - 1
462

    
463
    def add_memory_to_mapping(self, memory, bank):
464
        channels_in_bank = self._get_channel_numbers_in_bank(bank)
465
        channels_in_bank.add(memory.number)
466
        self._update_bank_with_channel_numbers(bank, channels_in_bank)
467

    
468
        # tells radio that banks are active
469
        self._radio._memobj.banksoff1 = bank.index
470
        self._radio._memobj.banksoff2 = bank.index
471

    
472
    def remove_memory_from_mapping(self, memory, bank):
473
        channels_in_bank = self._get_channel_numbers_in_bank(bank)
474
        try:
475
            channels_in_bank.remove(memory.number)
476
        except KeyError:
477
            raise Exception("Memory %i is not in bank %s. Cannot remove" %
478
                            (memory.number, bank))
479
        self._update_bank_with_channel_numbers(bank, channels_in_bank)
480

    
481
    def get_mapping_memories(self, bank):
482
        memories = []
483
        for channel in self._get_channel_numbers_in_bank(bank):
484
            memories.append(self._radio.get_memory(channel))
485

    
486
        return memories
487

    
488
    def get_memory_mappings(self, memory):
489
        banks = []
490
        for bank in self.get_mappings():
491
            if memory.number in self._get_channel_numbers_in_bank(bank):
492
                banks.append(bank)
493

    
494
        return banks
495

    
496

    
497
@directory.register
498
class FT2900Radio(YaesuCloneModeRadio):
499

    
500
    """Yaesu FT-2900"""
501
    VENDOR = "Yaesu"
502
    MODEL = "FT-2900 EU"
503
    IDBLOCK = "\x56\x43\x32\x33\x01\x02\x41\x02\x01\x01"
504
    BAUD_RATE = 19200
505

    
506
    _memsize = 8000
507
    _block_sizes = [8, 8000]
508

    
509
    def get_features(self):
510
        rf = chirp_common.RadioFeatures()
511

    
512
        rf.memory_bounds = (0, 199)
513

    
514
        rf.can_odd_split = True
515
        rf.has_ctone = True
516
        rf.has_rx_dtcs = True
517
        rf.has_cross = True
518
        rf.has_dtcs_polarity = False
519
        rf.has_bank = True
520
        rf.has_bank_names = True
521
        rf.has_settings = True
522

    
523
        rf.valid_tuning_steps = STEPS
524
        rf.valid_modes = MODES
525
        rf.valid_tmodes = TMODES
526
        rf.valid_cross_modes = CROSS_MODES
527
        rf.valid_bands = [(136000000, 174000000)]
528
        rf.valid_power_levels = POWER_LEVELS
529
        rf.valid_duplexes = DUPLEX
530
        rf.valid_skips = ["", "S", "P"]
531
        rf.valid_name_length = 6
532
        rf.valid_characters = CHARSET
533

    
534
        return rf
535

    
536
    def sync_in(self):
537
        start = time.time()
538
        try:
539
            self._mmap = _download(self)
540
        except errors.RadioError:
541
            raise
542
        except Exception, e:
543
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
544
        LOG.info("Downloaded in %.2f sec" % (time.time() - start))
545
        self.process_mmap()
546

    
547
    def sync_out(self):
548
        self.pipe.timeout = 1
549
        start = time.time()
550
        try:
551
            _upload(self)
552
        except errors.RadioError:
553
            raise
554
        except Exception, e:
555
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
556
        LOG.info("Uploaded in %.2f sec" % (time.time() - start))
557

    
558
    def process_mmap(self):
559
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
560

    
561
    def get_raw_memory(self, number):
562
        return repr(self._memobj.memory[number])
563

    
564
    def get_memory(self, number):
565
        _mem = self._memobj.memory[number]
566
        _flag = self._memobj.flags[(number) / 2]
567

    
568
        nibble = ((number) % 2) and "even" or "odd"
569
        used = _flag["%s_masked" % nibble]
570
        valid = _flag["%s_valid" % nibble]
571
        pskip = _flag["%s_pskip" % nibble]
572
        skip = _flag["%s_skip" % nibble]
573

    
574
        mem = chirp_common.Memory()
575

    
576
        mem.number = number
577

    
578
        if _mem.get_raw()[0] == "\xFF" or not valid or not used:
579
            mem.empty = True
580
            return mem
581

    
582
        mem.tuning_step = STEPS[_mem.step]
583
        mem.freq = int(_mem.freq) * 1000
584

    
585
        # compensate for 12.5 kHz tuning steps, add 500 Hz if needed
586
        if(mem.tuning_step == 12.5):
587
            lastdigit = int(_mem.freq) % 10
588
            if (lastdigit == 2 or lastdigit == 7):
589
                mem.freq += 500
590

    
591
        mem.offset = chirp_common.fix_rounded_step(int(_mem.offset) * 1000)
592
        mem.duplex = DUPLEX[_mem.duplex]
593
        if _mem.tmode < TMODES.index("Cross"):
594
            mem.tmode = TMODES[_mem.tmode]
595
            mem.cross_mode = CROSS_MODES[0]
596
        else:
597
            mem.tmode = "Cross"
598
            mem.cross_mode = CROSS_MODES[_mem.tmode - TMODES.index("Cross")]
599

    
600
        mem.rtone = _decode_tone(_mem.rtone)
601
        mem.ctone = _decode_tone(_mem.ctone)
602

    
603
        # check for unequal ctone/rtone in TSQL mode.  map it as a
604
        # cross tone mode
605
        if mem.rtone != mem.ctone and (mem.tmode == "TSQL" or
606
                                       mem.tmode == "Tone"):
607
            mem.tmode = "Cross"
608
            mem.cross_mode = "Tone->Tone"
609

    
610
        mem.dtcs = _decode_dtcs(_mem.dtcs)
611
        mem.rx_dtcs = _decode_dtcs(_mem.rx_dtcs)
612

    
613
        # check for unequal dtcs/rx_dtcs in DTCS mode.  map it as a
614
        # cross tone mode
615
        if mem.dtcs != mem.rx_dtcs and mem.tmode == "DTCS":
616
            mem.tmode = "Cross"
617
            mem.cross_mode = "DTCS->DTCS"
618

    
619
        if (int(_mem.name[0]) & 0x80) != 0:
620
            mem.name = _decode_name(_mem.name)
621

    
622
        mem.mode = _mem.isnarrow and "NFM" or "FM"
623
        mem.skip = pskip and "P" or skip and "S" or ""
624
        mem.power = POWER_LEVELS[3 - _mem.power]
625

    
626
        return mem
627

    
628
    def set_memory(self, mem):
629
        _mem = self._memobj.memory[mem.number]
630
        _flag = self._memobj.flags[(mem.number) / 2]
631

    
632
        nibble = ((mem.number) % 2) and "even" or "odd"
633

    
634
        valid = _flag["%s_valid" % nibble]
635
        used = _flag["%s_masked" % nibble]
636

    
637
        if not valid:
638
            _wipe_memory(_mem)
639

    
640
        if mem.empty and valid and not used:
641
            _flag["%s_valid" % nibble] = False
642
            return
643

    
644
        _flag["%s_masked" % nibble] = not mem.empty
645

    
646
        if mem.empty:
647
            return
648

    
649
        _flag["%s_valid" % nibble] = True
650

    
651
        _mem.freq = mem.freq / 1000
652
        _mem.offset = mem.offset / 1000
653
        _mem.duplex = DUPLEX.index(mem.duplex)
654

    
655
        # clear all the split tone flags -- we'll set them as needed below
656
        _mem.ctonesplitflag = 0
657
        _mem.rx_dtcssplitflag = 0
658
        _mem.rtonesplitflag = 0
659
        _mem.dtcssplitflag = 0
660

    
661
        if mem.tmode != "Cross":
662
            _mem.tmode = TMODES.index(mem.tmode)
663
            # for the non-cross modes, use ONE tone for both send
664
            # and receive but figure out where to get it from.
665
            if mem.tmode == "TSQL" or mem.tmode == "TSQL-R":
666
                _mem.rtone = chirp_common.TONES.index(mem.ctone)
667
                _mem.ctone = chirp_common.TONES.index(mem.ctone)
668
            else:
669
                _mem.rtone = chirp_common.TONES.index(mem.rtone)
670
                _mem.ctone = chirp_common.TONES.index(mem.rtone)
671

    
672
            # and one tone for dtcs, but this is always the sending one
673
            _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
674
            _mem.rx_dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
675

    
676
        else:
677
            _mem.rtone = chirp_common.TONES.index(mem.rtone)
678
            _mem.ctone = chirp_common.TONES.index(mem.ctone)
679
            _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
680
            _mem.rx_dtcs = chirp_common.DTCS_CODES.index(mem.rx_dtcs)
681
            if mem.cross_mode == "Tone->Tone":
682
                # tone->tone cross mode is treated as
683
                # TSQL, but with separate tones for
684
                # send and receive
685
                _mem.tmode = TMODES.index("TSQL")
686
                _mem.rtonesplitflag = 1
687
            elif mem.cross_mode == "DTCS->DTCS":
688
                # DTCS->DTCS cross mode is treated as
689
                # DTCS, but with separate codes for
690
                # send and receive
691
                _mem.tmode = TMODES.index("DTCS")
692
                _mem.dtcssplitflag = 1
693
            else:
694
                _mem.tmode = TMODES.index("Cross") + \
695
                    CROSS_MODES.index(mem.cross_mode)
696

    
697
        _mem.isnarrow = MODES.index(mem.mode)
698
        _mem.step = STEPS.index(mem.tuning_step)
699
        _flag["%s_pskip" % nibble] = mem.skip == "P"
700
        _flag["%s_skip" % nibble] = mem.skip == "S"
701
        if mem.power:
702
            _mem.power = 3 - POWER_LEVELS.index(mem.power)
703
        else:
704
            _mem.power = 3
705

    
706
        _mem.name = _encode_name(mem.name)
707

    
708
        # set all unknown areas of the memory map to 0
709
        _mem.unknown0 = 0
710
        _mem.unknown1 = 0
711
        _mem.unknown2 = 0
712
        _mem.unknown3 = 0
713
        _mem.unknown4 = 0
714
        _mem.unknown5 = 0
715

    
716
        LOG.debug("encoded mem\n%s\n" % (util.hexprint(_mem.get_raw()[0:20])))
717

    
718
    def get_settings(self):
719
        _settings = self._memobj.settings
720
        _dtmf_strings = self._memobj.dtmf_strings
721
        _passwd = self._memobj.passwd
722

    
723
        repeater = RadioSettingGroup("repeater", "Repeater Settings")
724
        ctcss = RadioSettingGroup("ctcss", "CTCSS/DCS/EPCS Settings")
725
        arts = RadioSettingGroup("arts", "ARTS Settings")
726
        mbls = RadioSettingGroup("banks", "Memory Settings")
727
        scan = RadioSettingGroup("scan", "Scan Settings")
728
        dtmf = RadioSettingGroup("dtmf", "DTMF Settings")
729
        wires = RadioSettingGroup("wires", "WiRES(tm) Settings")
730
        switch = RadioSettingGroup("switch", "Switch/Knob Settings")
731
        disp = RadioSettingGroup("disp", "Display Settings")
732
        misc = RadioSettingGroup("misc", "Miscellaneous Settings")
733

    
734
        setmode = RadioSettings(repeater, ctcss, arts, mbls, scan,
735
                                dtmf, wires, switch, disp, misc)
736

    
737
        # numbers and names of settings refer to the way they're
738
        # presented in the set menu, as well as the list starting on
739
        # page 74 of the manual
740

    
741
        # 1 APO
742
        opts = ["Off", "30 Min", "1 Hour", "3 Hour", "5 Hour", "8 Hour"]
743
        misc.append(
744
            RadioSetting(
745
                "apo", "Automatic Power Off",
746
                RadioSettingValueList(opts, opts[_settings.apo])))
747

    
748
        # 2 AR.BEP
749
        opts = ["Off", "In Range", "Always"]
750
        arts.append(
751
            RadioSetting(
752
                "arts_beep", "ARTS Beep",
753
                RadioSettingValueList(opts, opts[_settings.arts_beep])))
754

    
755
        # 3 AR.INT
756
        opts = ["15 Sec", "25 Sec"]
757
        arts.append(
758
            RadioSetting(
759
                "arts_interval", "ARTS Polling Interval",
760
                RadioSettingValueList(opts, opts[_settings.arts_interval])))
761

    
762
        # 4 ARS
763
        opts = ["Off", "On"]
764
        repeater.append(
765
            RadioSetting(
766
                "ars", "Automatic Repeater Shift",
767
                RadioSettingValueList(opts, opts[_settings.ars])))
768

    
769
        # 5 BCLO
770
        opts = ["Off", "On"]
771
        misc.append(RadioSetting(
772
            "busy_lockout", "Busy Channel Lock-Out",
773
            RadioSettingValueList(opts, opts[_settings.busy_lockout])))
774

    
775
        # 6 BEEP
776
        opts = ["Off", "Key+Scan", "Key"]
777
        switch.append(RadioSetting(
778
            "beep", "Enable the Beeper",
779
            RadioSettingValueList(opts, opts[_settings.beep])))
780

    
781
        # 7 BELL
782
        opts = ["Off", "1", "3", "5", "8", "Continuous"]
783
        ctcss.append(RadioSetting("bell", "Bell Repetitions",
784
                                  RadioSettingValueList(opts, opts[
785
                                                        _settings.bell])))
786

    
787
        # 8 BNK.LNK
788
        for i in range(0, 8):
789
            opts = ["Off", "On"]
790
            mbs = (self._memobj.mbs >> i) & 1
791
            rs = RadioSetting("mbs%i" % i, "Bank %s Scan" % (i + 1),
792
                              RadioSettingValueList(opts, opts[mbs]))
793

    
794
            def apply_mbs(s, index):
795
                if int(s.value):
796
                    self._memobj.mbs |= (1 << index)
797
                else:
798
                    self._memobj.mbs &= ~(1 << index)
799
            rs.set_apply_callback(apply_mbs, i)
800
            mbls.append(rs)
801

    
802
        # 9 BNK.NM - A per-bank attribute, nothing to do here.
803

    
804
        # 10 CLK.SFT - A per-channel attribute, nothing to do here.
805

    
806
        # 11 CW.ID
807
        opts = ["Off", "On"]
808
        arts.append(RadioSetting("cw_id", "CW ID Enable",
809
                                 RadioSettingValueList(opts, opts[
810
                                                       _settings.cw_id])))
811

    
812
        cw_id_text = ""
813
        for i in _settings.cw_id_string:
814
            try:
815
                cw_id_text += CHARSET[i & 0x7F]
816
            except IndexError:
817
                if i != 0xff:
818
                    LOG.debug("unknown char index in cw id: %x " % (i))
819

    
820
        val = RadioSettingValueString(0, 16, cw_id_text, True)
821
        val.set_charset(CHARSET + "abcdefghijklmnopqrstuvwxyz")
822
        rs = RadioSetting("cw_id_string", "CW Identifier Text", val)
823

    
824
        def apply_cw_id(s):
825
            str = s.value.get_value().upper().rstrip()
826
            mval = ""
827
            mval = [chr(CHARSET.index(x)) for x in str]
828
            for x in range(len(mval), 16):
829
                mval.append(chr(0xff))
830
            for x in range(0, 16):
831
                _settings.cw_id_string[x] = ord(mval[x])
832
        rs.set_apply_callback(apply_cw_id)
833
        arts.append(rs)
834

    
835
        # 12 CWTRNG
836
        opts = ["Off", "4WPM", "5WPM", "6WPM", "7WPM", "8WPM", "9WPM",
837
                "10WPM", "11WPM", "12WPM", "13WPM", "15WPM", "17WPM",
838
                "20WPM", "24WPM", "30WPM", "40WPM"]
839
        misc.append(RadioSetting("cw_trng", "CW Training",
840
                                 RadioSettingValueList(opts, opts[
841
                                                       _settings.cw_trng])))
842

    
843
        # todo: make the setting of the units here affect the display
844
        # of the speed.  Not critical, but would be slick.
845
        opts = ["CPM", "WPM"]
846
        misc.append(RadioSetting("cw_trng_units", "CW Training Units",
847
                                 RadioSettingValueList(opts,
848
                                                       opts[_settings.
849
                                                            cw_trng_units])))
850

    
851
        # 13 DC VLT - a read-only status, so nothing to do here
852

    
853
        # 14 DCS CD - A per-channel attribute, nothing to do here
854

    
855
        # 15 DCS.RV
856
        opts = ["Disabled", "Enabled"]
857
        ctcss.append(RadioSetting(
858
                     "inverted_dcs",
859
                     "\"Inverted\" DCS Code Decoding",
860
                     RadioSettingValueList(opts,
861
                                           opts[_settings.inverted_dcs])))
862

    
863
        # 16 DIMMER
864
        opts = ["Off"] + ["Level %d" % (x) for x in range(1, 11)]
865
        disp.append(RadioSetting("dimmer", "Dimmer",
866
                                 RadioSettingValueList(opts,
867
                                                       opts[_settings
868
                                                            .dimmer])))
869

    
870
        # 17 DT.A/M
871
        opts = ["Manual", "Auto"]
872
        dtmf.append(RadioSetting("dtmf_mode", "DTMF Autodialer",
873
                                 RadioSettingValueList(opts,
874
                                                       opts[_settings
875
                                                            .dtmf_mode])))
876

    
877
        # 18 DT.DLY
878
        opts = ["50 ms", "250 ms", "450 ms", "750 ms", "1000 ms"]
879
        dtmf.append(RadioSetting("dtmf_delay", "DTMF Autodialer Delay Time",
880
                                 RadioSettingValueList(opts,
881
                                                       opts[_settings
882
                                                            .dtmf_delay])))
883

    
884
        # 19 DT.SET
885
        for memslot in range(0, 10):
886
            dtmf_memory = ""
887
            for i in _dtmf_strings[memslot].dtmf_string:
888
                if i != 0xFF:
889
                    try:
890
                        dtmf_memory += CHARSET[i]
891
                    except IndexError:
892
                        LOG.debug("unknown char index in dtmf: %x " % (i))
893

    
894
            val = RadioSettingValueString(0, 16, dtmf_memory, True)
895
            val.set_charset(CHARSET + "abcdef")
896
            rs = RadioSetting("dtmf_string_%d" % memslot,
897
                              "DTMF Memory %d" % memslot, val)
898

    
899
            def apply_dtmf(s, i):
900
                LOG.debug("applying dtmf for %x\n" % i)
901
                str = s.value.get_value().upper().rstrip()
902
                LOG.debug("str is %s\n" % str)
903
                mval = ""
904
                mval = [chr(CHARSET.index(x)) for x in str]
905
                for x in range(len(mval), 16):
906
                    mval.append(chr(0xff))
907
                for x in range(0, 16):
908
                    _dtmf_strings[i].dtmf_string[x] = ord(mval[x])
909
            rs.set_apply_callback(apply_dtmf, memslot)
910
            dtmf.append(rs)
911

    
912
        # 20 DT.SPD
913
        opts = ["50 ms", "100 ms"]
914
        dtmf.append(RadioSetting("dtmf_speed",
915
                                 "DTMF Autodialer Sending Speed",
916
                                 RadioSettingValueList(opts,
917
                                                       opts[_settings.
918
                                                            dtmf_speed])))
919

    
920
        # 21 EDG.BEP
921
        opts = ["Off", "On"]
922
        mbls.append(RadioSetting("edge_beep", "Band Edge Beeper",
923
                                 RadioSettingValueList(opts,
924
                                                       opts[_settings.
925
                                                            edge_beep])))
926

    
927
        # 22 INT.CD
928
        opts = ["DTMF %X" % (x) for x in range(0, 16)]
929
        wires.append(RadioSetting("int_cd", "Access Number for WiRES(TM)",
930
                                  RadioSettingValueList(opts, opts[
931
                                                        _settings.int_cd])))
932

    
933
        # 23 ING MD
934
        opts = ["Sister Radio Group", "Friends Radio Group"]
935
        wires.append(RadioSetting("wires_mode",
936
                                  "Internet Link Connection Mode",
937
                                  RadioSettingValueList(opts,
938
                                                        opts[_settings.
939
                                                             wires_mode])))
940

    
941
        # 24 INT.A/M
942
        opts = ["Manual", "Auto"]
943
        wires.append(RadioSetting("wires_auto", "Internet Link Autodialer",
944
                                  RadioSettingValueList(opts,
945
                                                        opts[_settings
946
                                                             .wires_auto])))
947
        # 25 INT.SET
948
        opts = ["F%d" % (x) for x in range(0, 10)]
949

    
950
        wires.append(RadioSetting("int_set", "Memory Register for "
951
                                  "non-WiRES Internet",
952
                                  RadioSettingValueList(opts,
953
                                                        opts[_settings
954
                                                             .int_set])))
955

    
956
        # 26 LOCK
957
        opts = ["Key", "Dial", "Key + Dial", "PTT",
958
                "Key + PTT", "Dial + PTT", "All"]
959
        switch.append(RadioSetting("lock", "Control Locking",
960
                                   RadioSettingValueList(opts,
961
                                                         opts[_settings
962
                                                              .lock])))
963

    
964
        # 27 MCGAIN
965
        opts = ["Level %d" % (x) for x in range(1, 10)]
966
        misc.append(RadioSetting("mic_gain", "Microphone Gain",
967
                                 RadioSettingValueList(opts,
968
                                                       opts[_settings
969
                                                            .mic_gain])))
970

    
971
        # 28 MEM.SCN
972
        opts = ["Tag 1", "Tag 2", "All Channels"]
973
        rs = RadioSetting("scan_mode", "Memory Scan Mode",
974
                          RadioSettingValueList(opts,
975
                                                opts[_settings
976
                                                     .scan_mode - 1]))
977
        # this setting is unusual in that it starts at 1 instead of 0.
978
        # that is, index 1 corresponds to "Tag 1", and index 0 is invalid.
979
        # so we create a custom callback to handle this.
980

    
981
        def apply_scan_mode(s):
982
            myopts = ["Tag 1", "Tag 2", "All Channels"]
983
            _settings.scan_mode = myopts.index(s.value.get_value()) + 1
984
        rs.set_apply_callback(apply_scan_mode)
985
        mbls.append(rs)
986

    
987
        # 29 MW MD
988
        opts = ["Lower", "Next"]
989
        mbls.append(RadioSetting("mw_mode", "Memory Write Mode",
990
                                 RadioSettingValueList(opts,
991
                                                       opts[_settings
992
                                                            .mw_mode])))
993

    
994
        # 30 NM SET - This is per channel, so nothing to do here
995

    
996
        # 31 OPN.MSG
997
        opts = ["Off", "DC Supply Voltage", "Text Message"]
998
        disp.append(RadioSetting("open_msg", "Opening Message Type",
999
                                 RadioSettingValueList(opts,
1000
                                                       opts[_settings.
1001
                                                            open_msg])))
1002

    
1003
        openmsg = ""
1004
        for i in _settings.openMsg_Text:
1005
            try:
1006
                openmsg += CHARSET[i & 0x7F]
1007
            except IndexError:
1008
                if i != 0xff:
1009
                    LOG.debug("unknown char index in openmsg: %x " % (i))
1010

    
1011
        val = RadioSettingValueString(0, 6, openmsg, True)
1012
        val.set_charset(CHARSET + "abcdefghijklmnopqrstuvwxyz")
1013
        rs = RadioSetting("openMsg_Text", "Opening Message Text", val)
1014

    
1015
        def apply_openmsg(s):
1016
            str = s.value.get_value().upper().rstrip()
1017
            mval = ""
1018
            mval = [chr(CHARSET.index(x)) for x in str]
1019
            for x in range(len(mval), 6):
1020
                mval.append(chr(0xff))
1021
            for x in range(0, 6):
1022
                _settings.openMsg_Text[x] = ord(mval[x])
1023
        rs.set_apply_callback(apply_openmsg)
1024
        disp.append(rs)
1025

    
1026
        # 32 PAGER - a per-channel attribute
1027

    
1028
        # 33 PAG.ABK
1029
        opts = ["Off", "On"]
1030
        ctcss.append(RadioSetting("pag_abk", "Paging Answer Back",
1031
                                  RadioSettingValueList(opts,
1032
                                                        opts[_settings
1033
                                                             .pag_abk])))
1034

    
1035
        # 34 PAG.CDR
1036
        opts = ["%2.2d" % (x) for x in range(1, 50)]
1037
        ctcss.append(RadioSetting("pag_cdr_1", "Receive Page Code 1",
1038
                                  RadioSettingValueList(opts,
1039
                                                        opts[_settings
1040
                                                             .pag_cdr_1])))
1041

    
1042
        ctcss.append(RadioSetting("pag_cdr_2", "Receive Page Code 2",
1043
                                  RadioSettingValueList(opts,
1044
                                                        opts[_settings
1045
                                                             .pag_cdr_2])))
1046

    
1047
        # 35 PAG.CDT
1048
        opts = ["%2.2d" % (x) for x in range(1, 50)]
1049
        ctcss.append(RadioSetting("pag_cdt_1", "Transmit Page Code 1",
1050
                                  RadioSettingValueList(opts,
1051
                                                        opts[_settings
1052
                                                             .pag_cdt_1])))
1053

    
1054
        ctcss.append(RadioSetting("pag_cdt_2", "Transmit Page Code 2",
1055
                                  RadioSettingValueList(opts,
1056
                                                        opts[_settings
1057
                                                             .pag_cdt_2])))
1058

    
1059
        # Common Button Options
1060
        button_opts = ["Squelch Off", "Weather", "Smart Search",
1061
                       "Tone Scan", "Scan", "T Call", "ARTS"]
1062

    
1063
        # 36 PRG P1
1064
        opts = button_opts + ["DC Volts"]
1065
        switch.append(RadioSetting(
1066
            "prog_p1", "P1 Button",
1067
            RadioSettingValueList(opts, opts[_settings.prog_p1])))
1068

    
1069
        # 37 PRG P2
1070
        opts = button_opts + ["Dimmer"]
1071
        switch.append(RadioSetting(
1072
            "prog_p2", "P2 Button",
1073
            RadioSettingValueList(opts, opts[_settings.prog_p2])))
1074

    
1075
        # 38 PRG P3
1076
        opts = button_opts + ["Mic Gain"]
1077
        switch.append(RadioSetting(
1078
            "prog_p3", "P3 Button",
1079
            RadioSettingValueList(opts, opts[_settings.prog_p3])))
1080

    
1081
        # 39 PRG P4
1082
        opts = button_opts + ["Skip"]
1083
        switch.append(RadioSetting(
1084
            "prog_p4", "P4 Button",
1085
            RadioSettingValueList(opts, opts[_settings.prog_p4])))
1086

    
1087
        # 40 PSWD
1088
        password = ""
1089
        for i in _passwd:
1090
            if i != 0xFF:
1091
                try:
1092
                    password += CHARSET[i]
1093
                except IndexError:
1094
                    LOG.debug("unknown char index in password: %x " % (i))
1095

    
1096
        val = RadioSettingValueString(0, 4, password, True)
1097
        val.set_charset(CHARSET[0:15] + "abcdef ")
1098
        rs = RadioSetting("passwd", "Password", val)
1099

    
1100
        def apply_password(s):
1101
            str = s.value.get_value().upper().rstrip()
1102
            mval = ""
1103
            mval = [chr(CHARSET.index(x)) for x in str]
1104
            for x in range(len(mval), 4):
1105
                mval.append(chr(0xff))
1106
            for x in range(0, 4):
1107
                _passwd[x] = ord(mval[x])
1108
        rs.set_apply_callback(apply_password)
1109
        misc.append(rs)
1110

    
1111
        # 41 RESUME
1112
        opts = ["3 Sec", "5 Sec", "10 Sec", "Busy", "Hold"]
1113
        scan.append(RadioSetting("resume", "Scan Resume Mode",
1114
                                 RadioSettingValueList(opts, opts[
1115
                                                       _settings.resume])))
1116

    
1117
        # 42 RF.SQL
1118
        opts = ["Off"] + ["S-%d" % (x) for x in range(1, 10)]
1119
        misc.append(RadioSetting("rf_sql", "RF Squelch Threshold",
1120
                                 RadioSettingValueList(opts, opts[
1121
                                                       _settings.rf_sql])))
1122

    
1123
        # 43 RPT - per channel attribute, nothing to do here
1124

    
1125
        # 44 RVRT
1126
        opts = ["Off", "On"]
1127
        misc.append(RadioSetting("revert", "Priority Revert",
1128
                                 RadioSettingValueList(opts, opts[
1129
                                                       _settings.revert])))
1130

    
1131
        # 45 S.SRCH
1132
        opts = ["Single", "Continuous"]
1133
        misc.append(RadioSetting("s_search", "Smart Search Sweep Mode",
1134
                                 RadioSettingValueList(opts, opts[
1135
                                                       _settings.s_search])))
1136

    
1137
        # 46 SHIFT - per channel setting, nothing to do here
1138

    
1139
        # 47 SKIP = per channel setting, nothing to do here
1140

    
1141
        # 48 SPLIT - per channel attribute, nothing to do here
1142

    
1143
        # 49 SQL.TYP - per channel attribute, nothing to do here
1144

    
1145
        # 50 STEP - per channel attribute, nothing to do here
1146

    
1147
        # 51 TEMP - read-only status, nothing to do here
1148

    
1149
        # 52 TN FRQ - per channel attribute, nothing to do here
1150

    
1151
        # 53 TOT
1152
        opts = ["Off", "1 Min", "3 Min", "5 Min", "10 Min"]
1153
        misc.append(RadioSetting("tot", "Timeout Timer",
1154
                                 RadioSettingValueList(opts,
1155
                                                       opts[_settings.tot])))
1156

    
1157
        # 54 TS MUT
1158
        opts = ["Off", "On"]
1159
        ctcss.append(RadioSetting("ts_mut", "Tone Search Mute",
1160
                                  RadioSettingValueList(opts,
1161
                                                        opts[_settings
1162
                                                             .ts_mut])))
1163

    
1164
        # 55 TS SPEED
1165
        opts = ["Fast", "Slow"]
1166
        ctcss.append(RadioSetting("ts_speed", "Tone Search Scanner Speed",
1167
                                  RadioSettingValueList(opts,
1168
                                                        opts[_settings
1169
                                                             .ts_speed])))
1170

    
1171
        # 56 VFO.SCN
1172
        opts = ["+/- 1MHz", "+/- 2MHz", "+/-5MHz", "All"]
1173
        scan.append(RadioSetting("vfo_scan", "VFO Scanner Width",
1174
                                 RadioSettingValueList(opts,
1175
                                                       opts[_settings
1176
                                                            .vfo_scan])))
1177

    
1178
        # 57 WX.ALT
1179
        opts = ["Off", "On"]
1180
        misc.append(RadioSetting("wx_alert", "Weather Alert Scan",
1181
                                 RadioSettingValueList(opts, opts[
1182
                                                       _settings.wx_alert])))
1183

    
1184
        # 58 WX.VOL
1185
        opts = ["Normal", "Maximum"]
1186
        misc.append(RadioSetting("wx_vol_max", "Weather Alert Volume",
1187
                                 RadioSettingValueList(opts, opts[
1188
                                                       _settings.wx_vol_max])))
1189

    
1190
        # 59 W/N DV - this is a per-channel attribute, nothing to do here
1191

    
1192
        return setmode
1193

    
1194
    def set_settings(self, uisettings):
1195
        _settings = self._memobj.settings
1196
        for element in uisettings:
1197
            if not isinstance(element, RadioSetting):
1198
                self.set_settings(element)
1199
                continue
1200
            if not element.changed():
1201
                continue
1202

    
1203
            try:
1204
                name = element.get_name()
1205
                value = element.value
1206

    
1207
                if element.has_apply_callback():
1208
                    LOG.debug("Using apply callback")
1209
                    element.run_apply_callback()
1210
                else:
1211
                    obj = getattr(_settings, name)
1212
                    setattr(_settings, name, value)
1213

    
1214
                LOG.debug("Setting %s: %s" % (name, value))
1215
            except Exception, e:
1216
                LOG.debug(element.get_name())
1217
                raise
1218

    
1219
    def get_bank_model(self):
1220
        return FT2900BankModel(self)
1221

    
1222
    @classmethod
1223
    def match_model(cls, filedata, filename):
1224
        return len(filedata) == cls._memsize
1225

    
1226
    @classmethod
1227
    def get_prompts(cls):
1228
        rp = chirp_common.RadioPrompts()
1229
        rp.pre_download = _(dedent("""\
1230
            1. Turn Radio off.
1231
            2. Connect data cable.
1232
            3. While holding "A/N LOW" button, turn radio on.
1233
            4. <b>After clicking OK</b>, press "SET MHz" to send image."""))
1234
        rp.pre_upload = _(dedent("""\
1235
            1. Turn Radio off.
1236
            2. Connect data cable.
1237
            3. While holding "A/N LOW" button, turn radio on.
1238
            4. Press "MW D/MR" to receive image.
1239
            5. Make sure display says "-WAIT-" (see note below if not)
1240
            6. Click OK to dismiss this dialog and start transfer.
1241

    
1242
            Note: if you don't see "-WAIT-" at step 5, try cycling
1243
                  power and pressing and holding red "*L" button to unlock
1244
                  radio, then start back at step 1."""))
1245
        return rp
1246

    
1247

    
1248
# the FT2900E is the European version of the radio, almost identical
1249
# to the R (USA) version, except for the model number and ID Block.  We
1250
# create and register a class for it, with only the needed overrides
1251
# NOTE: Disabled until detection is fixed
1252
# @directory.register
1253
class FT2900ERadio(FT2900Radio):
1254

    
1255
    """Yaesu FT-2900E"""
1256
    MODEL = "FT-2900E/1900E"
1257
    VARIANT = "E"
1258
    IDBLOCK = "\x56\x43\x32\x33\x00\x02\x41\x02\x01\x01"
    (1-1/1)