Project

General

Profile

Bug #10969 » ft1d.py

55fc046c - Dan Smith, 11/27/2023 03:13 PM

 
1
# Copyright 2010 Dan Smith <dsmith@danplanet.com>
2
# Copyright 2014 Angus Ainslie <angus@akkea.ca>
3
# Copyright 2023 Declan Rieb <WD5EQY@arrl.net>
4
# Sections of digital settings applied from ft70.py, thus
5
# Copyright 2017 Nicolas Pike <nick@zbm2.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
import re
21
import string
22
import logging
23

    
24
from chirp.drivers import yaesu_clone
25
from chirp import chirp_common, directory, bitwise
26
from chirp import memmap
27
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings, \
28
            RadioSettingValueInteger, RadioSettingValueString, \
29
            RadioSettingValueList, RadioSettingValueBoolean, \
30
            InvalidValueError
31
from chirp import util
32

    
33
LOG = logging.getLogger(__name__)
34

    
35
MEM_SETTINGS_FORMAT = """
36
#seekto 0x049a;
37
struct {
38
  u8 vfo_a;
39
  u8 vfo_b;
40
} squelch;
41

    
42
#seekto 0x04c1;
43
struct {
44
  u8 beep;
45
} beep_select;
46

    
47
#seekto 0x04ce;
48
struct {
49
  u8 lcd_dimmer;        // 14 DIMMER
50
  u8 dtmf_delay;        // 18 DT DLY
51
  u8 unknown0[3];
52
  u8 unknown1:4,
53
     lcd_contrast:4;
54
  u8 lamp;              // 28 LAMP
55
  u8 lock;              // 30 LOCK
56
  u8 unknown2;
57
  u8 mic_gain;          // 31 MCGAIN
58
  u8 unknown2_3;
59
  u8 dw_interval;       // 21 DW INT
60
  u8 ptt_delay;         // 42 PTT.DLY
61
  u8 rx_save;           // 48 RX.SAVE
62
  u8 scan_restart;      // 53 SCN.STR
63
  u8 unknown3;
64
  u8 scan_resume;
65
  u8 unknown4[5];
66
  u8 tot;
67
  u8 unknown5[3];
68
  u8 vfo_mode:1,        // 60 VFO.MOD
69
     unknown6:1,
70
     scan_lamp:1,       // 51 SCN.LMP
71
     unknown7:1,
72
     ars:1,             // 45 RPT.ARS
73
     dtmf_speed:1,      // 20 DT SPD
74
     unknown8:1,
75
     dtmf_mode:1;
76
  u8 busy_led:1,
77
     unknown9:2,
78
     bclo:1,            // 03 BCLO
79
     beep_edge:1,       // 06 BEP.EDG
80
     unknown9_1:3;
81
  u8 unknown10:5,
82
     password:1,
83
     home_rev:1,        // 26 HOME/REV
84
     moni:1;            // 32 MON/T-Call
85
  u8 gm_interval:4,     // 25 GM INT
86
     unknown11:4;
87
  u8 vol_mode:1,
88
     unknown11_1:7;
89
  u8 unknown12:4,
90
     home_vfo:1,        // 27 HOME->VFO
91
     unknown12_1:2,
92
     dw_rt:1;           // 23 DW RVT;
93
} scan_settings;
94

    
95
#seekto 0x064a;
96
struct {
97
  u8 unknown0[4];
98
  u8 frequency_band;
99
  u8 unknown1:6,
100
     manual_or_mr:2;
101
  u8 unknown2:7,
102
     mr_banks:1;
103
  u8 unknown3;
104
  u16 mr_index;
105
  u16 bank_index;
106
  u16 bank_enable;
107
  u8 unknown4[5];
108
  u8 unknown5:6,
109
     power:2;
110
  u8 unknown6:4,
111
     tune_step:4;
112
  u8 unknown7:6,
113
     duplex:2;
114
  u8 unknown8:6,
115
     tone_mode:2;
116
  u8 unknown9:2,
117
     tone:6;
118
  u8 unknown10;
119
  u8 unknown11:6,
120
     mode:2;
121
  bbcd freq0[4];
122
  bbcd offset_freq[4];
123
  u8 unknown12[2];
124
  char label[16];
125
  u8 unknown13[6];
126
  bbcd band_lower[4];
127
  bbcd band_upper[4];
128
  bbcd rx_freq[4];
129
  u8 unknown14[22];
130
  bbcd freq1[4];
131
  u8 unknown15[11];
132
  u8 unknown16:3,
133
     volume:5;
134
  u8 unknown17[18];
135
  u8 active_menu_item;
136
  u8 checksum;
137
} vfo_info[6];
138

    
139
#seekto 0x047e;
140
struct {
141
  u8 unknown1;
142
  u8 flag;
143
  u16 unknown2;
144
  struct {
145
    u8 padded_yaesu[16];
146
  } message;
147
} opening_message;
148

    
149
#seekto 0x%(dtmadd)04X; // FT-1D:0e4a, FT2D:094a
150
struct {
151
  u8 memory[16];
152
} dtmf[10];
153

    
154
#seekto 0x154a;
155
// These "channels" seem to actually be a structure:
156
//  first five bits are flags
157
//      0   Unused (1=entry is unused)
158
//      1   SW Broadcast
159
//      2   VHF Marine
160
//      3   WX (weather)
161
//      4   ? a mode? ?
162
//  11 bits of index into frequency tables
163
//
164
struct {
165
    u16 channel[100];
166
} bank_members[24];
167

    
168
#seekto 0x54a;
169
struct {
170
    u16 in_use;
171
} bank_used[24];
172

    
173
#seekto 0x0EFE;
174
struct {
175
  u8 unknown[2];
176
  u8 name[16];
177
} bank_info[24];
178
"""
179

    
180
MEM_FORMAT = """
181
struct memslot {
182
  u8 unknown0:2,
183
     mode_alt:1,  // mode for FTM-3200D
184
     clock_shift:1,
185
     unknown1:4;
186
  u8 mode:2,
187
     duplex:2,
188
     tune_step:4;
189
  bbcd freq[3];
190
  u8 power:2,
191
     digmode:2,   // 0=Analog, 1=AMS, 2=DN, 3=VW
192
     tone_mode:4;
193
  u16 charsetbits;
194
  char label[16];
195
  bbcd offset[3];
196
  u8 unknown5:2,
197
     tone:6;
198
  u8 unknown6:1,
199
     dcs:7;
200
  u16 unknown7;
201
  u8 unknown8:2,
202
     att:1,
203
     autostep:1,
204
     automode:1,
205
     unknown9:3;
206
};
207
#seekto 0x2D4A;
208
struct memslot memory[%(memnum)d];
209
struct memslot Skip[99];
210
struct memslot PMS[100];
211
#seekto 0x10ca;
212
struct memslot Home[11];
213

    
214
struct flagslot {
215
  u8 nosubvfo:1,
216
     unknown:3,
217
     pskip:1,
218
     skip:1,
219
     used:1,
220
     valid:1;
221
};
222
#seekto 0x280A;
223
struct flagslot flag[%(memnum)d];
224
struct flagslot flagskp[99];
225
struct flagslot flagPMS[100];
226
"""
227

    
228
MEM_APRS_FORMAT = """
229
#seekto 0xbeca;
230
struct {
231
  u8 rx_baud;
232
  u8 custom_symbol;
233
  struct {
234
    char callsign[6];
235
    u8 ssid;
236
  } my_callsign;
237
  u8 unknown3:4,
238
     selected_position_comment:4;
239
  u8 unknown4;
240
  u8 set_time_manually:1,
241
     tx_interval_beacon:1,
242
     ring_beacon:1,
243
     ring_msg:1,
244
     aprs_mute:1,
245
     unknown6:1,
246
     tx_smartbeacon:1,
247
     af_dual:1;
248
  u8 unknown7:1,
249
     aprs_units_wind_mph:1,
250
     aprs_units_rain_inch:1,
251
     aprs_units_temperature_f:1,
252
     aprs_units_altitude_ft:1,
253
     unknown8:1,
254
     aprs_units_distance_m:1,
255
     aprs_units_position_mmss:1;
256
  u8 unknown9:6,
257
     aprs_units_speed:2;
258
  u8 unknown11:1,
259
     filter_other:1,
260
     filter_status:1,
261
     filter_item:1,
262
     filter_object:1,
263
     filter_weather:1,
264
     filter_position:1,
265
     filter_mic_e:1;
266
  u8 unknown12;
267
  u8 unknown13;
268
  u8 unknown14;
269
  u8 unknown15:7,
270
     latitude_sign:1;
271
  u8 latitude_degree;
272
  u8 latitude_minute;
273
  u8 latitude_second;
274
  u8 unknown16:7,
275
     longitude_sign:1;
276
  u8 longitude_degree;
277
  u8 longitude_minute;
278
  u8 longitude_second;
279
  u8 unknown17:4,
280
     selected_position:4;
281
  u8 unknown18:5,
282
     selected_beacon_status_txt:3;
283
  u8 unknown19:4,
284
     beacon_interval:4;
285
  u8 unknowni21:4,
286
       tx_delay:4;
287
  u8 unknown21b:6,
288
     gps_units_altitude_ft:1,
289
     gps_units_position_sss:1;
290
  u8 unknown20:6,
291
     gps_units_speed:2;
292
  u8 unknown21c[4];
293
  struct {
294
    struct {
295
      char callsign[6];
296
      u8 ssid;
297
    } entry[8];
298
  } digi_path_7;
299
  u8 unknown22[18];
300
  struct {
301
    char padded_string[16];
302
  } message_macro[7];
303
  u8 unknown23:5,
304
     selected_msg_group:3;
305
  u8 unknown24;
306
  struct {
307
    char padded_string[9];
308
  } msg_group[8];
309
  u8 unknown25;
310
  u8 unknown25a:2,
311
     timezone:6;
312
  u8 unknown25b[2];
313
  u8 active_smartbeaconing;
314
  struct {
315
    u8 low_speed_mph;
316
    u8 high_speed_mph;
317
    u8 slow_rate_min;
318
    u8 fast_rate_sec;
319
    u8 turn_angle;
320
    u8 turn_slop;
321
    u8 turn_time_sec;
322
  } smartbeaconing_profile[3];
323
  u8 unknown26:2,
324
     flash_msg:6;
325
  u8 unknown27:2,
326
     flash_grp:6;
327
  u8 unknown28:2,
328
     flash_bln:6;
329
  u8 selected_digi_path;
330
  struct {
331
    struct {
332
      char callsign[6];
333
      u8 ssid;
334
    } entry[2];
335
  } digi_path_3_6[4];
336
  u8 unknown30:6,
337
     selected_my_symbol:2;
338
  u8 unknown31[3];
339
  u8 unknown32:2,
340
     vibrate_msg:6;
341
  u8 unknown33:2,
342
     vibrate_grp:6;
343
  u8 unknown34:2,
344
     vibrate_bln:6;
345
} aprs;
346

    
347
#seekto 0xc26a;
348
struct {
349
  char padded_string[60];
350
} aprs_beacon_status_txt[5];
351

    
352
#seekto 0xFECA;
353
struct {
354
  bbcd date[3];
355
  bbcd time[2];
356
  u8 sequence;
357
  u8 unknown1;
358
  u8 unknown2;
359
  char sender_callsign[9];
360
  u8 data_type;
361
  u8 yeasu_data_type;
362
  u8 unknown4:1,
363
     callsign_is_ascii:1,
364
     unknown5:6;
365
  u8 unknown6;
366
  u16 pkt_len;
367
  u8 unknown7;
368
  u16 in_use;
369
  u16 unknown8;
370
  u16 unknown9;
371
  u16 unknown10;
372
} aprs_beacon_meta[60];
373

    
374
#seekto 0x1064A;
375
struct {
376
  char dst_callsign[9];
377
  char path[30];
378
  u16 flags;
379
  u8 separator;
380
  char body[134];
381
} aprs_beacon_pkt[60];
382

    
383
#seekto 0x137c4;
384
struct {
385
  u8 flag;
386
  char dst_callsign[6];
387
  u8 dst_callsign_ssid;
388
  char path_and_body[66];
389
  u8 unknown[70];
390
} aprs_message_pkt[60];
391
"""
392

    
393
MEM_BACKTRACK_FORMAT = """
394
#seekto 0xdf06;
395
struct {
396
  u8 status; // 01 full 08 empty
397
  u8 reserved0; // 00
398
  bbcd year; // 17
399
  bbcd mon; // 06
400
  bbcd day; // 01
401
  u8 reserved1; // 06
402
  bbcd hour; // 21
403
  bbcd min; // xx
404
  u8 reserved2; // 00
405
  u8 reserved3; // 00
406
  char NShemi[1];
407
  char lat[3];
408
  char lat_min[2];
409
  char lat_dec_sec[4];
410
  char WEhemi[1];
411
  char lon[3];
412
  char lon_min[2];
413
  char lon_dec_sec[4];
414
} backtrack[3];
415

    
416
"""
417

    
418
MEM_GM_FORMAT = """
419
#seekto 0x04ba;
420
struct {
421
    u8 unknown:3,
422
        scan_resume:5;          // 52 SCN.RSM
423
    u8 unknown1:3,
424
       dw_resume_interval:5;       // 22 DW RSM
425
    u8 unknown2;
426
    u8 unknown3:3,
427
        apo:5;                  // 02 APO
428
    u8 unknown4:6,
429
        gm_ring:2;              // 24 GM RNG
430
    u8 temp_cf;               // Placeholder as not found
431
    u8 unknown5;
432
    } first_settings;
433

    
434
#seekto 0x04ed;
435
struct {
436
    u8 unknown1:1,
437
       unknown2:1,
438
       unknown3:1,
439
       unknown4:1,
440
       unknown5:1,
441
       unknown6:1,
442
       unknown7:1,
443
       unknown8:1;
444
     } test_bit_field;
445

    
446
#seekto 0x04c0;
447
struct {
448
    u8 unknown1:5,
449
        beep_level:3;           // 05 BEP.LVL
450
    u8 unknown2:6,
451
        beep_select:2;          // 04 BEEP
452
    } beep_settings;
453

    
454
#seekto 0xCF30;
455
struct {
456
    u8 unknown0;
457
    u8 unknown1;
458
    u8 unknown2;
459
    u8 unknown3;
460
    u8 unknown4;
461
    u8 unknown5;
462
    u8 unknown6;
463
    u8 digital_popup;              // 15 DIG.POP
464
    } digital_settings_more;
465

    
466
#seekto 0xCF7C;
467
struct {
468
    u8 unknown0:6,
469
       ams_tx_mode:2;              // AMS TX Mode
470
    u8 unknown1;
471
    u8 unknown2:7,
472
       standby_beep:1;             // 07 BEP.STB
473
    u8 unknown3;
474
    u8 unknown4:6,
475
       gm_ring:2;                  // 24 GM RNG
476
    u8 unknown5;
477
    u8 rx_dg_id;                   // RX DG-ID
478
    u8 tx_dg_id;                   // TX DG-ID
479
    u8 unknown6:7,
480
       vw_mode:1;                  // 16 DIG VW
481
    u8 unknown7;
482
    } digital_settings;
483

    
484
#seekto 0x1d6d3;
485
struct {
486
    char message[32];
487
    } GM[10];
488

    
489
#seekto 0x0ced0;
490
struct {
491
    char callsign[10];              // 63 MYCALL
492
    u16 charset;                    // character set ID
493
    } my_call;
494

    
495
#seekto 0x1ddca;
496
struct {
497
    struct {
498
        char name[16];
499
    } Category[5];
500
    struct {
501
        struct {
502
            char ID[5];             // ASCII numerals
503
            char name[16];          // blank-fill
504
            u8 unknown[3];
505
        } Rooms[20];
506
    } RoomsPerCategory[5];
507
} WiresX_settings;
508
"""
509

    
510
MEM_CHECKSUM_FORMAT = """
511
#seekto 0x1FDC9;
512
u8 checksum;
513
"""
514

    
515
TMODES = ["", "Tone", "TSQL", "DTCS"]
516
DUPLEX = ["", "-", "+", "split"]
517
MODES = ["FM", "AM", "WFM"]
518
STEPS = list(chirp_common.TUNING_STEPS)
519
STEPS.remove(30.0)
520
STEPS.append(100.0)
521
STEPS.insert(2, 0.0)  # There is a skipped tuning step at index 2 (?)
522
SKIPS = ["", "S", "P"]
523
FT1_DTMF_CHARS = list("0123456789ABCD*#-")
524

    
525
CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
526
    [chr(x) for x in range(ord("A"), ord("Z") + 1)] + \
527
    [" ", ] + \
528
    [chr(x) for x in range(ord("a"), ord("z") + 1)] + \
529
    list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100)
530

    
531
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
532
                chirp_common.PowerLevel("L3", watts=2.50),
533
                chirp_common.PowerLevel("L2", watts=1.00),
534
                chirp_common.PowerLevel("L1", watts=0.05)]
535
SKIPNAMES = ["Skip%i" % i for i in range(901, 1000)]
536
PMSNAMES = ["%s%i" % (c, i) for i in range(1, 51) for c in ['L', 'U']]
537
HOMENAMES = ["Home%i" % i for i in range(1, 12)]
538
ALLNAMES = SKIPNAMES + PMSNAMES + HOMENAMES
539
# list of (array name, (list of memories in that array))
540
# array names must match names of memories defined for radio
541
SPECIALS = [
542
    ("Skip", SKIPNAMES),
543
    ("PMS", PMSNAMES),
544
    ("Home", HOMENAMES)
545
]
546
# Band edges are integer Hz.
547
VALID_BANDS = [
548
    (510000, 1790000),
549
    (1800000, 50490000),
550
    (50500000, 75990000),
551
    (76000000, 107990000),
552
    (108000000, 136990000),
553
    (145000000, 169920000),
554
    (174000000, 221950000),
555
    (222000000, 419990000),
556
    (420000000, 469990000),
557
    (470000000, 773990000),
558
    (810010000, 999000000)
559
]
560

    
561

    
562
class FT1Bank(chirp_common.NamedBank):
563
    """A FT1D bank"""
564

    
565
    def get_name(self):
566
        _bank = self._model._radio._memobj.bank_info[self.index]
567

    
568
        name = ""
569
        for i in _bank.name:
570
            if i == 0xFF:
571
                break
572
            name += CHARSET[i & 0x7F]
573
        return name.rstrip()
574

    
575
    def set_name(self, name):
576
        _bank = self._model._radio._memobj.bank_info[self.index]
577
        _bank.name = [CHARSET.index(x) for x in name.ljust(16)[:16]]
578

    
579

    
580
class FT1BankModel(chirp_common.BankModel):
581
    """A FT1D bank model"""
582
    def __init__(self, radio, name='Banks'):
583
        super(FT1BankModel, self).__init__(radio, name)
584

    
585
        _banks = self._radio._memobj.bank_info
586
        self._bank_mappings = []
587
        for index, _bank in enumerate(_banks):
588
            bank = FT1Bank(self, "%i" % index, "BANK-%i" % index)
589
            bank.index = index
590
            self._bank_mappings.append(bank)
591

    
592
    def get_num_mappings(self):
593
        return len(self._bank_mappings)
594

    
595
    def get_mappings(self):
596
        return self._bank_mappings
597

    
598
    def _channel_numbers_in_bank(self, bank):
599
        _bank_used = self._radio._memobj.bank_used[bank.index]
600
        if _bank_used.in_use == 0xFFFF:
601
            return set()
602

    
603
        _members = self._radio._memobj.bank_members[bank.index]
604
        return set([int(ch) + 1 for ch in _members.channel if ch != 0xFFFF])
605

    
606
    def update_vfo(self):
607
        chosen_bank = [None, None]
608
        chosen_mr = [None, None]
609

    
610
        flags = self._radio._memobj.flag
611

    
612
        # Find a suitable bank and MR for VFO A and B.
613
        for bank in self.get_mappings():
614
            for channel in self._channel_numbers_in_bank(bank):
615
                chosen_bank[0] = bank.index
616
                chosen_mr[0] = channel
617
                if channel & 0x7000 != 0:
618
                    # Ignore preset channels without comment DAR
619
                    break
620
                if not flags[channel].nosubvfo:
621
                    chosen_bank[1] = bank.index
622
                    chosen_mr[1] = channel
623
                    break
624
            if chosen_bank[1]:
625
                break
626

    
627
        for vfo_index in (0, 1):
628
            # 3 VFO info structs are stored as 3 pairs of (master, backup)
629
            vfo = self._radio._memobj.vfo_info[vfo_index * 2]
630
            vfo_bak = self._radio._memobj.vfo_info[(vfo_index * 2) + 1]
631

    
632
            if vfo.checksum != vfo_bak.checksum:
633
                LOG.warn("VFO settings are inconsistent with backup")
634
            else:
635
                if ((chosen_bank[vfo_index] is None) and (vfo.bank_index !=
636
                                                          0xFFFF)):
637
                    LOG.info("Disabling banks for VFO %d" % vfo_index)
638
                    vfo.bank_index = 0xFFFF
639
                    vfo.mr_index = 0xFFFF
640
                    vfo.bank_enable = 0xFFFF
641
                elif ((chosen_bank[vfo_index] is not None) and
642
                      (vfo.bank_index == 0xFFFF)):
643
                    LOG.info("Enabling banks for VFO %d" % vfo_index)
644
                    vfo.bank_index = chosen_bank[vfo_index]
645
                    vfo.mr_index = chosen_mr[vfo_index]
646
                    vfo.bank_enable = 0x0000
647
                vfo_bak.bank_index = vfo.bank_index
648
                vfo_bak.mr_index = vfo.mr_index
649
                vfo_bak.bank_enable = vfo.bank_enable
650

    
651
    def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
652
        _members = self._radio._memobj.bank_members[bank.index]
653
        if len(channels_in_bank) > len(_members.channel):
654
            raise Exception("Too many entries in bank %d" % bank.index)
655

    
656
        empty = 0
657
        for index, channel_number in enumerate(sorted(channels_in_bank)):
658
            _members.channel[index] = channel_number - 1
659
            if channel_number & 0x7000 != 0:
660
                LOG.warn("Bank %d uses Yaesu preset frequency id=%04X. "
661
                         "Chirp cannot see or change that entry." % (
662
                             bank.index, channel_number))
663
            empty = index + 1
664
        for index in range(empty, len(_members.channel)):
665
            _members.channel[index] = 0xFFFF
666

    
667
    def add_memory_to_mapping(self, memory, bank):
668
        channels_in_bank = self._channel_numbers_in_bank(bank)
669
        channels_in_bank.add(memory.number)
670
        self._update_bank_with_channel_numbers(bank, channels_in_bank)
671

    
672
        _bank_used = self._radio._memobj.bank_used[bank.index]
673
        _bank_used.in_use = 0x06
674

    
675
        self.update_vfo()
676

    
677
    def remove_memory_from_mapping(self, memory, bank):
678
        channels_in_bank = self._channel_numbers_in_bank(bank)
679
        try:
680
            channels_in_bank.remove(memory.number)
681
        except KeyError:
682
            raise Exception("Memory %i is not in bank %s. Cannot remove" %
683
                            (memory.number, bank))
684
        self._update_bank_with_channel_numbers(bank, channels_in_bank)
685

    
686
        if not channels_in_bank:
687
            _bank_used = self._radio._memobj.bank_used[bank.index]
688
            _bank_used.in_use = 0xFFFF
689

    
690
        self.update_vfo()
691

    
692
    def get_mapping_memories(self, bank):
693
        memories = []
694
        for channel in self._channel_numbers_in_bank(bank):
695
            memories.append(self._radio.get_memory(channel))
696

    
697
        return memories
698

    
699
    def get_memory_mappings(self, memory):
700
        banks = []
701
        for bank in self.get_mappings():
702
            if memory.number in self._channel_numbers_in_bank(bank):
703
                banks.append(bank)
704

    
705
        return banks
706

    
707

    
708
# Note: other radios like FTM3200Radio subclass this radio
709
@directory.register
710
class FT1Radio(yaesu_clone.YaesuCloneModeRadio):
711
    """Yaesu FT1DR"""
712
    BAUD_RATE = 38400
713
    VENDOR = "Yaesu"
714
    MODEL = "FT-1D"
715
    VARIANT = "R"
716
    FORMATS = [directory.register_format('FT1D ADMS-6', '*.ft1d')]
717
    class_specials = SPECIALS
718
    _model = b"AH44M"
719
    _memsize = 130507
720
    _block_lengths = [10, 130497]
721
    _block_size = 32
722
    MAX_MEM_SLOT = 900
723
    _mem_params = {
724
         "memnum": 900,            # size of memories array
725
         "flgnum": 900,            # size of flags array
726
         "dtmadd": 0xe4a,          # location of DTMF storage
727
    }
728
    _has_vibrate = False
729
    _has_af_dual = True
730
    _adms_ext = '.ft1d'
731

    
732
    _SG_RE = re.compile(r"(?P<sign>[-+NESW]?)(?P<d>[\d]+)[\s\.,]*"
733
                        r"(?P<m>[\d]*)[\s\']*(?P<s>[\d]*)")
734

    
735
    _RX_BAUD = ("off", "1200 baud", "9600 baud")
736
    _TX_DELAY = ("100ms", "150ms", "200ms", "250ms", "300ms",
737
                 "400ms", "500ms", "750ms", "1000ms")
738
    _WIND_UNITS = ("m/s", "mph")
739
    _RAIN_UNITS = ("mm", "inch")
740
    _TEMP_UNITS = ("C", "F")
741
    _ALT_UNITS = ("m", "ft")
742
    _DIST_UNITS = ("km", "mile")
743
    _POS_UNITS = ("dd.mmmm'", "dd mm'ss\"")
744
    _SPEED_UNITS = ("km/h", "knot", "mph")
745
    _TIME_SOURCE = ("manual", "GPS")
746
    _TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30",
747
           "-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30",
748
           "-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30",
749
           "-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30",
750
           "-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30",
751
           "+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30",
752
           "+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30",
753
           "+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30",
754
           "+11:00", "+11:30")
755
    _BEACON_TYPE = ("Off", "Interval", "SmartBeaconing")
756
    _SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3")
757
    _BEACON_INT = ("30s", "1m", "2m", "3m", "5m", "10m", "15m",
758
                   "20m", "30m", "60m")
759
    _DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4",
760
                   "Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8")
761
    _MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2",
762
                        "Message Group 3", "Message Group 4",
763
                        "Message Group 5", "Message Group 6",
764
                        "Message Group 7", "Message Group 8")
765
    _POSITIONS = ("GPS", "Manual Latitude/Longitude",
766
                  "Manual Latitude/Longitude", "P1", "P2", "P3", "P4",
767
                  "P5", "P6", "P7", "P8", "P9")
768
    _FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds",
769
              "10 seconds", "20 seconds", "30 seconds", "60 seconds",
770
              "CONTINUOUS", "every 2 seconds", "every 3 seconds",
771
              "every 4 seconds", "every 5 seconds", "every 6 seconds",
772
              "every 7 seconds", "every 8 seconds", "every 9 seconds",
773
              "every 10 seconds", "every 20 seconds", "every 30 seconds",
774
              "every 40 seconds", "every 50 seconds", "every minute",
775
              "every 2 minutes", "every 3 minutes", "every 4 minutes",
776
              "every 5 minutes", "every 6 minutes", "every 7 minutes",
777
              "every 8 minutes", "every 9 minutes", "every 10 minutes")
778
    _BEEP_SELECT = ("Off", "Key+Scan", "Key")
779
    _SQUELCH = ["%d" % x for x in range(0, 16)]
780
    _VOLUME = ["%d" % x for x in range(0, 33)]
781
    _OPENING_MESSAGE = ("Off", "DC", "Message", "Normal")
782
    _SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \
783
                   ["Busy", "Hold"]
784
    _SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \
785
                    ["%.1fs" % (0.5 * x) for x in range(2, 21)]
786
    _LAMP_KEY = ["Key %d sec" % x
787
                 for x in range(2, 11)] + ["Continuous", "OFF"]
788
    _LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)]
789
    _LCD_DIMMER = ["Level %d" % x for x in range(1, 7)]
790
    _TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)]
791
    _OFF_ON = ("Off", "On")
792
    _VOL_MODE = ("Normal", "Auto Back")
793
    _DTMF_MODE = ("Manual", "Auto")
794
    _DTMF_SPEED = ("50ms", "100ms")
795
    _DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms")
796
    _MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected")
797
    _BACKTRACK_STATUS = ("Valid", "Invalid")
798
    _NS_HEMI = ("N", "S")
799
    _WE_HEMI = ("W", "E")
800
    _APRS_HIGH_SPEED_MAX = 70
801
    _MIC_GAIN = ("Level %d" % i for i in range(1, 10))
802
    _AMS_TX_MODE = ("TX Auto", "TX DIGITAL", "TX FM")
803
    _VW_MODE = ("On", "Off")
804
    _DIG_POP_UP = ("Off", "2sec", "4sec", "6sec", "8sec", "10sec",
805
                   "20sec", "30sec", "60sec", "Continuous")
806
    _STANDBY_BEEP = ("On", "Off")
807
    _ON_OFF = ("On", "Off")
808
    _TEMP_CF = ("Centigrade", "Fahrenheit")
809
    _APO_SELECT = ("Off", "0.5H", "1.0H", "1.5H", "2.0H", "2.5H",
810
                   "3.0H", "3.5H", "4.0H", "4.5H", "5.0H",
811
                   "5.5H", "6.0H", "6.5H", "7.0H", "7.5H", "8.0H",
812
                   "8.5H", "9.0H", "9.5H", "10.0H", "10.5H",
813
                   "11.0H", "11.5H", "12.0H")
814
    _MONI_TCALL = ("Monitor", "Tone-CALL")
815
    _HOME_REV = ("Home", "Reverse")
816
    _LOCK = ("KEY", "DIAL", "Key+Dial", "PTT", "Key+PTT", "Dial+PTT", "ALL")
817
    _PTT_DELAY = ("Off", "20 ms", "50 ms", "100 ms", "200 ms")
818
    _BEEP_LEVEL = ("Level %i" % i for i in range(1, 7))
819
    _SET_MODE = ("Level %i" % i for i in range(1, 8))
820
    _RX_SAVE = ("OFF", "0.2s", ".3s", ".4s", ".5s", ".6s",
821
                ".7s", ".8s", ".9s", "1.0s", "1.5s",
822
                "2.0s", "2.5s", "3.0s", "3.5s", "4.0s", "4.5s",
823
                "5.0s", "5.5s", "6.0s", "6.5s", "7.0s",
824
                "7.5s", "8.0s", "8.5s", "9.0s", "10.0s", "15s",
825
                "20s", "25s", "30s", "35s", "40s", "45s", "50s", "55s",
826
                "60s")
827
    _VFO_MODE = ("ALL", "BAND")
828
    _VFO_SCAN_MODE = ("BAND", "ALL")
829
    _MEMORY_SCAN_MODE = ("BAND", "ALL")
830

    
831
    _SG_RE = re.compile(r"(?P<sign>[-+NESW]?)(?P<d>[\d]+)[\s\.,]*"
832
                        "(?P<m>[\d]*)[\s\']*(?P<s>[\d]*)")
833

    
834
    _RX_BAUD = ("off", "1200 baud", "9600 baud")
835
    _TX_DELAY = ("100ms", "150ms", "200ms", "250ms", "300ms",
836
                 "400ms", "500ms", "750ms", "1000ms")
837
    _WIND_UNITS = ("m/s", "mph")
838
    _RAIN_UNITS = ("mm", "inch")
839
    _TEMP_UNITS = ("C", "F")
840
    _ALT_UNITS = ("m", "ft")
841
    _DIST_UNITS = ("km", "mile")
842
    _POS_UNITS = ("dd.mmmm'", "dd mm'ss\"")
843
    _SPEED_UNITS = ("km/h", "knot", "mph")
844
    _TIME_SOURCE = ("manual", "GPS")
845
    _TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30",
846
           "-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30",
847
           "-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30",
848
           "-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30",
849
           "-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30",
850
           "+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30",
851
           "+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30",
852
           "+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30",
853
           "+11:00", "+11:30")
854
    _BEACON_TYPE = ("Off", "Interval", "SmartBeaconing")
855
    _SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3")
856
    _BEACON_INT = ("30s", "1m", "2m", "3m", "5m", "10m", "15m",
857
                   "20m", "30m", "60m")
858
    _DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4",
859
                   "Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8")
860
    _MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2",
861
                        "Message Group 3", "Message Group 4",
862
                        "Message Group 5", "Message Group 6",
863
                        "Message Group 7", "Message Group 8")
864
    _POSITIONS = ("GPS", "Manual Latitude/Longitude",
865
                  "Manual Latitude/Longitude", "P1", "P2", "P3", "P4",
866
                  "P5", "P6", "P7", "P8", "P9")
867
    _FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds",
868
              "10 seconds", "20 seconds", "30 seconds", "60 seconds",
869
              "CONTINUOUS", "every 2 seconds", "every 3 seconds",
870
              "every 4 seconds", "every 5 seconds", "every 6 seconds",
871
              "every 7 seconds", "every 8 seconds", "every 9 seconds",
872
              "every 10 seconds", "every 20 seconds", "every 30 seconds",
873
              "every 40 seconds", "every 50 seconds", "every minute",
874
              "every 2 minutes", "every 3 minutes", "every 4 minutes",
875
              "every 5 minutes", "every 6 minutes", "every 7 minutes",
876
              "every 8 minutes", "every 9 minutes", "every 10 minutes")
877
    _BEEP_SELECT = ("Off", "Key+Scan", "Key")
878
    _SQUELCH = ["%d" % x for x in range(0, 16)]
879
    _VOLUME = ["%d" % x for x in range(0, 33)]
880
    _DG_ID = ["%d" % x for x in range(0, 100)]
881
    _GM_RING = ("OFF", "IN RING", "AlWAYS")
882
    _GM_INTERVAL = ("LONG", "NORMAL", "OFF")
883

    
884
    _MYCALL_CHR_SET = list(string.ascii_uppercase) + \
885
        list(string.digits) + ['-', '/']
886

    
887
    @classmethod
888
    def match_model(cls, filedata, filename):
889
        if filename.endswith(cls._adms_ext):
890
            return True
891
        else:
892
            return super().match_model(filedata, filename)
893

    
894
    @classmethod
895
    def get_prompts(cls):
896
        rp = chirp_common.RadioPrompts()
897
        rp.pre_download = _(
898
            "1. Turn radio off.\n"
899
            "2. Connect cable to DATA terminal.\n"
900
            "3. Press and hold in the [F] key while turning the radio on\n"
901
            "     (\"CLONE\" will appear on the display).\n"
902
            "4. <b>After clicking OK</b>, press the [BAND] key to send"
903
            " image.\n")
904
        rp.pre_upload = _(
905
            "1. Turn radio off.\n"
906
            "2. Connect cable to DATA terminal.\n"
907
            "3. Press and hold in the [F] key while turning the radio on\n"
908
            "     (\"CLONE\" will appear on the display).\n"
909
            "4. Press the [Dx] key (\"-WAIT-\" will appear on the LCD).\n")
910
        return rp
911

    
912
    def process_mmap(self):
913
        mem_format = MEM_SETTINGS_FORMAT + MEM_FORMAT + MEM_APRS_FORMAT + \
914
                MEM_BACKTRACK_FORMAT + MEM_GM_FORMAT + MEM_CHECKSUM_FORMAT
915
        self._memobj = bitwise.parse(mem_format % self._mem_params, self._mmap)
916

    
917
    def get_features(self):
918
        rf = chirp_common.RadioFeatures()
919
        rf.has_dtcs_polarity = False
920
        rf.valid_modes = list(MODES) + ['NFM', 'DN']
921
        rf.valid_tmodes = list(TMODES)
922
        rf.valid_duplexes = list(DUPLEX)
923
        rf.valid_tuning_steps = list(STEPS)
924
        rf.valid_bands = [(500000, 999900000)]
925
        rf.valid_skips = SKIPS
926
        rf.valid_power_levels = POWER_LEVELS
927
        rf.valid_characters = "".join(CHARSET)
928
        rf.valid_name_length = 16
929
        rf.memory_bounds = (1, 900)
930
        rf.can_odd_split = True
931
        rf.has_ctone = False
932
        rf.has_bank_names = True
933
        rf.has_settings = True
934
        rf.valid_special_chans = [name for s in SPECIALS for name in s[1]]
935
        return rf
936

    
937
    def get_raw_memory(self, number):
938
        return "\n".join([repr(self._memobj.memory[number - 1]),
939
                          repr(self._memobj.flag[number - 1])])
940

    
941
    def _checksums(self):
942
        return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
943
                yaesu_clone.YaesuChecksum(0x06CA, 0x0748),
944
                yaesu_clone.YaesuChecksum(0x074A, 0x07C8),
945
                yaesu_clone.YaesuChecksum(0x07CA, 0x0848),
946
                yaesu_clone.YaesuChecksum(0x0000, 0x1FDC9)]
947

    
948
    @staticmethod
949
    def _add_ff_pad(val, length):
950
        return val.ljust(length, b"\xFF")[:length]
951

    
952
    @classmethod
953
    def _strip_ff_pads(cls, messages):
954
        result = []
955
        for msg_text in messages:
956
            result.append(str(msg_text).rstrip("\xFF"))
957
        return result
958

    
959
#   Called with a "memref" index to CHIRP memory (int or str)
960
#   and optionally with a "extref" extended name.
961
#   Find and return the corresponding memobj
962
#   Returns:
963
#       Corresponding radio memory object
964
#       Corresponding radio alag structure (if any)
965
#       index into the specific memory object structure (int) ndx
966
#       overall index into memory & specials (int) num
967
#       an indicator of the specific radio object structure (str)
968
    def slotloc(self, memref, extref=None):
969
        array = None
970
        num = memref
971
        ename = ""
972
        mstr = isinstance(memref, str)
973
        specials = ALLNAMES
974
        extr = False
975
        if extref is not None:
976
            extr = extref in specials
977
        if mstr or extr:        # named special?
978
            ename = memref
979
            num = self.MAX_MEM_SLOT + 1
980
            # num = -1
981
            sname = memref if mstr else extref
982
            # Find name of appropriate memory and index into that memory
983
            for x in self.class_specials:
984
                try:
985
                    ndx = x[1].index(sname)
986
                    array = x[0]
987
                    break
988
                except Exception:
989
                    num += len(x[1])
990
                    # num -= len(x[1])
991
            if array is None:
992
                raise IndexError("Unknown special %s", memref)
993
            num += ndx
994
            # num -= ndx
995
        elif memref > self.MAX_MEM_SLOT:         # numbered special
996
            ename = extref
997
            ndx = memref - (self.MAX_MEM_SLOT + 1)
998
            # Find name of appropriate memory and index into that memory
999
            # But assumes specials are in reverse-quantity order
1000
            for x in self.class_specials:
1001
                if ndx < len(x[1]):
1002
                    array = x[0]
1003
                    break
1004
                ndx -= len(x[1])
1005
            if array is None:
1006
                raise IndexError("Unknown memref number %s", memref)
1007
        else:
1008
            array = "memory"
1009
            ndx = memref - 1
1010
            _flag = self._memobj.flag[ndx]
1011
        if array == "Skip":
1012
            _flag = self._memobj.flagskp[ndx]
1013
        elif array == "PMS":
1014
            _flag = self._memobj.flagPMS[ndx]
1015
        elif array == "Home":
1016
            _flag = None
1017
        _mem = getattr(self._memobj, array)[ndx]
1018
        return (_mem, _flag,  ndx, num, array, ename)
1019

    
1020
    # Build CHIRP version (mem) of radio's memory (_mem)
1021
    def get_memory(self, number):
1022
        _mem, _flag, ndx, num, array, ename = self.slotloc(number)
1023
        mem = chirp_common.Memory()
1024
        mem.number = num
1025
        if array == "Home":
1026
            mem.empty = False
1027
            mem.extd_number = ename
1028
            mem.name = self._decode_label(_mem)
1029
            mem.immutable += ["empty", "number", "extd_number", "skip"]
1030
        elif array != "memory":
1031
            mem.extd_number = ename
1032
            mem.immutable += ["name", "extd_number"]
1033
        else:
1034
            mem.name = self._decode_label(_mem)
1035

    
1036
        if _flag is not None:
1037
            mem.skip = _flag.pskip and "P" or _flag.skip and "S" or ""
1038
            mem.empty = False
1039
            if not _flag.used:
1040
                mem.empty = True
1041
            if not _flag.valid:
1042
                mem.empty = True
1043
        if mem.empty:
1044
            mem.freq = 0
1045
            mem.offset = 0
1046
            mem.duplex = ""
1047
            mem.power = POWER_LEVELS[0]
1048
            mem.mode = "FM"
1049
            mem.tuning_step = 0
1050
        else:
1051
            mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
1052
            mem.offset = int(_mem.offset) * 1000
1053
            mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
1054
            self._get_tmode(mem, _mem)
1055
            if mem.duplex is None:
1056
                mem.duplex = DUPLEX[""]
1057
            else:
1058
                mem.duplex = DUPLEX[_mem.duplex]
1059
            if mem.duplex == "split":
1060
                mem.offset = chirp_common.fix_rounded_step(mem.offset)
1061
            mem.mode = self._decode_mode(_mem)
1062
            mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
1063
            mem.tuning_step = STEPS[_mem.tune_step]
1064
            mem.power = self._decode_power_level(_mem)
1065
            self._get_mem_extra(mem, _mem)
1066
        return mem
1067

    
1068
    def _get_mem_extra(self, mem, _mem):
1069
        mem.extra = RadioSettingGroup('Extra', 'extra')
1070

    
1071
        ams = _mem.digmode == 1
1072
        rs = RadioSetting('ysf_ams', 'AMS mode',
1073
                          RadioSettingValueBoolean(ams))
1074
        mem.extra.append(rs)
1075

    
1076
    def _set_mem_extra(self, mem, _mem):
1077
        if 'ysf_ams' in mem.extra:
1078
            # We only set AMS if the memory mode is DN. If it is FM,
1079
            # then that takes precedence as "analog-only".
1080
            if mem.mode == 'DN':
1081
                orig = int(_mem.digmode)
1082
                _mem.digmode = int(mem.extra['ysf_ams'].value) and 1 or 2
1083
                LOG.debug('Changed digmode from %i to %i' % (
1084
                    orig, _mem.digmode))
1085

    
1086
    def _decode_label(self, mem):
1087
        charset = ''.join(CHARSET).ljust(256, '.')
1088
        return str(mem.label).rstrip("\xFF").translate(charset)
1089

    
1090
    def _encode_label(self, mem):
1091
        label = "".join([chr(CHARSET.index(x))
1092
                         for x in mem.name.rstrip()]).encode()
1093
        return self._add_ff_pad(label, 16)
1094

    
1095
    def _encode_charsetbits(self, mem):
1096
        # We only speak English here in chirpville
1097
        return 0x0000
1098

    
1099
    def _decode_power_level(self, mem):
1100
        return POWER_LEVELS[3 - mem.power]
1101

    
1102
    def _encode_power_level(self, mem):
1103
        if mem.power is None:
1104
            return 3        # Choose lowest power
1105
        else:
1106
            return 3 - POWER_LEVELS.index(mem.power)
1107

    
1108
    def _decode_mode(self, mem):
1109
        mode = MODES[mem.mode]
1110
        if mode == 'FM' and int(mem.digmode):
1111
            # DN mode is FM with a digital flag. Since digmode can be AMS or
1112
            # DN, either will be called 'DN' in chirp
1113
            return 'DN'
1114
        if mode == 'FM' and int(mem.mode_alt):
1115
            return 'NFM'
1116
        else:
1117
            return mode
1118

    
1119
    def _encode_mode(self, mem):
1120
        mode = mem.mode
1121
        if mode == 'NFM':
1122
            # Narrow is handled by a separate flag
1123
            mode = 'FM'
1124
        elif mode == 'DN':
1125
            # DN mode is FM with a digital flag
1126
            mode = 'FM'
1127
        return MODES.index(mode)
1128

    
1129
    def _get_tmode(self, mem, _mem):
1130
        mem.tmode = TMODES[_mem.tone_mode]
1131

    
1132
    def _set_tmode(self, _mem, mem):
1133
        _mem.tone_mode = TMODES.index(mem.tmode)
1134

    
1135
    def _set_mode(self, _mem, mem):
1136
        _mem.mode_alt = mem.mode == 'NFM'
1137
        if mem.mode == 'DN' and int(_mem.digmode) == 0:
1138
            # If we are going to DN mode, default to AMS
1139
            LOG.debug('New mode DN, setting AMS')
1140
            if 'ysf_ams' not in mem.extra:
1141
                _mem.digmode = 1
1142
            else:
1143
                # If we have the extra setting, use that, since it will be
1144
                # applied at the end of set_memory() and we want it to apply
1145
                # the right value.
1146
                mem.extra['ysf_ams'].value = True
1147
        elif mem.mode == 'FM' and int(_mem.digmode):
1148
            # If we are going back to FM, that means analog-only, so AMS
1149
            # is disabled
1150
            LOG.debug('New mode FM, disabling AMS')
1151
            _mem.digmode = 0
1152
        _mem.mode = self._encode_mode(mem)
1153
        bm = self.get_bank_model()
1154
        for bank in bm.get_memory_mappings(mem):
1155
            bm.remove_memory_from_mapping(mem, bank)
1156

    
1157
    def _debank(self, mem):
1158
        bm = self.get_bank_model()
1159
        for bank in bm.get_memory_mappings(mem):
1160
            bm.remove_memory_from_mapping(mem, bank)
1161

    
1162
    def validate_memory(self, mem):
1163
        # Only check the home registers for appropriate bands
1164
        msgs = super().validate_memory(mem)
1165
        ndx = mem.number - ALLNAMES.index("Home1") - self.MAX_MEM_SLOT - 1
1166
        if 10 >= ndx >= 0:
1167
            f = VALID_BANDS[ndx]
1168
            if not(f[0] < mem.freq < f[1]):
1169
                msgs.append(chirp_common.ValidationError(
1170
                            "Frequency outside of band for Home%2d" %
1171
                            (ndx + 1)))
1172
        return msgs
1173

    
1174
    # Modify radio's memory (_mem) corresponding to CHIRP version at 'mem'
1175
    def set_memory(self, mem):
1176
        _mem, flag, ndx, num, regtype, ename = self.slotloc(mem.number,
1177
                                                            mem.extd_number)
1178
        if mem.empty:
1179
            self._wipe_memory(_mem)
1180
            if flag is not None:
1181
                flag.used = False
1182
            return
1183
        _mem.power = self._encode_power_level(mem)
1184
        _mem.tone = chirp_common.TONES.index(mem.rtone)
1185
        self._set_tmode(_mem, mem)
1186
        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
1187
        _mem.tune_step = STEPS.index(mem.tuning_step)
1188
        # duplex "off" is equivalent to "" and may show up in tox test.
1189
        if mem.duplex is None:
1190
            _mem.duplex = DUPLEX.index("")
1191
        else:
1192
            _mem.duplex = DUPLEX.index(mem.duplex)
1193
        self._set_mode(_mem, mem)
1194
        if flag is not None:
1195
            if mem.freq < 30000000 or \
1196
                    (mem.freq > 88000000 and mem.freq < 108000000) or \
1197
                    mem.freq > 580000000:
1198
                flag.nosubvfo = True     # Masked from VFO B
1199
            else:
1200
                flag.nosubvfo = False    # Available in both VFOs
1201
        if regtype != "Home":
1202
            self._debank(mem)
1203
            ndx = num - 1
1204
            flag.used = not mem.empty
1205
            flag.valid = True
1206
            flag.skip = mem.skip == "S"
1207
            flag.pskip = mem.skip == "P"
1208
        freq = mem.freq
1209
        _mem.freq = int(freq / 1000)
1210
        _mem.offset = int(mem.offset / 1000)
1211
        _mem.label = self._encode_label(mem)
1212
        _mem.charsetbits = self._encode_charsetbits(mem)
1213
        self._set_mem_extra(mem, _mem)
1214
        return
1215

    
1216
    @classmethod
1217
    def _wipe_memory(cls, mem):
1218
        mem.set_raw("\x00" * (mem.size() // 8))
1219
        mem.unknown1 = 0x05
1220

    
1221
    def get_bank_model(self):
1222
        return FT1BankModel(self)
1223

    
1224
    @classmethod
1225
    def _digi_path_to_str(cls, path):
1226
        path_cmp = []
1227
        for entry in path.entry:
1228
            callsign = str(entry.callsign).rstrip("\xFF")
1229
            if not callsign:
1230
                break
1231
            path_cmp.append("%s-%d" % (callsign, entry.ssid))
1232
        return ",".join(path_cmp)
1233

    
1234
    @staticmethod
1235
    def _latlong_sanity(sign, l_d, l_m, l_s, is_lat):
1236
        if sign not in (0, 1):
1237
            sign = 0
1238
        if is_lat:
1239
            d_max = 90
1240
        else:
1241
            d_max = 180
1242
        if l_d < 0 or l_d > d_max:
1243
            l_d = 0
1244
            l_m = 0
1245
            l_s = 0
1246
        if l_m < 0 or l_m > 60:
1247
            l_m = 0
1248
            l_s = 0
1249
        if l_s < 0 or l_s > 60:
1250
            l_s = 0
1251
        return sign, l_d, l_m, l_s
1252

    
1253
    @classmethod
1254
    def _latlong_to_str(cls, sign, l_d, l_m, l_s, is_lat, to_sexigesimal=True):
1255
        sign, l_d, l_m, l_s = cls._latlong_sanity(sign, l_d, l_m, l_s, is_lat)
1256
        mult = sign and -1 or 1
1257
        if to_sexigesimal:
1258
            return "%d,%d'%d\"" % (mult * l_d, l_m, l_s)
1259
        return "%0.5f" % (mult * l_d + (l_m / 60.0) + (l_s / (60.0 * 60.0)))
1260

    
1261
    @classmethod
1262
    def _str_to_latlong(cls, lat_long, is_lat):
1263
        sign = 0
1264
        result = [0, 0, 0]
1265

    
1266
        lat_long = lat_long.strip()
1267

    
1268
        if not lat_long:
1269
            return 1, 0, 0, 0
1270

    
1271
        try:
1272
            # DD.MMMMM is the simple case, try that first.
1273
            val = float(lat_long)
1274
            if val < 0:
1275
                sign = 1
1276
            val = abs(val)
1277
            result[0] = int(val)
1278
            result[1] = int(val * 60) % 60
1279
            result[2] = int(val * 3600) % 60
1280
        except ValueError:
1281
            # Try DD MM'SS" if DD.MMMMM failed.
1282
            match = cls._SG_RE.match(lat_long.strip())
1283
            if match:
1284
                if match.group("sign") and (match.group("sign") in "SE-"):
1285
                    sign = 1
1286
                else:
1287
                    sign = 0
1288
                if match.group("d"):
1289
                    result[0] = int(match.group("d"))
1290
                if match.group("m"):
1291
                    result[1] = int(match.group("m"))
1292
                if match.group("s"):
1293
                    result[2] = int(match.group("s"))
1294
            elif len(lat_long) > 4:
1295
                raise Exception("Lat/Long should be DD MM'SS\" or DD.MMMMM")
1296

    
1297
        return cls._latlong_sanity(sign, result[0], result[1], result[2],
1298
                                   is_lat)
1299

    
1300
    def _get_aprs_settings(self):
1301
        menu = RadioSettingGroup("aprs_top", "APRS")
1302
        menu.append(self._get_aprs_general_settings())
1303
        menu.append(self._get_aprs_rx_settings())
1304
        menu.append(self._get_aprs_tx_settings())
1305
        menu.append(self._get_aprs_smartbeacon())
1306
        menu.append(self._get_aprs_msgs())
1307
        menu.append(self._get_aprs_beacons())
1308
        return menu
1309

    
1310
    def _get_aprs_general_settings(self):
1311
        menu = RadioSettingGroup("aprs_general", "APRS General")
1312
        aprs = self._memobj.aprs
1313

    
1314
        val = RadioSettingValueString(
1315
            0, 6, str(aprs.my_callsign.callsign).rstrip("\xFF"))
1316
        rs = RadioSetting("aprs.my_callsign.callsign", "My Callsign", val)
1317
        rs.set_apply_callback(self.apply_callsign, aprs.my_callsign)
1318
        menu.append(rs)
1319

    
1320
        val = RadioSettingValueList(
1321
            chirp_common.APRS_SSID,
1322
            chirp_common.APRS_SSID[aprs.my_callsign.ssid])
1323
        rs = RadioSetting("aprs.my_callsign.ssid", "My SSID", val)
1324
        menu.append(rs)
1325

    
1326
        val = RadioSettingValueList(self._MY_SYMBOL,
1327
                                    self._MY_SYMBOL[aprs.selected_my_symbol])
1328
        rs = RadioSetting("aprs.selected_my_symbol", "My Symbol", val)
1329
        menu.append(rs)
1330

    
1331
        symbols = list(chirp_common.APRS_SYMBOLS)
1332
        selected = aprs.custom_symbol
1333
        if aprs.custom_symbol >= len(chirp_common.APRS_SYMBOLS):
1334
            symbols.append("%d" % aprs.custom_symbol)
1335
            selected = len(symbols) - 1
1336
        val = RadioSettingValueList(symbols, symbols[selected])
1337
        rs = RadioSetting("aprs.custom_symbol_text", "User Selected Symbol",
1338
                          val)
1339
        rs.set_apply_callback(self.apply_custom_symbol, aprs)
1340
        menu.append(rs)
1341

    
1342
        val = RadioSettingValueList(
1343
            chirp_common.APRS_POSITION_COMMENT,
1344
            chirp_common.APRS_POSITION_COMMENT[aprs.selected_position_comment])
1345
        rs = RadioSetting("aprs.selected_position_comment", "Position Comment",
1346
                          val)
1347
        menu.append(rs)
1348

    
1349
        latitude = self._latlong_to_str(aprs.latitude_sign,
1350
                                        aprs.latitude_degree,
1351
                                        aprs.latitude_minute,
1352
                                        aprs.latitude_second,
1353
                                        True, aprs.aprs_units_position_mmss)
1354
        longitude = self._latlong_to_str(aprs.longitude_sign,
1355
                                         aprs.longitude_degree,
1356
                                         aprs.longitude_minute,
1357
                                         aprs.longitude_second,
1358
                                         False, aprs.aprs_units_position_mmss)
1359

    
1360
        # TODO: Rebuild this when aprs_units_position_mmss changes.
1361
        # TODO: Rebuild this when latitude/longitude change.
1362
        # TODO: Add saved positions p1 - p10 to memory map.
1363
        position_str = list(self._POSITIONS)
1364
        # position_str[1] = "%s %s" % (latitude, longitude)
1365
        # position_str[2] = "%s %s" % (latitude, longitude)
1366
        val = RadioSettingValueList(position_str,
1367
                                    position_str[aprs.selected_position])
1368
        rs = RadioSetting("aprs.selected_position", "My Position", val)
1369
        menu.append(rs)
1370

    
1371
        val = RadioSettingValueString(0, 10, latitude)
1372
        rs = RadioSetting("latitude", "Manual Latitude", val)
1373
        rs.set_apply_callback(self.apply_lat_long, aprs)
1374
        menu.append(rs)
1375

    
1376
        val = RadioSettingValueString(0, 11, longitude)
1377
        rs = RadioSetting("longitude", "Manual Longitude", val)
1378
        rs.set_apply_callback(self.apply_lat_long, aprs)
1379
        menu.append(rs)
1380

    
1381
        val = RadioSettingValueList(
1382
            self._TIME_SOURCE, self._TIME_SOURCE[aprs.set_time_manually])
1383
        rs = RadioSetting("aprs.set_time_manually", "Time Source", val)
1384
        menu.append(rs)
1385

    
1386
        val = RadioSettingValueList(self._TZ, self._TZ[aprs.timezone])
1387
        rs = RadioSetting("aprs.timezone", "Timezone", val)
1388
        menu.append(rs)
1389

    
1390
        val = RadioSettingValueList(self._SPEED_UNITS,
1391
                                    self._SPEED_UNITS[aprs.aprs_units_speed])
1392
        rs = RadioSetting("aprs.aprs_units_speed", "APRS Speed Units", val)
1393
        menu.append(rs)
1394

    
1395
        val = RadioSettingValueList(self._SPEED_UNITS,
1396
                                    self._SPEED_UNITS[aprs.gps_units_speed])
1397
        rs = RadioSetting("aprs.gps_units_speed", "GPS Speed Units", val)
1398
        menu.append(rs)
1399

    
1400
        val = RadioSettingValueList(
1401
            self._ALT_UNITS, self._ALT_UNITS[aprs.aprs_units_altitude_ft])
1402
        rs = RadioSetting("aprs.aprs_units_altitude_ft", "APRS Altitude Units",
1403
                          val)
1404
        menu.append(rs)
1405

    
1406
        val = RadioSettingValueList(
1407
            self._ALT_UNITS, self._ALT_UNITS[aprs.gps_units_altitude_ft])
1408
        rs = RadioSetting("aprs.gps_units_altitude_ft", "GPS Altitude Units",
1409
                          val)
1410
        menu.append(rs)
1411

    
1412
        val = RadioSettingValueList(
1413
            self._POS_UNITS, self._POS_UNITS[aprs.aprs_units_position_mmss])
1414
        rs = RadioSetting("aprs.aprs_units_position_mmss",
1415
                          "APRS Position Format", val)
1416
        menu.append(rs)
1417

    
1418
        val = RadioSettingValueList(
1419
            self._POS_UNITS, self._POS_UNITS[aprs.gps_units_position_sss])
1420
        rs = RadioSetting("aprs.gps_units_position_sss",
1421
                          "GPS Position Format", val)
1422
        menu.append(rs)
1423

    
1424
        val = RadioSettingValueList(
1425
            self._DIST_UNITS, self._DIST_UNITS[aprs.aprs_units_distance_m])
1426
        rs = RadioSetting("aprs.aprs_units_distance_m", "APRS Distance Units",
1427
                          val)
1428
        menu.append(rs)
1429

    
1430
        val = RadioSettingValueList(self._WIND_UNITS,
1431
                                    self._WIND_UNITS[aprs.aprs_units_wind_mph])
1432
        rs = RadioSetting("aprs.aprs_units_wind_mph", "APRS Wind Speed Units",
1433
                          val)
1434
        menu.append(rs)
1435

    
1436
        val = RadioSettingValueList(
1437
            self._RAIN_UNITS, self._RAIN_UNITS[aprs.aprs_units_rain_inch])
1438
        rs = RadioSetting("aprs.aprs_units_rain_inch", "APRS Rain Units", val)
1439
        menu.append(rs)
1440

    
1441
        val = RadioSettingValueList(
1442
            self._TEMP_UNITS, self._TEMP_UNITS[aprs.aprs_units_temperature_f])
1443
        rs = RadioSetting("aprs.aprs_units_temperature_f",
1444
                          "APRS Temperature Units", val)
1445
        menu.append(rs)
1446

    
1447
        return menu
1448

    
1449
    def _get_aprs_msgs(self):
1450
        menu = RadioSettingGroup("aprs_msg", "APRS Messages")
1451
        aprs_msg = self._memobj.aprs_message_pkt
1452

    
1453
        for index in range(0, 60):
1454
            if aprs_msg[index].flag != 255:
1455
                astring = \
1456
                    str(aprs_msg[index].dst_callsign).partition("\xFF")[0]
1457

    
1458
                val = RadioSettingValueString(
1459
                    0, 9, chirp_common.sanitize_string(astring) +
1460
                    "-%d" % aprs_msg[index].dst_callsign_ssid)
1461
                val.set_mutable(False)
1462
                rs = RadioSetting(
1463
                    "aprs_msg.dst_callsign%d" % index,
1464
                    "Dst Callsign %d" % index, val)
1465
                menu.append(rs)
1466

    
1467
                astring = \
1468
                    str(aprs_msg[index].path_and_body).partition("\xFF")[0]
1469
                val = RadioSettingValueString(
1470
                    0, 66, chirp_common.sanitize_string(astring))
1471
                val.set_mutable(False)
1472
                rs = RadioSetting(
1473
                    "aprs_msg.path_and_body%d" % index, "Body", val)
1474
                menu.append(rs)
1475

    
1476
        return menu
1477

    
1478
    def _get_aprs_beacons(self):
1479
        menu = RadioSettingGroup("aprs_beacons", "APRS Beacons")
1480
        aprs_beacon = self._memobj.aprs_beacon_pkt
1481
        aprs_meta = self._memobj.aprs_beacon_meta
1482

    
1483
        for index in range(0, 60):
1484
            # There is probably a more pythonesque way to do this
1485
            scl = int(aprs_meta[index].sender_callsign[0])
1486
            dcl = int(aprs_beacon[index].dst_callsign[0])
1487
            if scl != 255 and scl != 0:  # ignore if empty send call
1488
                callsign = str(aprs_meta[index].sender_callsign).rstrip("\xFF")
1489
                val = RadioSettingValueString(0, 9, callsign)
1490
                val.set_mutable(False)
1491
                rs = RadioSetting(
1492
                    "aprs_beacon.src_callsign%d" % index,
1493
                    "SRC Callsign %d" % index, val)
1494
                menu.append(rs)
1495

    
1496
                if dcl != 255 and dcl != 0:   # ignore if empty dest call
1497
                    val = str(aprs_beacon[index].dst_callsign)
1498
                    val = RadioSettingValueString(0, 9, val.rstrip("\xFF"))
1499
                    val.set_mutable(False)
1500
                    rs = RadioSetting(
1501
                        "aprs_beacon.dst_callsign%d" % index,
1502
                        "DST Callsign %d" % index, val)
1503
                    menu.append(rs)
1504

    
1505
                date = "%02d/%02d/%02d" % (
1506
                    aprs_meta[index].date[0],
1507
                    aprs_meta[index].date[1],
1508
                    aprs_meta[index].date[2])
1509
                val = RadioSettingValueString(0, 8, date)
1510
                val.set_mutable(False)
1511
                rs = RadioSetting("aprs_beacon.date%d" % index, "Date", val)
1512
                menu.append(rs)
1513

    
1514
                time = "%02d:%02d" % (
1515
                    aprs_meta[index].time[0],
1516
                    aprs_meta[index].time[1])
1517
                val = RadioSettingValueString(0, 5, time)
1518
                val.set_mutable(False)
1519
                rs = RadioSetting("aprs_beacon.time%d" % index, "Time", val)
1520
                menu.append(rs)
1521

    
1522
                if dcl != 255 and dcl != 0:   # ignore if empty dest call
1523
                    path = str(aprs_beacon[index].path).replace("\x00", " ")
1524
                    path = ''.join(c for c in path
1525
                                   if c in string.printable).strip()
1526
                    path = str(path).replace("\xE0", "*")
1527
                    val = RadioSettingValueString(0, 32, path)
1528
                    val.set_mutable(False)
1529
                    rs = RadioSetting(
1530
                     "aprs_beacon.path%d" % index, "Digipath", val)
1531
                    menu.append(rs)
1532

    
1533
                body = str(aprs_beacon[index].body).rstrip("\xFF")
1534
                checksum = body[-2:]
1535
                body = ''.join(s for s in body[:-2]
1536
                               if s in string.printable).translate(
1537
                                   str.maketrans(
1538
                                       "", "", "\x09\x0a\x0b\x0c\x0d"))
1539
                try:
1540
                    val = RadioSettingValueString(0, 134, body.strip())
1541
                except Exception as e:
1542
                    LOG.error("Error in APRS beacon at index %s", index)
1543
                    raise e
1544
                val.set_mutable(False)
1545
                rs = RadioSetting("aprs_beacon.body%d" % index, "Body", val)
1546
                menu.append(rs)
1547

    
1548
        return menu
1549

    
1550
    def _get_aprs_rx_settings(self):
1551
        menu = RadioSettingGroup("aprs_rx", "APRS Receive")
1552
        aprs = self._memobj.aprs
1553

    
1554
        val = RadioSettingValueList(self._RX_BAUD, self._RX_BAUD[aprs.rx_baud])
1555
        rs = RadioSetting("aprs.rx_baud", "Modem RX", val)
1556
        menu.append(rs)
1557

    
1558
        val = RadioSettingValueBoolean(aprs.aprs_mute)
1559
        rs = RadioSetting("aprs.aprs_mute", "APRS Mute", val)
1560
        menu.append(rs)
1561

    
1562
        if self._has_af_dual:
1563
            val = RadioSettingValueBoolean(aprs.af_dual)
1564
            rs = RadioSetting("aprs.af_dual", "AF Dual", val)
1565
            menu.append(rs)
1566

    
1567
        val = RadioSettingValueBoolean(aprs.ring_msg)
1568
        rs = RadioSetting("aprs.ring_msg", "Ring on Message RX", val)
1569
        menu.append(rs)
1570

    
1571
        val = RadioSettingValueBoolean(aprs.ring_beacon)
1572
        rs = RadioSetting("aprs.ring_beacon", "Ring on Beacon RX", val)
1573
        menu.append(rs)
1574

    
1575
        val = RadioSettingValueList(self._FLASH,
1576
                                    self._FLASH[aprs.flash_msg])
1577
        rs = RadioSetting("aprs.flash_msg", "Flash on personal message", val)
1578
        menu.append(rs)
1579

    
1580
        if self._has_vibrate:
1581
            val = RadioSettingValueList(self._FLASH,
1582
                                        self._FLASH[aprs.vibrate_msg])
1583
            rs = RadioSetting("aprs.vibrate_msg",
1584
                              "Vibrate on personal message", val)
1585
            menu.append(rs)
1586

    
1587
        val = RadioSettingValueList(self._FLASH[:10],
1588
                                    self._FLASH[aprs.flash_bln])
1589
        rs = RadioSetting("aprs.flash_bln", "Flash on bulletin message", val)
1590
        menu.append(rs)
1591

    
1592
        if self._has_vibrate:
1593
            val = RadioSettingValueList(self._FLASH[:10],
1594
                                        self._FLASH[aprs.vibrate_bln])
1595
            rs = RadioSetting("aprs.vibrate_bln",
1596
                              "Vibrate on bulletin message", val)
1597
            menu.append(rs)
1598

    
1599
        val = RadioSettingValueList(self._FLASH[:10],
1600
                                    self._FLASH[aprs.flash_grp])
1601
        rs = RadioSetting("aprs.flash_grp", "Flash on group message", val)
1602
        menu.append(rs)
1603

    
1604
        if self._has_vibrate:
1605
            val = RadioSettingValueList(self._FLASH[:10],
1606
                                        self._FLASH[aprs.vibrate_grp])
1607
            rs = RadioSetting("aprs.vibrate_grp",
1608
                              "Vibrate on group message", val)
1609
            menu.append(rs)
1610

    
1611
        filter_val = [m.padded_string for m in aprs.msg_group]
1612
        filter_val = self._strip_ff_pads(filter_val)
1613
        for index, filter_text in enumerate(filter_val):
1614
            val = RadioSettingValueString(0, 9, filter_text)
1615
            rs = RadioSetting("aprs.msg_group_%d" % index,
1616
                              "Message Group %d" % (index + 1), val)
1617
            menu.append(rs)
1618
            rs.set_apply_callback(self.apply_ff_padded_string,
1619
                                  aprs.msg_group[index])
1620
        # TODO: Use filter_val as the list entries and update it on edit.
1621
        val = RadioSettingValueList(
1622
            self._MSG_GROUP_NAMES,
1623
            self._MSG_GROUP_NAMES[aprs.selected_msg_group])
1624
        rs = RadioSetting("aprs.selected_msg_group", "Selected Message Group",
1625
                          val)
1626
        menu.append(rs)
1627

    
1628
        val = RadioSettingValueBoolean(aprs.filter_mic_e)
1629
        rs = RadioSetting("aprs.filter_mic_e", "Receive Mic-E Beacons", val)
1630
        menu.append(rs)
1631

    
1632
        val = RadioSettingValueBoolean(aprs.filter_position)
1633
        rs = RadioSetting("aprs.filter_position", "Receive Position Beacons",
1634
                          val)
1635
        menu.append(rs)
1636

    
1637
        val = RadioSettingValueBoolean(aprs.filter_weather)
1638
        rs = RadioSetting("aprs.filter_weather", "Receive Weather Beacons",
1639
                          val)
1640
        menu.append(rs)
1641

    
1642
        val = RadioSettingValueBoolean(aprs.filter_object)
1643
        rs = RadioSetting("aprs.filter_object", "Receive Object Beacons", val)
1644
        menu.append(rs)
1645

    
1646
        val = RadioSettingValueBoolean(aprs.filter_item)
1647
        rs = RadioSetting("aprs.filter_item", "Receive Item Beacons", val)
1648
        menu.append(rs)
1649

    
1650
        val = RadioSettingValueBoolean(aprs.filter_status)
1651
        rs = RadioSetting("aprs.filter_status", "Receive Status Beacons", val)
1652
        menu.append(rs)
1653

    
1654
        val = RadioSettingValueBoolean(aprs.filter_other)
1655
        rs = RadioSetting("aprs.filter_other", "Receive Other Beacons", val)
1656
        menu.append(rs)
1657

    
1658
        return menu
1659

    
1660
    def _get_aprs_tx_settings(self):
1661
        menu = RadioSettingGroup("aprs_tx", "APRS Transmit")
1662
        aprs = self._memobj.aprs
1663

    
1664
        beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon
1665
        val = RadioSettingValueList(self._BEACON_TYPE,
1666
                                    self._BEACON_TYPE[beacon_type])
1667
        rs = RadioSetting("aprs.transmit", "TX Beacons", val)
1668
        rs.set_apply_callback(self.apply_beacon_type, aprs)
1669
        menu.append(rs)
1670

    
1671
        val = RadioSettingValueList(self._TX_DELAY,
1672
                                    self._TX_DELAY[aprs.tx_delay])
1673
        rs = RadioSetting("aprs.tx_delay", "TX Delay", val)
1674
        menu.append(rs)
1675

    
1676
        val = RadioSettingValueList(self._BEACON_INT,
1677
                                    self._BEACON_INT[aprs.beacon_interval])
1678
        rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val)
1679
        menu.append(rs)
1680

    
1681
        desc = []
1682
        status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt]
1683
        status = self._strip_ff_pads(status)
1684
        for index, msg_text in enumerate(status):
1685
            val = RadioSettingValueString(0, 60, msg_text)
1686
            desc.append("Beacon Status Text %d" % (index + 1))
1687
            rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1],
1688
                              val)
1689
            rs.set_apply_callback(self.apply_ff_padded_string,
1690
                                  self._memobj.aprs_beacon_status_txt[index])
1691
            menu.append(rs)
1692
        val = RadioSettingValueList(desc,
1693
                                    desc[aprs.selected_beacon_status_txt])
1694
        rs = RadioSetting("aprs.selected_beacon_status_txt",
1695
                          "Beacon Status Text", val)
1696
        menu.append(rs)
1697

    
1698
        message_macro = [m.padded_string for m in aprs.message_macro]
1699
        message_macro = self._strip_ff_pads(message_macro)
1700
        for index, msg_text in enumerate(message_macro):
1701
            val = RadioSettingValueString(0, 16, msg_text)
1702
            rs = RadioSetting("aprs.message_macro_%d" % index,
1703
                              "Message Macro %d" % (index + 1), val)
1704
            rs.set_apply_callback(self.apply_ff_padded_string,
1705
                                  aprs.message_macro[index])
1706
            menu.append(rs)
1707

    
1708
        path_str = list(self._DIGI_PATHS)
1709
        path_str[3] = self._digi_path_to_str(aprs.digi_path_3_6[0])
1710
        val = RadioSettingValueString(0, 22, path_str[3])
1711
        rs = RadioSetting("aprs.digi_path_3", "Digi Path 4 (2 entries)", val)
1712
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[0])
1713
        menu.append(rs)
1714

    
1715
        path_str[4] = self._digi_path_to_str(aprs.digi_path_3_6[1])
1716
        val = RadioSettingValueString(0, 22, path_str[4])
1717
        rs = RadioSetting("aprs.digi_path_4", "Digi Path 5 (2 entries)", val)
1718
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[1])
1719
        menu.append(rs)
1720

    
1721
        path_str[5] = self._digi_path_to_str(aprs.digi_path_3_6[2])
1722
        val = RadioSettingValueString(0, 22, path_str[5])
1723
        rs = RadioSetting("aprs.digi_path_5", "Digi Path 6 (2 entries)", val)
1724
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[2])
1725
        menu.append(rs)
1726

    
1727
        path_str[6] = self._digi_path_to_str(aprs.digi_path_3_6[3])
1728
        val = RadioSettingValueString(0, 22, path_str[6])
1729
        rs = RadioSetting("aprs.digi_path_6", "Digi Path 7 (2 entries)", val)
1730
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_3_6[3])
1731
        menu.append(rs)
1732

    
1733
        path_str[7] = self._digi_path_to_str(aprs.digi_path_7)
1734
        val = RadioSettingValueString(0, 88, path_str[7])
1735
        rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val)
1736
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7)
1737
        menu.append(rs)
1738

    
1739
        # Show friendly messages for empty slots rather than blanks.
1740
        # TODO: Rebuild this when digi_path_[34567] change.
1741
        # path_str[3] = path_str[3] or self._DIGI_PATHS[3]
1742
        # path_str[4] = path_str[4] or self._DIGI_PATHS[4]
1743
        # path_str[5] = path_str[5] or self._DIGI_PATHS[5]
1744
        # path_str[6] = path_str[6] or self._DIGI_PATHS[6]
1745
        # path_str[7] = path_str[7] or self._DIGI_PATHS[7]
1746
        path_str[3] = self._DIGI_PATHS[3]
1747
        path_str[4] = self._DIGI_PATHS[4]
1748
        path_str[5] = self._DIGI_PATHS[5]
1749
        path_str[6] = self._DIGI_PATHS[6]
1750
        path_str[7] = self._DIGI_PATHS[7]
1751
        val = RadioSettingValueList(path_str,
1752
                                    path_str[aprs.selected_digi_path])
1753
        rs = RadioSetting("aprs.selected_digi_path", "Selected Digi Path", val)
1754
        menu.append(rs)
1755

    
1756
        return menu
1757

    
1758
    def _get_aprs_smartbeacon(self):
1759
        menu = RadioSettingGroup("aprs_smartbeacon", "APRS SmartBeacon")
1760
        aprs = self._memobj.aprs
1761

    
1762
        val = RadioSettingValueList(
1763
            self._SMARTBEACON_PROFILE,
1764
            self._SMARTBEACON_PROFILE[aprs.active_smartbeaconing])
1765
        rs = RadioSetting("aprs.active_smartbeaconing", "SmartBeacon profile",
1766
                          val)
1767
        menu.append(rs)
1768

    
1769
        for profile in range(3):
1770
            pfx = "type%d" % (profile + 1)
1771
            path = "aprs.smartbeaconing_profile[%d]" % profile
1772
            prof = aprs.smartbeaconing_profile[profile]
1773

    
1774
            low_val = RadioSettingValueInteger(2, 30, prof.low_speed_mph)
1775
            high_val = RadioSettingValueInteger(3, self._APRS_HIGH_SPEED_MAX,
1776
                                                prof.high_speed_mph)
1777
            low_val.get_max = lambda: min(30, int(high_val.get_value()) - 1)
1778

    
1779
            rs = RadioSetting("%s.low_speed_mph" % path,
1780
                              "%s Low Speed (mph)" % pfx, low_val)
1781
            menu.append(rs)
1782

    
1783
            rs = RadioSetting("%s.high_speed_mph" % path,
1784
                              "%s High Speed (mph)" % pfx, high_val)
1785
            menu.append(rs)
1786

    
1787
            val = RadioSettingValueInteger(1, 100, prof.slow_rate_min)
1788
            rs = RadioSetting("%s.slow_rate_min" % path,
1789
                              "%s Slow rate (minutes)" % pfx, val)
1790
            menu.append(rs)
1791

    
1792
            val = RadioSettingValueInteger(10, 180, prof.fast_rate_sec)
1793
            rs = RadioSetting("%s.fast_rate_sec" % path,
1794
                              "%s Fast rate (seconds)" % pfx, val)
1795
            menu.append(rs)
1796

    
1797
            val = RadioSettingValueInteger(5, 90, prof.turn_angle)
1798
            rs = RadioSetting("%s.turn_angle" % path,
1799
                              "%s Turn angle (degrees)" % pfx, val)
1800
            menu.append(rs)
1801

    
1802
            val = RadioSettingValueInteger(1, 255, prof.turn_slop)
1803
            rs = RadioSetting("%s.turn_slop" % path,
1804
                              "%s Turn slop" % pfx, val)
1805
            menu.append(rs)
1806

    
1807
            val = RadioSettingValueInteger(5, 180, prof.turn_time_sec)
1808
            rs = RadioSetting("%s.turn_time_sec" % path,
1809
                              "%s Turn time (seconds)" % pfx, val)
1810
            menu.append(rs)
1811

    
1812
        return menu
1813

    
1814
    def _get_digital_settings(self):
1815
        topmenu = RadioSettingGroup("digital_settings", "Digital")
1816
        menu = RadioSettingGroup("settings", "Digital Modes")
1817
        topmenu.append(menu)
1818
        GMmenu = RadioSettingGroup("first_settings", "Group Monitor(GM)")
1819
        topmenu.append(GMmenu)
1820
        WXmenu = RadioSettingGroup("WiresX_settings", "Wires-X")
1821
        topmenu.append(WXmenu)
1822

    
1823
        # MYCALL
1824
        mycall = self._memobj.my_call
1825
        mycallstr = str(mycall.callsign).rstrip("\xff").rstrip()
1826
        mycalle = RadioSettingValueString(0, 10, mycallstr, False,
1827
                                          charset=self._MYCALL_CHR_SET)
1828
        rs = RadioSetting('mycall.callsign',
1829
                          'MYCALL (10 uppercase chars)', mycalle)
1830
        rs.set_apply_callback(self.apply_mycall, mycall)
1831
        menu.append(rs)
1832

    
1833
        # Short Press AMS button AMS TX Mode
1834
        digital_settings = self._memobj.digital_settings
1835
        val = RadioSettingValueList(
1836
            self._AMS_TX_MODE,
1837
            self._AMS_TX_MODE[digital_settings.ams_tx_mode])
1838
        rs = RadioSetting("digital_settings.ams_tx_mode",
1839
                          "AMS TX Mode", val)
1840
        menu.append(rs)
1841

    
1842
        # 16 DIG VW  Turn the VW mode selection ON or OFF.
1843
        val = RadioSettingValueList(
1844
            self._VW_MODE,
1845
            self._VW_MODE[digital_settings.vw_mode])
1846
        rs = RadioSetting("digital_settings.vw_mode", "VW Mode", val)
1847
        menu.append(rs)
1848

    
1849
        # TX DG-ID Long Press Mode Key, Dial
1850
        val = RadioSettingValueList(
1851
            self._DG_ID,
1852
            self._DG_ID[digital_settings.tx_dg_id])
1853
        rs = RadioSetting("digital_settings.tx_dg_id",
1854
                          "TX DG-ID", val)
1855
        menu.append(rs)
1856

    
1857
        # RX DG-ID Long Press Mode Key, Mode Key to select, Dial
1858
        val = RadioSettingValueList(
1859
            self._DG_ID,
1860
            self._DG_ID[digital_settings.rx_dg_id])
1861
        rs = RadioSetting("digital_settings.rx_dg_id",
1862
                          "RX DG-ID", val)
1863
        menu.append(rs)
1864

    
1865
        # 15 DIG.POP    Call sign display pop up time
1866
        digital_settings_more = self._memobj.digital_settings_more
1867

    
1868
        val = RadioSettingValueList(
1869
            self._DIG_POP_UP,
1870
            self._DIG_POP_UP[
1871
                0 if digital_settings_more.digital_popup == 0
1872
                else digital_settings_more.digital_popup - 9])
1873

    
1874
        rs = RadioSetting("digital_settings_more.digital_popup",
1875
                          "Digital Popup", val)
1876
        rs.set_apply_callback(self.apply_digital_popup,
1877
                              digital_settings_more)
1878
        menu.append(rs)
1879

    
1880
        # 07  BEP.STB    Standby Beep in the digital C4FM mode. On/Off
1881
        val = RadioSettingValueList(
1882
            self._STANDBY_BEEP,
1883
            self._STANDBY_BEEP[digital_settings.standby_beep])
1884
        rs = RadioSetting("digital_settings.standby_beep",
1885
                          "Standby Beep", val)
1886
        menu.append(rs)
1887

    
1888
        # GM settings
1889
        # 24 GM RNG Select the beep option
1890
        first_settings = self._memobj.first_settings
1891
        val = RadioSettingValueList(
1892
            self._GM_RING,
1893
            self._GM_RING[first_settings.gm_ring])
1894
        rs = RadioSetting("first_settings.gm_ring", "GM Ring", val)
1895
        GMmenu.append(rs)
1896

    
1897
        # 25 GM INT transmission interval of digital GM info
1898
        scan_settings = self._memobj.scan_settings
1899
        val = RadioSettingValueList(
1900
            self._GM_INTERVAL,
1901
            self._GM_INTERVAL[scan_settings.gm_interval])
1902
        rs = RadioSetting("scan_settings.gm_interval",
1903
                          "GM Interval", val)
1904
        GMmenu.append(rs)
1905

    
1906
        m = self._memobj.GM
1907
        for i in range(0, 10):
1908
            cname = "GM[%d].message" % i
1909
            msg = str(m[i].message).rstrip("\xff)")
1910
            val = RadioSettingValueString(0, 32, msg)
1911
            rs = RadioSetting(cname, "GM Message%2d" % (i + 1),
1912
                              val)
1913
            GMmenu.append(rs)
1914

    
1915
        # WiresX settings
1916
        wxc = self._memobj.WiresX_settings
1917
        for i in range(0, 5):
1918
            cname = "WiresX_settings.Category[%d].name" % (i + 1)
1919
            c = ''
1920
            for j in range(0, 16):
1921
                s = wxc.Category[i].name[j]
1922
                if int(s) != 0xff:
1923
                    c = c + str(s)
1924
            val = RadioSettingValueString(0, 16, c)
1925
            rs = RadioSetting(cname, "Category %d" % (i+1), val)
1926
            rs.set_apply_callback(self.apply_WiresX_category,
1927
                                  wxc.Category[i].name)
1928
            WXmenu.append(rs)
1929

    
1930
            r = wxc.RoomsPerCategory[i]
1931
            rn = False
1932
            for j in range(0, 20):
1933
                idn = "0"
1934
                if int(r.Rooms[j].ID[1]) != 0xff:
1935
                    idn = r.Rooms[j].ID
1936
                    rn = False
1937
                elif rn:
1938
                    break
1939
                elif j > 0:
1940
                    rn = True
1941
                val = RadioSettingValueInteger(0, 99999, int(str(idn)))
1942
                vname = "WiresX_settings.RoomsperCategory%s" \
1943
                        "Rooms[%d].ID" % (i, j)
1944
                rs = RadioSetting(vname, "   Room ID%2s (5 numerals)" %
1945
                                  (j+1), val)
1946
                rs.set_apply_callback(self.apply_WiresX_roomid,
1947
                                      r.Rooms[j])
1948
                WXmenu.append(rs)
1949
                cn = ''
1950
                for l in range(0, 16):
1951
                    s = r.Rooms[j].name[l]
1952
                    if int(s) != 0xff:
1953
                        cn = cn + str(s)
1954
                val = RadioSettingValueString(0, 16, str(cn))
1955
                cname = "WiresX_settings.RoomsperCategory%s" \
1956
                        "Rooms[%d].name" % (i, j)
1957
                rs = RadioSetting(cname, "   Room Name%2s (16 chars)" %
1958
                                  (j+1), val)
1959
                rs.set_apply_callback(self.apply_WiresX_roomname,
1960
                                      r.Rooms[j])
1961
                WXmenu.append(rs)
1962
            pass
1963
        return topmenu
1964

    
1965
    def apply_WiresX_category(cls, setting, obj):
1966
        val = setting.value.get_value()
1967
        setattr(obj, "name", val)
1968

    
1969
    def apply_WiresX_roomid(self, setting, obj):
1970
        val = setting.value.get_value()
1971
        obj.ID = self.zero_pad(val, 5)
1972

    
1973
    def apply_WiresX_roomname(cls, setting, obj):
1974
        val = setting.value.get_value()
1975
        obj.name = str(val)
1976

    
1977
    def _get_dtmf_settings(self):
1978
        menu = RadioSettingGroup("dtmf_settings", "DTMF")
1979
        dtmf = self._memobj.scan_settings
1980

    
1981
        val = RadioSettingValueList(
1982
            self._DTMF_MODE,
1983
            self._DTMF_MODE[dtmf.dtmf_mode])
1984
        rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val)
1985
        menu.append(rs)
1986

    
1987
        val = RadioSettingValueList(
1988
            self._DTMF_SPEED,
1989
            self._DTMF_SPEED[dtmf.dtmf_speed])
1990
        rs = RadioSetting(
1991
            "scan_settings.dtmf_speed", "DTMF AutoDial Speed", val)
1992
        menu.append(rs)
1993

    
1994
        val = RadioSettingValueList(
1995
            self._DTMF_DELAY,
1996
            self._DTMF_DELAY[dtmf.dtmf_delay])
1997
        rs = RadioSetting(
1998
            "scan_settings.dtmf_delay", "DTMF AutoDial Delay", val)
1999
        menu.append(rs)
2000

    
2001
        for i in range(10):
2002
            name = "dtmf_%02d" % i
2003
            dtmfsetting = self._memobj.dtmf[i]
2004
            dtmfstr = ""
2005
            for c in dtmfsetting.memory:
2006
                if c == 0xFF:
2007
                    break
2008
                if c < len(FT1_DTMF_CHARS):
2009
                    dtmfstr += FT1_DTMF_CHARS[c]
2010
            dtmfentry = RadioSettingValueString(0, 16, dtmfstr)
2011
            dtmfentry.set_charset(FT1_DTMF_CHARS + list("abcd "))
2012
            rs = RadioSetting(name, name.upper(), dtmfentry)
2013
            rs.set_apply_callback(self.apply_dtmf, i)
2014
            menu.append(rs)
2015

    
2016
        return menu
2017

    
2018
    def _get_misc_settings(self):
2019
        menu = RadioSettingGroup("misc_settings", "Misc")
2020
        scan_settings = self._memobj.scan_settings
2021

    
2022
        val = RadioSettingValueList(
2023
            self._LCD_DIMMER,
2024
            self._LCD_DIMMER[scan_settings.lcd_dimmer])
2025
        rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val)
2026
        menu.append(rs)
2027

    
2028
        val = RadioSettingValueList(
2029
            self._LCD_CONTRAST,
2030
            self._LCD_CONTRAST[scan_settings.lcd_contrast - 1])
2031
        rs = RadioSetting("scan_settings.lcd_contrast", "LCD Contrast",
2032
                          val)
2033
        rs.set_apply_callback(self.apply_lcd_contrast, scan_settings)
2034
        menu.append(rs)
2035

    
2036
        val = RadioSettingValueList(
2037
            self._LAMP_KEY,
2038
            self._LAMP_KEY[scan_settings.lamp])
2039
        rs = RadioSetting("scan_settings.lamp", "Lamp", val)
2040
        menu.append(rs)
2041

    
2042
        beep_select = self._memobj.beep_select
2043

    
2044
        val = RadioSettingValueList(
2045
            self._BEEP_SELECT,
2046
            self._BEEP_SELECT[beep_select.beep])
2047
        rs = RadioSetting("beep_select.beep", "Beep Select", val)
2048
        menu.append(rs)
2049

    
2050
        opening_message = self._memobj.opening_message
2051

    
2052
        val = RadioSettingValueList(
2053
            self._OPENING_MESSAGE,
2054
            self._OPENING_MESSAGE[opening_message.flag])
2055
        rs = RadioSetting("opening_message.flag", "Opening Msg Mode",
2056
                          val)
2057
        menu.append(rs)
2058

    
2059
        rs = self._decode_opening_message(opening_message)
2060
        menu.append(rs)
2061

    
2062
        return menu
2063

    
2064
    def _decode_opening_message(self, opening_message):
2065
        msg = ""
2066
        for i in opening_message.message.padded_yaesu:
2067
            if i == 0xFF:
2068
                break
2069
            msg += CHARSET[i & 0x7F]
2070
        val = RadioSettingValueString(0, 16, msg)
2071
        rs = RadioSetting("opening_message.message.padded_yaesu",
2072
                          "Opening Message", val)
2073
        rs.set_apply_callback(self.apply_ff_padded_yaesu,
2074
                              opening_message.message)
2075
        return rs
2076

    
2077
    def backtrack_ll_validate(self, number, min, max):
2078
        if str(number).lstrip('0').strip().isdigit() and \
2079
                int(str(number).lstrip('0')) <= max and \
2080
                int(str(number).lstrip('0')) >= min:
2081
            return True
2082

    
2083
        return False
2084

    
2085
    @staticmethod
2086
    def zero_pad(number, length):
2087
        """
2088
        Applies a leading zero pad of length `length` to `number`
2089
        """
2090
        return str(number).rjust(length, "0")
2091

    
2092
    def _get_backtrack_settings(self):
2093

    
2094
        menu = RadioSettingGroup("backtrack", "Backtrack")
2095

    
2096
        for i in range(3):
2097
            prefix = ''
2098
            if i == 0:
2099
                prefix = "Star "
2100
            if i == 1:
2101
                prefix = "L1 "
2102
            if i == 2:
2103
                prefix = "L2 "
2104

    
2105
            bt_idx = "backtrack[%d]" % i
2106

    
2107
            bt = self._memobj.backtrack[i]
2108

    
2109
            val = RadioSettingValueList(
2110
                self._BACKTRACK_STATUS,
2111
                self._BACKTRACK_STATUS[0 if bt.status == 1 else 1])
2112
            rs = RadioSetting(
2113
                    "%s.status" % bt_idx,
2114
                    prefix + "status", val)
2115
            rs.set_apply_callback(self.apply_backtrack_status, bt)
2116
            menu.append(rs)
2117

    
2118
            if bt.status == 1 and int(bt.year) < 100:
2119
                val = RadioSettingValueInteger(0, 99, bt.year)
2120
            else:
2121
                val = RadioSettingValueInteger(0, 99, 0)
2122
            rs = RadioSetting(
2123
                    "%s.year" % bt_idx,
2124
                    prefix + "year", val)
2125
            menu.append(rs)
2126

    
2127
            if bt.status == 1 and int(bt.mon) <= 12:
2128
                val = RadioSettingValueInteger(0, 12, bt.mon)
2129
            else:
2130
                val = RadioSettingValueInteger(0, 12, 0)
2131
            rs = RadioSetting(
2132
                    "%s.mon" % bt_idx,
2133
                    prefix + "month", val)
2134
            menu.append(rs)
2135

    
2136
            if bt.status == 1:
2137
                val = RadioSettingValueInteger(0, 31, bt.day)
2138
            else:
2139
                val = RadioSettingValueInteger(0, 31, 0)
2140
            rs = RadioSetting(
2141
                    "%s.day" % bt_idx,
2142
                    prefix + "day", val)
2143
            menu.append(rs)
2144

    
2145
            if bt.status == 1:
2146
                val = RadioSettingValueInteger(0, 23, bt.hour)
2147
            else:
2148
                val = RadioSettingValueInteger(0, 23, 0)
2149
            rs = RadioSetting(
2150
                    "%s.hour" % bt_idx,
2151
                    prefix + "hour", val)
2152
            menu.append(rs)
2153

    
2154
            if bt.status == 1:
2155
                val = RadioSettingValueInteger(0, 59, bt.min)
2156
            else:
2157
                val = RadioSettingValueInteger(0, 59, 0)
2158
            rs = RadioSetting(
2159
                    "%s.min" % bt_idx,
2160
                    prefix + "min", val)
2161
            menu.append(rs)
2162

    
2163
            if bt.status == 1 and \
2164
                    (str(bt.NShemi) == 'N' or str(bt.NShemi) == 'S'):
2165
                val = RadioSettingValueString(0, 1, str(bt.NShemi))
2166
            else:
2167
                val = RadioSettingValueString(0, 1, ' ')
2168
            rs = RadioSetting(
2169
                    "%s.NShemi" % bt_idx,
2170
                    prefix + "NS hemisphere", val)
2171
            rs.set_apply_callback(self.apply_NShemi, bt)
2172
            menu.append(rs)
2173

    
2174
            if bt.status == 1 and self.backtrack_ll_validate(bt.lat, 0, 90):
2175
                val = RadioSettingValueString(
2176
                        0, 3, self.zero_pad(bt.lat, 3))
2177
            else:
2178
                val = RadioSettingValueString(0, 3, '   ')
2179
            rs = RadioSetting("%s.lat" % bt_idx, prefix + "Latitude", val)
2180
            rs.set_apply_callback(self.apply_bt_lat, bt)
2181
            menu.append(rs)
2182

    
2183
            if bt.status == 1 and \
2184
                    self.backtrack_ll_validate(bt.lat_min, 0, 59):
2185
                val = RadioSettingValueString(
2186
                    0, 2, self.zero_pad(bt.lat_min, 2))
2187
            else:
2188
                val = RadioSettingValueString(0, 2, '  ')
2189
            rs = RadioSetting(
2190
                    "%s.lat_min" % bt_idx,
2191
                    prefix + "Latitude Minutes", val)
2192
            rs.set_apply_callback(self.apply_bt_lat_min, bt)
2193
            menu.append(rs)
2194

    
2195
            if bt.status == 1 and \
2196
                    self.backtrack_ll_validate(bt.lat_dec_sec, 0, 9999):
2197
                val = RadioSettingValueString(
2198
                    0, 4, self.zero_pad(bt.lat_dec_sec, 4))
2199
            else:
2200
                val = RadioSettingValueString(0, 4, '    ')
2201
            rs = RadioSetting(
2202
                    "%s.lat_dec_sec" % bt_idx,
2203
                    prefix + "Latitude Decimal Seconds", val)
2204
            rs.set_apply_callback(self.apply_bt_lat_dec_sec, bt)
2205
            menu.append(rs)
2206

    
2207
            if bt.status == 1 and \
2208
                    (str(bt.WEhemi) == 'W' or str(bt.WEhemi) == 'E'):
2209
                val = RadioSettingValueString(
2210
                    0, 1, str(bt.WEhemi))
2211
            else:
2212
                val = RadioSettingValueString(0, 1, ' ')
2213
            rs = RadioSetting(
2214
                    "%s.WEhemi" % bt_idx,
2215
                    prefix + "WE hemisphere", val)
2216
            rs.set_apply_callback(self.apply_WEhemi, bt)
2217
            menu.append(rs)
2218

    
2219
            if bt.status == 1 and self.backtrack_ll_validate(bt.lon, 0, 180):
2220
                val = RadioSettingValueString(
2221
                    0, 3, self.zero_pad(bt.lon, 3))
2222
            else:
2223
                val = RadioSettingValueString(0, 3, '   ')
2224
            rs = RadioSetting("%s.lon" % bt_idx, prefix + "Longitude", val)
2225
            rs.set_apply_callback(self.apply_bt_lon, bt)
2226
            menu.append(rs)
2227

    
2228
            if bt.status == 1 and \
2229
                    self.backtrack_ll_validate(bt.lon_min, 0, 59):
2230
                val = RadioSettingValueString(
2231
                    0, 2, self.zero_pad(bt.lon_min, 2))
2232
            else:
2233
                val = RadioSettingValueString(0, 2, '  ')
2234
            rs = RadioSetting(
2235
                    "%s.lon_min" % bt_idx,
2236
                    prefix + "Longitude Minutes", val)
2237
            rs.set_apply_callback(self.apply_bt_lon_min, bt)
2238
            menu.append(rs)
2239

    
2240
            if bt.status == 1 and \
2241
                    self.backtrack_ll_validate(bt.lon_dec_sec, 0, 9999):
2242
                val = RadioSettingValueString(
2243
                    0, 4, self.zero_pad(bt.lon_dec_sec, 4))
2244
            else:
2245
                val = RadioSettingValueString(0, 4, '    ')
2246
            rs = RadioSetting(
2247
                "%s.lon_dec_sec" % bt_idx,
2248
                prefix + "Longitude Decimal Seconds", val)
2249
            rs.set_apply_callback(self.apply_bt_lon_dec_sec, bt)
2250
            menu.append(rs)
2251

    
2252
        return menu
2253

    
2254
    def _get_scan_settings(self):
2255
        menu = RadioSettingGroup("scan_settings", "Scan")
2256
        scan_settings = self._memobj.scan_settings
2257

    
2258
        val = RadioSettingValueList(
2259
            self._VOL_MODE,
2260
            self._VOL_MODE[scan_settings.vol_mode])
2261
        rs = RadioSetting("scan_settings.vol_mode", "Volume Mode", val)
2262
        menu.append(rs)
2263

    
2264
        vfoa = self._memobj.vfo_info[0]
2265
        val = RadioSettingValueList(
2266
            self._VOLUME,
2267
            self._VOLUME[vfoa.volume])
2268
        rs = RadioSetting("vfo_info[0].volume", "VFO A Volume", val)
2269
        rs.set_apply_callback(self.apply_volume, 0)
2270
        menu.append(rs)
2271

    
2272
        vfob = self._memobj.vfo_info[1]
2273
        val = RadioSettingValueList(
2274
            self._VOLUME,
2275
            self._VOLUME[vfob.volume])
2276
        rs = RadioSetting("vfo_info[1].volume", "VFO B Volume", val)
2277
        rs.set_apply_callback(self.apply_volume, 1)
2278
        menu.append(rs)
2279

    
2280
        squelch = self._memobj.squelch
2281
        val = RadioSettingValueList(
2282
            self._SQUELCH,
2283
            self._SQUELCH[squelch.vfo_a])
2284
        rs = RadioSetting("squelch.vfo_a", "VFO A Squelch", val)
2285
        menu.append(rs)
2286

    
2287
        val = RadioSettingValueList(
2288
            self._SQUELCH,
2289
            self._SQUELCH[squelch.vfo_b])
2290
        rs = RadioSetting("squelch.vfo_b", "VFO B Squelch", val)
2291
        menu.append(rs)
2292

    
2293
        val = RadioSettingValueList(
2294
            self._SCAN_RESTART,
2295
            self._SCAN_RESTART[scan_settings.scan_restart])
2296
        rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val)
2297
        menu.append(rs)
2298

    
2299
        val = RadioSettingValueList(
2300
            self._SCAN_RESUME,
2301
            self._SCAN_RESUME[scan_settings.scan_resume])
2302
        rs = RadioSetting("scan_settings.scan_resume", "Scan Resume", val)
2303
        menu.append(rs)
2304

    
2305
        val = RadioSettingValueList(
2306
            self._OFF_ON,
2307
            self._OFF_ON[scan_settings.busy_led])
2308
        rs = RadioSetting("scan_settings.busy_led", "Busy LED", val)
2309
        menu.append(rs)
2310

    
2311
        val = RadioSettingValueList(
2312
            self._OFF_ON,
2313
            self._OFF_ON[scan_settings.scan_lamp])
2314
        rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val)
2315
        menu.append(rs)
2316

    
2317
        val = RadioSettingValueList(
2318
            self._TOT_TIME,
2319
            self._TOT_TIME[scan_settings.tot])
2320
        rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val)
2321
        menu.append(rs)
2322

    
2323
        return menu
2324

    
2325
    def _get_settings(self):
2326
        top = RadioSettings(self._get_aprs_settings(),
2327
                            self._get_digital_settings(),
2328
                            self._get_dtmf_settings(),
2329
                            self._get_misc_settings(),
2330
                            self._get_scan_settings(),
2331
                            self._get_backtrack_settings())
2332
        return top
2333

    
2334
    def get_settings(self):
2335
        try:
2336
            return self._get_settings()
2337
        except:
2338
            import traceback
2339
            LOG.error("Failed to parse settings: %s", traceback.format_exc())
2340
            return None
2341

    
2342
    @staticmethod
2343
    def apply_custom_symbol(setting, obj):
2344
        # Ensure new value falls within known bounds, otherwise leave it as
2345
        # it's a custom value from the radio that's outside our list.
2346
        if setting.value.get_value() in chirp_common.APRS_SYMBOLS:
2347
            setattr(obj, "custom_symbol",
2348
                    chirp_common.APRS_SYMBOLS.index(setting.value.get_value()))
2349

    
2350
    @classmethod
2351
    def _apply_callsign(cls, callsign, obj, default_ssid=None):
2352
        ssid = default_ssid
2353
        dash_index = callsign.find("-")
2354
        if dash_index >= 0:
2355
            ssid = callsign[dash_index + 1:]
2356
            callsign = callsign[:dash_index]
2357
            try:
2358
                ssid = int(ssid) % 16
2359
            except ValueError:
2360
                ssid = default_ssid
2361
        setattr(obj, "callsign", cls._add_ff_pad(callsign.encode(), 6))
2362
        if ssid is not None:
2363
            setattr(obj, "ssid", ssid)
2364

    
2365
    def apply_beacon_type(cls, setting, obj):
2366
        beacon_type = str(setting.value.get_value())
2367
        beacon_index = cls._BEACON_TYPE.index(beacon_type)
2368
        tx_smartbeacon = beacon_index >> 1
2369
        tx_interval_beacon = beacon_index & 1
2370
        if tx_interval_beacon:
2371
            setattr(obj, "tx_interval_beacon", 1)
2372
            setattr(obj, "tx_smartbeacon", 0)
2373
        elif tx_smartbeacon:
2374
            setattr(obj, "tx_interval_beacon", 0)
2375
            setattr(obj, "tx_smartbeacon", 1)
2376
        else:
2377
            setattr(obj, "tx_interval_beacon", 0)
2378
            setattr(obj, "tx_smartbeacon", 0)
2379

    
2380
    # digital settings callback routines
2381
    def apply_digital_popup(cls, setting, obj):
2382
        rawval = setting.value.get_value()
2383
        val = 0 if cls._DIG_POP_UP.index(rawval) == 0 \
2384
            else cls._DIG_POP_UP.index(rawval) + 9
2385
        obj.digital_popup = val
2386

    
2387
    def apply_mycall(cls, setting, obj):
2388
        cs = setting.value.get_value()
2389
        if cs[0] in ('-', '/'):
2390
            raise InvalidValueError("First character of"
2391
                                    " call sign can't be - or /:  {0:s}"
2392
                                    .format(cs))
2393
        else:
2394
            obj.callsign = cls._add_ff_pad(cs.rstrip(), 10)
2395

    
2396
    @classmethod
2397
    def apply_callsign(cls, setting, obj, default_ssid=None):
2398
        # Uppercase, strip SSID then FF pad to max string length.
2399
        callsign = setting.value.get_value().upper()
2400
        cls._apply_callsign(callsign, obj, default_ssid)
2401

    
2402
    def apply_digi_path(self, setting, obj):
2403
        # Parse and map to aprs.digi_path_4_7[0-3] or aprs.digi_path_8
2404
        # and FF terminate.
2405
        path = str(setting.value.get_value())
2406
        callsigns = [c.strip() for c in path.split(",")]
2407
        for index in range(len(obj.entry)):
2408
            try:
2409
                self._apply_callsign(callsigns[index], obj.entry[index], 0)
2410
            except IndexError:
2411
                self._apply_callsign("", obj.entry[index], 0)
2412
        if len(callsigns) > len(obj.entry):
2413
            raise Exception("This path only supports %d entries" % (index + 1))
2414

    
2415
    @classmethod
2416
    def apply_ff_padded_string(cls, setting, obj):
2417
        # FF pad.
2418
        val = setting.value.get_value()
2419
        max_len = getattr(obj, "padded_string").size() / 8
2420
        val = str(val).rstrip()
2421
        setattr(obj, "padded_string", cls._add_ff_pad(val, max_len))
2422

    
2423
    @classmethod
2424
    def apply_lat_long(cls, setting, obj):
2425
        name = setting.get_name()
2426
        is_latitude = name.endswith("latitude")
2427
        lat_long = setting.value.get_value().strip()
2428
        sign, l_d, l_m, l_s = cls._str_to_latlong(lat_long, is_latitude)
2429
        LOG.debug("%s: %d %d %d %d" % (name, sign, l_d, l_m, l_s))
2430
        setattr(obj, "%s_sign" % name, sign)
2431
        setattr(obj, "%s_degree" % name, l_d)
2432
        setattr(obj, "%s_minute" % name, l_m)
2433
        setattr(obj, "%s_second" % name, l_s)
2434

    
2435
    def set_settings(self, settings):
2436
        _mem = self._memobj
2437
        for element in settings:
2438
            if not isinstance(element, RadioSetting):
2439
                self.set_settings(element)
2440
                continue
2441
            if not element.changed():
2442
                continue
2443
            try:
2444
                if element.has_apply_callback():
2445
                    LOG.debug("Using apply callback")
2446
                    try:
2447
                        element.run_apply_callback()
2448
                    except NotImplementedError as e:
2449
                        LOG.error("ft1d.set_settings: %s", e)
2450
                    continue
2451

    
2452
                # Find the object containing setting.
2453
                obj = _mem
2454
                bits = element.get_name().split(".")
2455
                setting = bits[-1]
2456
                for name in bits[:-1]:
2457
                    if name.endswith("]"):
2458
                        name, index = name.split("[")
2459
                        index = int(index[:-1])
2460
                        obj = getattr(obj, name)[index]
2461
                    else:
2462
                        obj = getattr(obj, name)
2463

    
2464
                try:
2465
                    old_val = getattr(obj, setting)
2466
                    LOG.debug("Setting %s(%r) <= %s" % (
2467
                            element.get_name(), old_val, element.value))
2468
                    setattr(obj, setting, element.value)
2469
                except AttributeError as e:
2470
                    LOG.error("Setting %s is not in the memory map: %s" %
2471
                              (element.get_name(), e))
2472
            except Exception:
2473
                LOG.debug(element.get_name())
2474
                raise
2475

    
2476
    def apply_ff_padded_yaesu(cls, setting, obj):
2477
        # FF pad yaesus custom string format.
2478
        rawval = setting.value.get_value()
2479
        max_len = getattr(obj, "padded_yaesu").size() / 8
2480
        rawval = str(rawval).rstrip()
2481
        val = [CHARSET.index(x) for x in rawval]
2482
        for x in range(len(val), max_len):
2483
            val.append(0xFF)
2484
        obj.padded_yaesu = val
2485

    
2486
    def apply_volume(cls, setting, vfo):
2487
        val = setting.value.get_value()
2488
        cls._memobj.vfo_info[(vfo * 2)].volume = val
2489
        cls._memobj.vfo_info[(vfo * 2) + 1].volume = val
2490

    
2491
    def apply_lcd_contrast(cls, setting, obj):
2492
        rawval = setting.value.get_value()
2493
        val = cls._LCD_CONTRAST.index(rawval) + 1
2494
        obj.lcd_contrast = val
2495

    
2496
    def apply_dtmf(cls, setting, i):
2497
        rawval = setting.value.get_value().upper().rstrip()
2498
        val = [FT1_DTMF_CHARS.index(x) for x in rawval]
2499
        for x in range(len(val), 16):
2500
            val.append(0xFF)
2501
        cls._memobj.dtmf[i].memory = val
2502

    
2503
    def apply_backtrack_status(cls, setting, obj):
2504
        status = setting.value.get_value()
2505

    
2506
        if status == 'Valid':
2507
            val = 1
2508
        else:
2509
            val = 8
2510
        setattr(obj, "status", val)
2511

    
2512
    def apply_NShemi(cls, setting, obj):
2513
        hemi = setting.value.get_value().upper()
2514

    
2515
        if hemi != 'N' and hemi != 'S':
2516
            hemi = ' '
2517
        setattr(obj, "NShemi", hemi)
2518

    
2519
    def apply_WEhemi(cls, setting, obj):
2520
        hemi = setting.value.get_value().upper()
2521

    
2522
        if hemi != 'W' and hemi != 'E':
2523
            hemi = ' '
2524
        setattr(obj, "WEhemi", hemi)
2525

    
2526
    def apply_bt_lat(cls, setting, obj):
2527
        val = setting.value.get_value()
2528
        val = cls.backtrack_zero_pad(val, 3)
2529

    
2530
        setattr(obj, "lat", val)
2531

    
2532
    def apply_bt_lat_min(cls, setting, obj):
2533
        val = setting.value.get_value()
2534
        val = cls.backtrack_zero_pad(val, 2)
2535

    
2536
        setattr(obj, "lat_min", val)
2537

    
2538
    def apply_bt_lat_dec_sec(cls, setting, obj):
2539
        val = setting.value.get_value()
2540
        val = cls.backtrack_zero_pad(val, 4)
2541

    
2542
        setattr(obj, "lat_dec_sec", val)
2543

    
2544
    def apply_bt_lon(cls, setting, obj):
2545
        val = setting.value.get_value()
2546
        val = cls.backtrack_zero_pad(val, 3)
2547

    
2548
        setattr(obj, "lon", val)
2549

    
2550
    def apply_bt_lon_min(cls, setting, obj):
2551
        val = setting.value.get_value()
2552
        val = cls.backtrack_zero_pad(val, 2)
2553

    
2554
        setattr(obj, "lon_min", val)
2555

    
2556
    def apply_bt_lon_dec_sec(cls, setting, obj):
2557
        val = setting.value.get_value()
2558
        val = cls.backtrack_zero_pad(val, 4)
2559

    
2560
        setattr(obj, "lon_dec_sec", val)
2561

    
2562
    def load_mmap(self, filename):
2563
        if filename.lower().endswith(self._adms_ext):
2564
            with open(filename, 'rb') as f:
2565
                self._adms_header = f.read(0x16)
2566
                LOG.debug('ADMS Header:\n%s',
2567
                          util.hexprint(self._adms_header))
2568
                self._mmap = memmap.MemoryMapBytes(self._model + f.read())
2569
                LOG.info('Loaded ADMS file')
2570
            self.process_mmap()
2571
        else:
2572
            chirp_common.CloneModeRadio.load_mmap(self, filename)
2573

    
2574
    def save_mmap(self, filename):
2575
        if filename.lower().endswith(self._adms_ext):
2576
            if not hasattr(self, '_adms_header'):
2577
                raise Exception('Unable to save .img to %s' % self._adms_ext)
2578
            with open(filename, 'wb') as f:
2579
                f.write(self._adms_header)
2580
                f.write(self._mmap.get_packed()[5:])
2581
                LOG.info('Wrote file')
2582
        else:
2583
            chirp_common.CloneModeRadio.save_mmap(self, filename)
(2-2/2)