Project

General

Profile

Bug #9101 » vx8_test.py

Jim Unroe, 06/07/2021 12:41 PM

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

    
16
import os
17
import re
18
import logging
19

    
20
from chirp.drivers import yaesu_clone
21
from chirp import chirp_common, directory, bitwise
22
from chirp.settings import RadioSettingGroup, RadioSetting, RadioSettings
23
from chirp.settings import RadioSettingValueInteger, RadioSettingValueString
24
from chirp.settings import RadioSettingValueList, RadioSettingValueBoolean
25
from textwrap import dedent
26

    
27
LOG = logging.getLogger(__name__)
28

    
29
MEM_FORMAT = """
30
#seekto 0x047f;
31
struct {
32
  u8 flag;
33
  u16 unknown;
34
  struct {
35
    u8 padded_yaesu[16];
36
  } message;
37
} opening_message;
38

    
39
#seekto 0x049a;
40
struct {
41
  u8 vfo_a;
42
  u8 vfo_b;
43
} squelch;
44

    
45
#seekto 0x04bf;
46
struct {
47
  u8 beep;
48
} beep_select;
49

    
50
#seekto 0x04cc;
51
struct {
52
  u8 lcd_dimmer;
53
  u8 dtmf_delay;
54
  u8 unknown0[3];
55
  u8 unknown1:4
56
     lcd_contrast:4;
57
  u8 lamp;
58
  u8 unknown2[7];
59
  u8 scan_restart;
60
  u8 unknown3;
61
  u8 scan_resume;
62
  u8 unknown4[6];
63
  u8 tot;
64
  u8 unknown5[3];
65
  u8 unknown6:2,
66
     scan_lamp:1,
67
     unknown7:2,
68
     dtmf_speed:1,
69
     unknown8:1,
70
     dtmf_mode:1;
71
  u8 busy_led:1,
72
     unknown9:7;
73
  u8 unknown10[2];
74
  u8 vol_mode:1,
75
     unknown11:7;
76
} scan_settings;
77

    
78
#seekto 0x54a;
79
struct {
80
    u16 in_use;
81
} bank_used[24];
82

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

    
127
#seekto 0x094a;
128
struct {
129
  u8 memory[16];
130
} dtmf[10];
131

    
132
#seekto 0x135A;
133
struct {
134
  u8 unknown[2];
135
  u8 name[16];
136
} bank_info[24];
137

    
138
#seekto 0x198a;
139
struct {
140
    u16 channel[100];
141
} bank_members[24];
142

    
143
#seekto 0x2C4A;
144
struct {
145
  u8 nosubvfo:1,
146
     unknown:3,
147
     pskip:1,
148
     skip:1,
149
     used:1,
150
     valid:1;
151
} flag[900];
152

    
153
#seekto 0x328A;
154
struct {
155
  u8 unknown1a:2,
156
     half_deviation:1,
157
     unknown1b:5;
158
  u8 mode:2,
159
     duplex:2,
160
     tune_step:4;
161
  bbcd freq[3];
162
  u8 power:2,
163
     unknown2:4,
164
     tone_mode:2;
165
  u8 charsetbits[2];
166
  char label[16];
167
  bbcd offset[3];
168
  u8 unknown5:2,
169
     tone:6;
170
  u8 unknown6:1,
171
     dcs:7;
172
  u8 pr_frequency;
173
  u8 unknown7;
174
  u8 unknown8a:3,
175
     unknown8b:1,
176
     rx_mode_auto:1,
177
     unknown8c:3;
178
} memory[900];
179

    
180
#seekto 0xC0CA;
181
struct {
182
  u8 unknown0:6,
183
     rx_baud:2;
184
  u8 unknown1:4,
185
     tx_delay:4;
186
  u8 custom_symbol;
187
  u8 unknown2;
188
  struct {
189
    char callsign[6];
190
    u8 ssid;
191
  } my_callsign;
192
  u8 unknown3:4,
193
     selected_position_comment:4;
194
  u8 unknown4;
195
  u8 set_time_manually:1,
196
     tx_interval_beacon:1,
197
     ring_beacon:1,
198
     ring_msg:1,
199
     aprs_mute:1,
200
     unknown6:1,
201
     tx_smartbeacon:1,
202
     af_dual:1;
203
  u8 unknown7:1,
204
     aprs_units_wind_mph:1,
205
     aprs_units_rain_inch:1,
206
     aprs_units_temperature_f:1
207
     aprs_units_altitude_ft:1,
208
     unknown8:1,
209
     aprs_units_distance_m:1,
210
     aprs_units_position_mmss:1;
211
  u8 unknown9:6,
212
     aprs_units_speed:2;
213
  u8 unknown11:1,
214
     filter_other:1,
215
     filter_status:1,
216
     filter_item:1,
217
     filter_object:1,
218
     filter_weather:1,
219
     filter_position:1,
220
     filter_mic_e:1;
221
  u8 unknown12:2,
222
     timezone:6;
223
  u8 unknown13:4,
224
     beacon_interval:4;
225
  u8 unknown14;
226
  u8 unknown15:7,
227
     latitude_sign:1;
228
  u8 latitude_degree;
229
  u8 latitude_minute;
230
  u8 latitude_second;
231
  u8 unknown16:7,
232
     longitude_sign:1;
233
  u8 longitude_degree;
234
  u8 longitude_minute;
235
  u8 longitude_second;
236
  u8 unknown17:4,
237
     selected_position:4;
238
  u8 unknown18:5,
239
     selected_beacon_status_txt:3;
240
  u8 unknown19:6,
241
     gps_units_altitude_ft:1,
242
     gps_units_position_sss:1;
243
  u8 unknown20:6,
244
     gps_units_speed:2;
245
  u8 unknown21[4];
246
  struct {
247
    struct {
248
      char callsign[6];
249
      u8 ssid;
250
    } entry[8];
251
  } digi_path_7;
252
  u8 unknown22[2];
253
} aprs;
254

    
255
#seekto 0x%04X;
256
struct {
257
  char padded_string[16];
258
} aprs_msg_macro[%d];
259

    
260
#seekto 0x%04X;
261
struct {
262
  u8 unknown23:5,
263
     selected_msg_group:3;
264
  u8 unknown24;
265
  struct {
266
    char padded_string[9];
267
  } msg_group[8];
268
  u8 unknown25[4];
269
  u8 active_smartbeaconing;
270
  struct {
271
    u8 low_speed_mph;
272
    u8 high_speed_mph;
273
    u8 slow_rate_min;
274
    u8 fast_rate_sec;
275
    u8 turn_angle;
276
    u8 turn_slop;
277
    u8 turn_time_sec;
278
  } smartbeaconing_profile[3];
279
  u8 unknown26:2,
280
     flash_msg:6;
281
  u8 unknown27:2,
282
     flash_grp:6;
283
  u8 unknown28:2,
284
     flash_bln:6;
285
  u8 selected_digi_path;
286
  struct {
287
    struct {
288
      char callsign[6];
289
      u8 ssid;
290
    } entry[2];
291
  } digi_path_3_6[4];
292
  u8 unknown30:6,
293
     selected_my_symbol:2;
294
  u8 unknown31[3];
295
  u8 unknown32:2,
296
     vibrate_msg:6;
297
  u8 unknown33:2,
298
     vibrate_grp:6;
299
  u8 unknown34:2,
300
     vibrate_bln:6;
301
} aprs2;
302

    
303
#seekto 0x%04X;
304
struct {
305
  bbcd date[3];
306
  u8 unknown1;
307
  bbcd time[2];
308
  u8 sequence;
309
  u8 unknown2;
310
  u8 sender_callsign[7];
311
  u8 data_type;
312
  u8 yeasu_data_type;
313
  u8 unknown3;
314
  u8 unknown4:1,
315
     callsign_is_ascii:1,
316
     unknown5:6;
317
  u8 unknown6;
318
  u16 pkt_len;
319
  u16 in_use;
320
} aprs_beacon_meta[%d];
321

    
322
#seekto 0x%04X;
323
struct {
324
  u8 dst_callsign[6];
325
  u8 dst_callsign_ssid;
326
  u8 src_callsign[6];
327
  u8 src_callsign_ssid;
328
  u8 path_and_body[%d];
329
} aprs_beacon_pkt[%d];
330

    
331
#seekto 0xf92a;
332
struct {
333
  char padded_string[60];
334
} aprs_beacon_status_txt[5];
335

    
336
#seekto 0xFECA;
337
u8 checksum;
338
"""
339

    
340
TMODES = ["", "Tone", "TSQL", "DTCS"]
341
DUPLEX = ["", "-", "+", "split"]
342
MODES = ["FM", "AM", "WFM", "NFM"]
343
STEPS = list(chirp_common.TUNING_STEPS)
344
STEPS.remove(30.0)
345
STEPS.append(100.0)
346
STEPS.insert(2, 8.33)  # Index 2 is 8.33kHz airband step
347
SKIPS = ["", "S", "P"]
348
VX8_DTMF_CHARS = list("0123456789ABCD*#-")
349

    
350
CHARSET = ["%i" % int(x) for x in range(0, 10)] + \
351
    [chr(x) for x in range(ord("A"), ord("Z")+1)] + \
352
    [" "] + \
353
    [chr(x) for x in range(ord("a"), ord("z")+1)] + \
354
    list(".,:;*#_-/&()@!?^ ") + list("\x00" * 100)
355

    
356
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=5.00),
357
                chirp_common.PowerLevel("L3", watts=2.50),
358
                chirp_common.PowerLevel("L2", watts=1.00),
359
                chirp_common.PowerLevel("L1", watts=0.05)]
360

    
361

    
362
class VX8Bank(chirp_common.NamedBank):
363
    """A VX-8 bank"""
364

    
365
    def get_name(self):
366
        _bank = self._model._radio._memobj.bank_info[self.index]
367
        _bank_used = self._model._radio._memobj.bank_used[self.index]
368

    
369
        name = ""
370
        for i in _bank.name:
371
            if i == 0xFF:
372
                break
373
            name += CHARSET[i & 0x7F]
374
        return name.rstrip()
375

    
376
    def set_name(self, name):
377
        _bank = self._model._radio._memobj.bank_info[self.index]
378
        _bank.name = [CHARSET.index(x) for x in name.ljust(16)[:16]]
379

    
380

    
381
class VX8BankModel(chirp_common.BankModel):
382
    """A VX-8 bank model"""
383
    def __init__(self, radio, name='Banks'):
384
        super(VX8BankModel, self).__init__(radio, name)
385

    
386
        _banks = self._radio._memobj.bank_info
387
        self._bank_mappings = []
388
        for index, _bank in enumerate(_banks):
389
            bank = VX8Bank(self, "%i" % index, "BANK-%i" % index)
390
            bank.index = index
391
            self._bank_mappings.append(bank)
392

    
393
    def get_num_mappings(self):
394
        return len(self._bank_mappings)
395

    
396
    def get_mappings(self):
397
        return self._bank_mappings
398

    
399
    def _channel_numbers_in_bank(self, bank):
400
        _bank_used = self._radio._memobj.bank_used[bank.index]
401
        if _bank_used.in_use == 0xFFFF:
402
            return set()
403

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

    
407
    def update_vfo(self):
408
        chosen_bank = [None, None]
409
        chosen_mr = [None, None]
410

    
411
        flags = self._radio._memobj.flag
412

    
413
        # Find a suitable bank and MR for VFO A and B.
414
        for bank in self.get_mappings():
415
            for channel in self._channel_numbers_in_bank(bank):
416
                chosen_bank[0] = bank.index
417
                chosen_mr[0] = channel
418
                if not flags[channel].nosubvfo:
419
                    chosen_bank[1] = bank.index
420
                    chosen_mr[1] = channel
421
                    break
422
            if chosen_bank[1]:
423
                break
424

    
425
        for vfo_index in (0, 1):
426
            # 3 VFO info structs are stored as 3 pairs of (master, backup)
427
            vfo = self._radio._memobj.vfo_info[vfo_index * 2]
428
            vfo_bak = self._radio._memobj.vfo_info[(vfo_index * 2) + 1]
429

    
430
            if vfo.checksum != vfo_bak.checksum:
431
                LOG.warn("VFO settings are inconsistent with backup")
432
            else:
433
                if ((chosen_bank[vfo_index] is None) and
434
                        (vfo.bank_index != 0xFFFF)):
435
                    LOG.info("Disabling banks for VFO %d" % vfo_index)
436
                    vfo.bank_index = 0xFFFF
437
                    vfo.mr_index = 0xFFFF
438
                    vfo.bank_enable = 0xFFFF
439
                elif ((chosen_bank[vfo_index] is not None) and
440
                        (vfo.bank_index == 0xFFFF)):
441
                    LOG.debug("Enabling banks for VFO %d" % vfo_index)
442
                    vfo.bank_index = chosen_bank[vfo_index]
443
                    vfo.mr_index = chosen_mr[vfo_index]
444
                    vfo.bank_enable = 0x0000
445
                vfo_bak.bank_index = vfo.bank_index
446
                vfo_bak.mr_index = vfo.mr_index
447
                vfo_bak.bank_enable = vfo.bank_enable
448

    
449
    def _update_bank_with_channel_numbers(self, bank, channels_in_bank):
450
        _members = self._radio._memobj.bank_members[bank.index]
451
        if len(channels_in_bank) > len(_members.channel):
452
            raise Exception("Too many entries in bank %d" % bank.index)
453

    
454
        empty = 0
455
        for index, channel_number in enumerate(sorted(channels_in_bank)):
456
            _members.channel[index] = channel_number - 1
457
            empty = index + 1
458
        for index in range(empty, len(_members.channel)):
459
            _members.channel[index] = 0xFFFF
460

    
461
    def add_memory_to_mapping(self, memory, bank):
462
        channels_in_bank = self._channel_numbers_in_bank(bank)
463
        channels_in_bank.add(memory.number)
464
        self._update_bank_with_channel_numbers(bank, channels_in_bank)
465

    
466
        _bank_used = self._radio._memobj.bank_used[bank.index]
467
        _bank_used.in_use = 0x06
468

    
469
        self.update_vfo()
470

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

    
480
        if not channels_in_bank:
481
            _bank_used = self._radio._memobj.bank_used[bank.index]
482
            _bank_used.in_use = 0xFFFF
483

    
484
        self.update_vfo()
485

    
486
    def get_mapping_memories(self, bank):
487
        memories = []
488
        for channel in self._channel_numbers_in_bank(bank):
489
            memories.append(self._radio.get_memory(channel))
490

    
491
        return memories
492

    
493
    def get_memory_mappings(self, memory):
494
        banks = []
495
        for bank in self.get_mappings():
496
            if memory.number in self._channel_numbers_in_bank(bank):
497
                banks.append(bank)
498

    
499
        return banks
500

    
501

    
502
def _wipe_memory(mem):
503
    mem.set_raw("\x00" * (mem.size() / 8))
504
    mem.pr_frequency = 0x1d  # default PR frequency of 1600 Hz
505
    mem.unknown8b = 1        # This bit must be 1, but its meaning is unknown
506
    mem.rx_mode_auto = 1     # rx auto mode bit defaults to 1
507

    
508

    
509
@directory.register
510
class VX8Radio(yaesu_clone.YaesuCloneModeRadio):
511
    """Yaesu VX-8"""
512
    BAUD_RATE = 38400
513
    VENDOR = "Yaesu"
514
    MODEL = "VX-8R"
515

    
516
    _model = "AH029"
517
    _memsize = 65227
518
    _block_lengths = [10, 65217]
519
    _block_size = 32
520
    _mem_params = (0xC128,  # APRS message macros
521
                   5,       # Number of message macros
522
                   0xC178,  # APRS2
523
                   0xC24A,  # APRS beacon metadata address.
524
                   40,      # Number of beacons stored.
525
                   0xC60A,  # APRS beacon content address.
526
                   194,     # Length of beacon data stored.
527
                   40)      # Number of beacons stored.
528
    _has_vibrate = False
529
    _has_af_dual = True
530

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

    
534
    _RX_BAUD = ("off", "1200 baud", "9600 baud")
535
    _TX_DELAY = ("100ms", "200ms", "300ms",
536
                 "400ms", "500ms", "750ms", "1000ms")
537
    _WIND_UNITS = ("m/s", "mph")
538
    _RAIN_UNITS = ("mm", "inch")
539
    _TEMP_UNITS = ("C", "F")
540
    _ALT_UNITS = ("m", "ft")
541
    _DIST_UNITS = ("km", "mile")
542
    _POS_UNITS = ("dd.mmmm'", "dd mm'ss\"")
543
    _SPEED_UNITS = ("km/h", "knot", "mph")
544
    _TIME_SOURCE = ("manual", "GPS")
545
    _TZ = ("-13:00", "-13:30", "-12:00", "-12:30", "-11:00", "-11:30",
546
           "-10:00", "-10:30", "-09:00", "-09:30", "-08:00", "-08:30",
547
           "-07:00", "-07:30", "-06:00", "-06:30", "-05:00", "-05:30",
548
           "-04:00", "-04:30", "-03:00", "-03:30", "-02:00", "-02:30",
549
           "-01:00", "-01:30", "-00:00", "-00:30", "+01:00", "+01:30",
550
           "+02:00", "+02:30", "+03:00", "+03:30", "+04:00", "+04:30",
551
           "+05:00", "+05:30", "+06:00", "+06:30", "+07:00", "+07:30",
552
           "+08:00", "+08:30", "+09:00", "+09:30", "+10:00", "+10:30",
553
           "+11:00", "+11:30")
554
    _BEACON_TYPE = ("Off", "Interval")
555
    _BEACON_INT = ("15s", "30s", "1m", "2m", "3m", "5m", "10m", "15m",
556
                   "30m")
557
    _DIGI_PATHS = ("OFF", "WIDE1-1", "WIDE1-1, WIDE2-1", "Digi Path 4",
558
                   "Digi Path 5", "Digi Path 6", "Digi Path 7", "Digi Path 8")
559
    _MSG_GROUP_NAMES = ("Message Group 1", "Message Group 2",
560
                        "Message Group 3", "Message Group 4",
561
                        "Message Group 5", "Message Group 6",
562
                        "Message Group 7", "Message Group 8")
563
    _POSITIONS = ("GPS", "Manual Latitude/Longitude",
564
                  "Manual Latitude/Longitude", "P1", "P2", "P3", "P4",
565
                  "P5", "P6", "P7", "P8", "P9", "P10")
566
    _FLASH = ("OFF", "ON")
567
    _BEEP_SELECT = ("Off", "Key+Scan", "Key")
568
    _SQUELCH = ["%d" % x for x in range(0, 16)]
569
    _VOLUME = ["%d" % x for x in range(0, 33)]
570
    _OPENING_MESSAGE = ("Off", "DC", "Message", "Normal")
571
    _SCAN_RESUME = ["%.1fs" % (0.5 * x) for x in range(4, 21)] + \
572
                   ["Busy", "Hold"]
573
    _SCAN_RESTART = ["%.1fs" % (0.1 * x) for x in range(1, 10)] + \
574
                    ["%.1fs" % (0.5 * x) for x in range(2, 21)]
575
    _LAMP_KEY = ["Key %d sec" % x for x in range(2, 11)] + \
576
                ["Continuous", "OFF"]
577
    _LCD_CONTRAST = ["Level %d" % x for x in range(1, 33)]
578
    _LCD_DIMMER = ["Level %d" % x for x in range(1, 5)]
579
    _TOT_TIME = ["Off"] + ["%.1f min" % (0.5 * x) for x in range(1, 21)]
580
    _OFF_ON = ("Off", "On")
581
    _VOL_MODE = ("Normal", "Auto Back")
582
    _DTMF_MODE = ("Manual", "Auto")
583
    _DTMF_SPEED = ("50ms", "100ms")
584
    _DTMF_DELAY = ("50ms", "250ms", "450ms", "750ms", "1000ms")
585
    _MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected")
586

    
587
    @classmethod
588
    def get_prompts(cls):
589
        rp = chirp_common.RadioPrompts()
590
        rp.pre_download = _(dedent("""\
591
1. Turn radio off.
592
2. Connect cable to DATA jack.
593
3. Press and hold in the [FW] key while turning the radio on
594
     ("CLONE" will appear on the display).
595
4. <b>After clicking OK</b>, press the [BAND] key to send image."""))
596
        rp.pre_upload = _(dedent("""\
597
1. Turn radio off.
598
2. Connect cable to DATA jack.
599
3. Press and hold in the [FW] key while turning the radio on
600
     ("CLONE" will appear on the display).
601
4. Press the [MODE] key ("-WAIT-" will appear on the LCD)."""))
602
        return rp
603

    
604
    def process_mmap(self):
605
        self._memobj = bitwise.parse(MEM_FORMAT % self._mem_params, self._mmap)
606

    
607
    def get_features(self):
608
        rf = chirp_common.RadioFeatures()
609
        rf.has_dtcs_polarity = False
610
        rf.valid_modes = list(MODES)
611
        rf.valid_tmodes = list(TMODES)
612
        rf.valid_duplexes = list(DUPLEX)
613
        rf.valid_tuning_steps = list(STEPS)
614
        rf.valid_bands = [(500000, 999900000)]
615
        rf.valid_skips = SKIPS
616
        rf.valid_power_levels = POWER_LEVELS
617
        rf.valid_characters = "".join(CHARSET)
618
        rf.valid_name_length = 16
619
        rf.memory_bounds = (1, 900)
620
        rf.can_odd_split = True
621
        rf.has_ctone = False
622
        rf.has_bank_names = True
623
        rf.has_settings = True
624
        return rf
625

    
626
    def get_raw_memory(self, number):
627
        return repr(self._memobj.memory[number-1])
628

    
629
    def _checksums(self):
630
        return [yaesu_clone.YaesuChecksum(0x064A, 0x06C8),
631
                yaesu_clone.YaesuChecksum(0x06CA, 0x0748),
632
                yaesu_clone.YaesuChecksum(0x074A, 0x07C8),
633
                yaesu_clone.YaesuChecksum(0x07CA, 0x0848),
634
                yaesu_clone.YaesuChecksum(0x0000, 0xFEC9)]
635

    
636
    @staticmethod
637
    def _add_ff_pad(val, length):
638
        return val.ljust(length, "\xFF")[:length]
639

    
640
    @classmethod
641
    def _strip_ff_pads(cls, messages):
642
        result = []
643
        for msg_text in messages:
644
            result.append(str(msg_text).rstrip("\xFF"))
645
        return result
646

    
647
    def get_memory(self, number):
648
        flag = self._memobj.flag[number-1]
649
        _mem = self._memobj.memory[number-1]
650

    
651
        mem = chirp_common.Memory()
652
        mem.number = number
653
        if not flag.used:
654
            mem.empty = True
655
        if not flag.valid:
656
            mem.empty = True
657
            return mem
658
        mem.freq = chirp_common.fix_rounded_step(int(_mem.freq) * 1000)
659
        mem.offset = int(_mem.offset) * 1000
660
        mem.rtone = mem.ctone = chirp_common.TONES[_mem.tone]
661
        mem.tmode = TMODES[_mem.tone_mode]
662
        mem.duplex = DUPLEX[_mem.duplex]
663
        if mem.duplex == "split":
664
            mem.offset = chirp_common.fix_rounded_step(mem.offset)
665
        if _mem.mode == "FM" and _mem.half_deviation == 1:
666
            mem.mode = "NFM"
667
        else:
668
            mem.mode = MODES[_mem.mode]
669
        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs]
670
        mem.tuning_step = STEPS[_mem.tune_step]
671
        mem.power = POWER_LEVELS[3 - _mem.power]
672
        mem.skip = flag.pskip and "P" or flag.skip and "S" or ""
673

    
674
        charset = ''.join(CHARSET).ljust(256, '.')
675
        mem.name = str(_mem.label).rstrip("\xFF").translate(charset)
676

    
677
        return mem
678

    
679
    def _debank(self, mem):
680
        bm = self.get_bank_model()
681
        for bank in bm.get_memory_mappings(mem):
682
            bm.remove_memory_from_mapping(mem, bank)
683

    
684
    def set_memory(self, mem):
685
        _mem = self._memobj.memory[mem.number-1]
686
        flag = self._memobj.flag[mem.number-1]
687

    
688
        self._debank(mem)
689

    
690
        if not mem.empty and not flag.valid:
691
            _wipe_memory(_mem)
692

    
693
        if mem.empty and flag.valid and not flag.used:
694
            flag.valid = False
695
            return
696
        flag.used = not mem.empty
697
        flag.valid = flag.used
698

    
699
        if mem.empty:
700
            return
701

    
702
        if mem.freq < 30000000 or \
703
                (mem.freq > 88000000 and mem.freq < 108000000) or \
704
                mem.freq > 580000000:
705
            flag.nosubvfo = True   # Masked from VFO B
706
        else:
707
            flag.nosubvfo = False  # Available in both VFOs
708

    
709
        _mem.freq = int(mem.freq / 1000)
710
        _mem.offset = int(mem.offset / 1000)
711
        _mem.tone = chirp_common.TONES.index(mem.rtone)
712
        _mem.tone_mode = TMODES.index(mem.tmode)
713
        _mem.duplex = DUPLEX.index(mem.duplex)
714
        if mem.mode == "NFM":
715
            _mem.mode = 0            # Yaesu's NFM, i.e. regular FM
716
            _mem.half_deviation = 1  # but half bandwidth
717
        else:
718
            _mem.mode = MODES.index(mem.mode)
719
            _mem.half_deviation = 0
720
        _mem.mode = MODES.index(mem.mode)
721
        _mem.dcs = chirp_common.DTCS_CODES.index(mem.dtcs)
722
        _mem.tune_step = STEPS.index(mem.tuning_step)
723
        if mem.power:
724
            _mem.power = 3 - POWER_LEVELS.index(mem.power)
725
        else:
726
            _mem.power = 0
727

    
728
        label = "".join([chr(CHARSET.index(x)) for x in mem.name.rstrip()])
729
        _mem.label = self._add_ff_pad(label, 16)
730
        # We only speak english here in chirpville
731
        _mem.charsetbits[0] = 0x00
732
        _mem.charsetbits[1] = 0x00
733

    
734
        flag.skip = mem.skip == "S"
735
        flag.pskip = mem.skip == "P"
736

    
737
    def get_bank_model(self):
738
        return VX8BankModel(self)
739

    
740
    @classmethod
741
    def _digi_path_to_str(cls, path):
742
        path_cmp = []
743
        for entry in path.entry:
744
            callsign = str(entry.callsign).rstrip("\xFF")
745
            if not callsign:
746
                break
747
            path_cmp.append("%s-%d" % (callsign, entry.ssid))
748
        return ",".join(path_cmp)
749

    
750
    @staticmethod
751
    def _latlong_sanity(sign, l_d, l_m, l_s, is_lat):
752
        if sign not in (0, 1):
753
            sign = 0
754
        if is_lat:
755
            d_max = 90
756
        else:
757
            d_max = 180
758
        if l_d < 0 or l_d > d_max:
759
            l_d = 0
760
            l_m = 0
761
            l_s = 0
762
        if l_m < 0 or l_m > 60:
763
            l_m = 0
764
            l_s = 0
765
        if l_s < 0 or l_s > 60:
766
            l_s = 0
767
        return sign, l_d, l_m, l_s
768

    
769
    @classmethod
770
    def _latlong_to_str(cls, sign, l_d, l_m, l_s, is_lat, to_sexigesimal=True):
771
        sign, l_d, l_m, l_s = cls._latlong_sanity(sign, l_d, l_m, l_s, is_lat)
772
        mult = sign and -1 or 1
773
        if to_sexigesimal:
774
            return "%d,%d'%d\"" % (mult * l_d, l_m, l_s)
775
        return "%0.5f" % (mult * l_d + (l_m / 60.0) + (l_s / (60.0 * 60.0)))
776

    
777
    @classmethod
778
    def _str_to_latlong(cls, lat_long, is_lat):
779
        sign = 0
780
        result = [0, 0, 0]
781

    
782
        lat_long = lat_long.strip()
783

    
784
        if not lat_long:
785
            return 1, 0, 0, 0
786

    
787
        try:
788
            # DD.MMMMM is the simple case, try that first.
789
            val = float(lat_long)
790
            if val < 0:
791
                sign = 1
792
            val = abs(val)
793
            result[0] = int(val)
794
            result[1] = int(val * 60) % 60
795
            result[2] = int(val * 3600) % 60
796
        except ValueError:
797
            # Try DD MM'SS" if DD.MMMMM failed.
798
            match = cls._SG_RE.match(lat_long.strip())
799
            if match:
800
                if match.group("sign") and (match.group("sign") in "SE-"):
801
                    sign = 1
802
                else:
803
                    sign = 0
804
                if match.group("d"):
805
                    result[0] = int(match.group("d"))
806
                if match.group("m"):
807
                    result[1] = int(match.group("m"))
808
                if match.group("s"):
809
                    result[2] = int(match.group("s"))
810
            elif len(lat_long) > 4:
811
                raise Exception("Lat/Long should be DD MM'SS\" or DD.MMMMM")
812

    
813
        return cls._latlong_sanity(sign, result[0], result[1], result[2],
814
                                   is_lat)
815

    
816
    def _get_aprs_general_settings(self):
817
        menu = RadioSettingGroup("aprs_general", "APRS General")
818
        aprs = self._memobj.aprs
819
        aprs2 = self._memobj.aprs2
820

    
821
        val = RadioSettingValueString(
822
                0, 6, str(aprs.my_callsign.callsign).rstrip("\xFF"))
823
        rs = RadioSetting("aprs.my_callsign.callsign", "My Callsign", val)
824
        rs.set_apply_callback(self.apply_callsign, aprs.my_callsign)
825
        menu.append(rs)
826

    
827
        val = RadioSettingValueList(
828
            chirp_common.APRS_SSID,
829
            chirp_common.APRS_SSID[aprs.my_callsign.ssid])
830
        rs = RadioSetting("aprs.my_callsign.ssid", "My SSID", val)
831
        menu.append(rs)
832

    
833
        val = RadioSettingValueList(self._MY_SYMBOL,
834
                                    self._MY_SYMBOL[aprs2.selected_my_symbol])
835
        rs = RadioSetting("aprs2.selected_my_symbol", "My Symbol", val)
836
        menu.append(rs)
837

    
838
        symbols = list(chirp_common.APRS_SYMBOLS)
839
        selected = aprs.custom_symbol
840
        if aprs.custom_symbol >= len(chirp_common.APRS_SYMBOLS):
841
            symbols.append("%d" % aprs.custom_symbol)
842
            selected = len(symbols) - 1
843
        val = RadioSettingValueList(symbols, symbols[selected])
844
        rs = RadioSetting("aprs.custom_symbol_text", "User Selected Symbol",
845
                          val)
846
        rs.set_apply_callback(self.apply_custom_symbol, aprs)
847
        menu.append(rs)
848

    
849
        val = RadioSettingValueList(
850
            chirp_common.APRS_POSITION_COMMENT,
851
            chirp_common.APRS_POSITION_COMMENT[aprs.selected_position_comment])
852
        rs = RadioSetting("aprs.selected_position_comment", "Position Comment",
853
                          val)
854
        menu.append(rs)
855

    
856
        latitude = self._latlong_to_str(aprs.latitude_sign,
857
                                        aprs.latitude_degree,
858
                                        aprs.latitude_minute,
859
                                        aprs.latitude_second,
860
                                        True, aprs.aprs_units_position_mmss)
861
        longitude = self._latlong_to_str(aprs.longitude_sign,
862
                                         aprs.longitude_degree,
863
                                         aprs.longitude_minute,
864
                                         aprs.longitude_second,
865
                                         False, aprs.aprs_units_position_mmss)
866

    
867
        # TODO: Rebuild this when aprs_units_position_mmss changes.
868
        # TODO: Rebuild this when latitude/longitude change.
869
        # TODO: Add saved positions p1 - p10 to memory map.
870
        position_str = list(self._POSITIONS)
871
        # position_str[1] = "%s %s" % (latitude, longitude)
872
        # position_str[2] = "%s %s" % (latitude, longitude)
873
        val = RadioSettingValueList(position_str,
874
                                    position_str[aprs.selected_position])
875
        rs = RadioSetting("aprs.selected_position", "My Position", val)
876
        menu.append(rs)
877

    
878
        val = RadioSettingValueString(0, 10, latitude)
879
        rs = RadioSetting("latitude", "Manual Latitude", val)
880
        rs.set_apply_callback(self.apply_lat_long, aprs)
881
        menu.append(rs)
882

    
883
        val = RadioSettingValueString(0, 11, longitude)
884
        rs = RadioSetting("longitude", "Manual Longitude", val)
885
        rs.set_apply_callback(self.apply_lat_long, aprs)
886
        menu.append(rs)
887

    
888
        val = RadioSettingValueList(
889
                self._TIME_SOURCE, self._TIME_SOURCE[aprs.set_time_manually])
890
        rs = RadioSetting("aprs.set_time_manually", "Time Source", val)
891
        menu.append(rs)
892

    
893
        val = RadioSettingValueList(self._TZ, self._TZ[aprs.timezone])
894
        rs = RadioSetting("aprs.timezone", "Timezone", val)
895
        menu.append(rs)
896

    
897
        val = RadioSettingValueList(
898
                self._SPEED_UNITS, self._SPEED_UNITS[aprs.aprs_units_speed])
899
        rs = RadioSetting("aprs.aprs_units_speed", "APRS Speed Units", val)
900
        menu.append(rs)
901

    
902
        val = RadioSettingValueList(
903
                self._SPEED_UNITS, self._SPEED_UNITS[aprs.gps_units_speed])
904
        rs = RadioSetting("aprs.gps_units_speed", "GPS Speed Units", val)
905
        menu.append(rs)
906

    
907
        val = RadioSettingValueList(
908
                self._ALT_UNITS, self._ALT_UNITS[aprs.aprs_units_altitude_ft])
909
        rs = RadioSetting("aprs.aprs_units_altitude_ft", "APRS Altitude Units",
910
                          val)
911
        menu.append(rs)
912

    
913
        val = RadioSettingValueList(
914
                self._ALT_UNITS, self._ALT_UNITS[aprs.gps_units_altitude_ft])
915
        rs = RadioSetting("aprs.gps_units_altitude_ft", "GPS Altitude Units",
916
                          val)
917
        menu.append(rs)
918

    
919
        val = RadioSettingValueList(
920
                self._POS_UNITS,
921
                self._POS_UNITS[aprs.aprs_units_position_mmss])
922
        rs = RadioSetting("aprs.aprs_units_position_mmss",
923
                          "APRS Position Format", val)
924
        menu.append(rs)
925

    
926
        val = RadioSettingValueList(
927
                self._POS_UNITS, self._POS_UNITS[aprs.gps_units_position_sss])
928
        rs = RadioSetting("aprs.gps_units_position_sss",
929
                          "GPS Position Format", val)
930
        menu.append(rs)
931

    
932
        val = RadioSettingValueList(
933
                self._DIST_UNITS,
934
                self._DIST_UNITS[aprs.aprs_units_distance_m])
935
        rs = RadioSetting("aprs.aprs_units_distance_m", "APRS Distance Units",
936
                          val)
937
        menu.append(rs)
938

    
939
        val = RadioSettingValueList(
940
                self._WIND_UNITS, self._WIND_UNITS[aprs.aprs_units_wind_mph])
941
        rs = RadioSetting("aprs.aprs_units_wind_mph", "APRS Wind Speed Units",
942
                          val)
943
        menu.append(rs)
944

    
945
        val = RadioSettingValueList(
946
                self._RAIN_UNITS, self._RAIN_UNITS[aprs.aprs_units_rain_inch])
947
        rs = RadioSetting("aprs.aprs_units_rain_inch", "APRS Rain Units", val)
948
        menu.append(rs)
949

    
950
        val = RadioSettingValueList(
951
                self._TEMP_UNITS,
952
                self._TEMP_UNITS[aprs.aprs_units_temperature_f])
953
        rs = RadioSetting("aprs.aprs_units_temperature_f",
954
                          "APRS Temperature Units", val)
955
        menu.append(rs)
956

    
957
        return menu
958

    
959
    def _get_aprs_rx_settings(self):
960
        menu = RadioSettingGroup("aprs_rx", "APRS Receive")
961
        aprs = self._memobj.aprs
962
        aprs2 = self._memobj.aprs2
963

    
964
        val = RadioSettingValueList(self._RX_BAUD, self._RX_BAUD[aprs.rx_baud])
965
        rs = RadioSetting("aprs.rx_baud", "Modem RX", val)
966
        menu.append(rs)
967

    
968
        val = RadioSettingValueBoolean(aprs.aprs_mute)
969
        rs = RadioSetting("aprs.aprs_mute", "APRS Mute", val)
970
        menu.append(rs)
971

    
972
        if self._has_af_dual:
973
            val = RadioSettingValueBoolean(aprs.af_dual)
974
            rs = RadioSetting("aprs.af_dual", "AF Dual", val)
975
            menu.append(rs)
976

    
977
        val = RadioSettingValueBoolean(aprs.ring_msg)
978
        rs = RadioSetting("aprs.ring_msg", "Ring on Message RX", val)
979
        menu.append(rs)
980

    
981
        val = RadioSettingValueBoolean(aprs.ring_beacon)
982
        rs = RadioSetting("aprs.ring_beacon", "Ring on Beacon RX", val)
983
        menu.append(rs)
984

    
985
        val = RadioSettingValueList(self._FLASH,
986
                                    self._FLASH[aprs2.flash_msg])
987
        rs = RadioSetting("aprs2.flash_msg", "Flash on personal message", val)
988
        menu.append(rs)
989

    
990
        if self._has_vibrate:
991
            val = RadioSettingValueList(self._FLASH,
992
                                        self._FLASH[aprs2.vibrate_msg])
993
            rs = RadioSetting("aprs2.vibrate_msg",
994
                              "Vibrate on personal message", val)
995
            menu.append(rs)
996

    
997
        val = RadioSettingValueList(self._FLASH[:10],
998
                                    self._FLASH[aprs2.flash_bln])
999
        rs = RadioSetting("aprs2.flash_bln", "Flash on bulletin message", val)
1000
        menu.append(rs)
1001

    
1002
        if self._has_vibrate:
1003
            val = RadioSettingValueList(self._FLASH[:10],
1004
                                        self._FLASH[aprs2.vibrate_bln])
1005
            rs = RadioSetting("aprs2.vibrate_bln",
1006
                              "Vibrate on bulletin message", val)
1007
            menu.append(rs)
1008

    
1009
        val = RadioSettingValueList(self._FLASH[:10],
1010
                                    self._FLASH[aprs2.flash_grp])
1011
        rs = RadioSetting("aprs2.flash_grp", "Flash on group message", val)
1012
        menu.append(rs)
1013

    
1014
        if self._has_vibrate:
1015
            val = RadioSettingValueList(self._FLASH[:10],
1016
                                        self._FLASH[aprs2.vibrate_grp])
1017
            rs = RadioSetting("aprs2.vibrate_grp",
1018
                              "Vibrate on group message", val)
1019
            menu.append(rs)
1020

    
1021
        filter_val = [m.padded_string for m in aprs2.msg_group]
1022
        filter_val = self._strip_ff_pads(filter_val)
1023
        for index, filter_text in enumerate(filter_val):
1024
            val = RadioSettingValueString(0, 9, filter_text)
1025
            rs = RadioSetting("aprs2.msg_group_%d" % index,
1026
                              "Message Group %d" % (index + 1), val)
1027
            menu.append(rs)
1028
            rs.set_apply_callback(self.apply_ff_padded_string,
1029
                                  aprs2.msg_group[index])
1030
        # TODO: Use filter_val as the list entries and update it on edit.
1031
        val = RadioSettingValueList(
1032
            self._MSG_GROUP_NAMES,
1033
            self._MSG_GROUP_NAMES[aprs2.selected_msg_group])
1034
        rs = RadioSetting("aprs2.selected_msg_group", "Selected Message Group",
1035
                          val)
1036
        menu.append(rs)
1037

    
1038
        val = RadioSettingValueBoolean(aprs.filter_mic_e)
1039
        rs = RadioSetting("aprs.filter_mic_e", "Receive Mic-E Beacons", val)
1040
        menu.append(rs)
1041

    
1042
        val = RadioSettingValueBoolean(aprs.filter_position)
1043
        rs = RadioSetting("aprs.filter_position", "Receive Position Beacons",
1044
                          val)
1045
        menu.append(rs)
1046

    
1047
        val = RadioSettingValueBoolean(aprs.filter_weather)
1048
        rs = RadioSetting("aprs.filter_weather", "Receive Weather Beacons",
1049
                          val)
1050
        menu.append(rs)
1051

    
1052
        val = RadioSettingValueBoolean(aprs.filter_object)
1053
        rs = RadioSetting("aprs.filter_object", "Receive Object Beacons", val)
1054
        menu.append(rs)
1055

    
1056
        val = RadioSettingValueBoolean(aprs.filter_item)
1057
        rs = RadioSetting("aprs.filter_item", "Receive Item Beacons", val)
1058
        menu.append(rs)
1059

    
1060
        val = RadioSettingValueBoolean(aprs.filter_status)
1061
        rs = RadioSetting("aprs.filter_status", "Receive Status Beacons", val)
1062
        menu.append(rs)
1063

    
1064
        val = RadioSettingValueBoolean(aprs.filter_other)
1065
        rs = RadioSetting("aprs.filter_other", "Receive Other Beacons", val)
1066
        menu.append(rs)
1067

    
1068
        return menu
1069

    
1070
    def _get_aprs_tx_settings(self):
1071
        menu = RadioSettingGroup("aprs_tx", "APRS Transmit")
1072
        aprs = self._memobj.aprs
1073
        aprs2 = self._memobj.aprs2
1074

    
1075
        beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon
1076
        val = RadioSettingValueList(
1077
                self._BEACON_TYPE, self._BEACON_TYPE[beacon_type])
1078
        rs = RadioSetting("aprs.transmit", "TX Beacons", val)
1079
        rs.set_apply_callback(self.apply_beacon_type, aprs)
1080
        menu.append(rs)
1081

    
1082
        val = RadioSettingValueList(
1083
                self._TX_DELAY, self._TX_DELAY[aprs.tx_delay])
1084
        rs = RadioSetting("aprs.tx_delay", "TX Delay", val)
1085
        menu.append(rs)
1086

    
1087
        val = RadioSettingValueList(
1088
                self._BEACON_INT, self._BEACON_INT[aprs.beacon_interval])
1089
        rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val)
1090
        menu.append(rs)
1091

    
1092
        desc = []
1093
        status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt]
1094
        status = self._strip_ff_pads(status)
1095
        for index, msg_text in enumerate(status):
1096
            val = RadioSettingValueString(0, 60, msg_text)
1097
            desc.append("Beacon Status Text %d" % (index + 1))
1098
            rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1],
1099
                              val)
1100
            rs.set_apply_callback(self.apply_ff_padded_string,
1101
                                  self._memobj.aprs_beacon_status_txt[index])
1102
            menu.append(rs)
1103
        val = RadioSettingValueList(desc,
1104
                                    desc[aprs.selected_beacon_status_txt])
1105
        rs = RadioSetting("aprs.selected_beacon_status_txt",
1106
                          "Beacon Status Text", val)
1107
        menu.append(rs)
1108

    
1109
        message_macro = [m.padded_string for m in self._memobj.aprs_msg_macro]
1110
        message_macro = self._strip_ff_pads(message_macro)
1111
        for index, msg_text in enumerate(message_macro):
1112
            val = RadioSettingValueString(0, 16, msg_text)
1113
            rs = RadioSetting("aprs_msg_macro_%d" % index,
1114
                              "Message Macro %d" % (index + 1), val)
1115
            rs.set_apply_callback(self.apply_ff_padded_string,
1116
                                  self._memobj.aprs_msg_macro[index])
1117
            menu.append(rs)
1118

    
1119
        path_str = list(self._DIGI_PATHS)
1120

    
1121
        path_str[7] = self._digi_path_to_str(aprs.digi_path_7)
1122
        val = RadioSettingValueString(0, 88, path_str[7])
1123
        rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val)
1124
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7)
1125
        menu.append(rs)
1126

    
1127
        # Show friendly messages for empty slots rather than blanks.
1128
        # TODO: Rebuild this when digi_path_[34567] change.
1129
        # path_str[3] = path_str[3] or self._DIGI_PATHS[3]
1130
        # path_str[4] = path_str[4] or self._DIGI_PATHS[4]
1131
        # path_str[5] = path_str[5] or self._DIGI_PATHS[5]
1132
        # path_str[6] = path_str[6] or self._DIGI_PATHS[6]
1133
        # path_str[7] = path_str[7] or self._DIGI_PATHS[7]
1134

    
1135
        path_str[7] = self._DIGI_PATHS[7]
1136
        val = RadioSettingValueList(path_str,
1137
                                    path_str[aprs2.selected_digi_path])
1138
        rs = RadioSetting("aprs2.selected_digi_path",
1139
                          "Selected Digi Path", val)
1140
        menu.append(rs)
1141

    
1142
        return menu
1143

    
1144
    def _get_dtmf_settings(self):
1145
        menu = RadioSettingGroup("dtmf_settings", "DTMF")
1146
        dtmf = self._memobj.scan_settings
1147

    
1148
        val = RadioSettingValueList(
1149
            self._DTMF_MODE,
1150
            self._DTMF_MODE[dtmf.dtmf_mode])
1151
        rs = RadioSetting("scan_settings.dtmf_mode", "DTMF Mode", val)
1152
        menu.append(rs)
1153

    
1154
        val = RadioSettingValueList(
1155
            self._DTMF_SPEED,
1156
            self._DTMF_SPEED[dtmf.dtmf_speed])
1157
        rs = RadioSetting("scan_settings.dtmf_speed",
1158
                          "DTMF AutoDial Speed", val)
1159
        menu.append(rs)
1160

    
1161
        val = RadioSettingValueList(
1162
            self._DTMF_DELAY,
1163
            self._DTMF_DELAY[dtmf.dtmf_delay])
1164
        rs = RadioSetting("scan_settings.dtmf_delay",
1165
                          "DTMF AutoDial Delay", val)
1166
        menu.append(rs)
1167

    
1168
        for i in range(10):
1169
            name = "dtmf_%02d" % i
1170
            dtmfsetting = self._memobj.dtmf[i]
1171
            dtmfstr = ""
1172
            for c in dtmfsetting.memory:
1173
                if c == 0xFF:
1174
                    break
1175
                if c < len(VX8_DTMF_CHARS):
1176
                    dtmfstr += VX8_DTMF_CHARS[c]
1177
            dtmfentry = RadioSettingValueString(0, 16, dtmfstr)
1178
            dtmfentry.set_charset(VX8_DTMF_CHARS + list("abcd "))
1179
            rs = RadioSetting(name, name.upper(), dtmfentry)
1180
            rs.set_apply_callback(self.apply_dtmf, i)
1181
            menu.append(rs)
1182

    
1183
        return menu
1184

    
1185
    def _get_misc_settings(self):
1186
        menu = RadioSettingGroup("misc_settings", "Misc")
1187
        scan_settings = self._memobj.scan_settings
1188

    
1189
        val = RadioSettingValueList(
1190
            self._LCD_DIMMER,
1191
            self._LCD_DIMMER[scan_settings.lcd_dimmer])
1192
        rs = RadioSetting("scan_settings.lcd_dimmer", "LCD Dimmer", val)
1193
        menu.append(rs)
1194

    
1195
        val = RadioSettingValueList(
1196
            self._LCD_CONTRAST,
1197
            self._LCD_CONTRAST[scan_settings.lcd_contrast - 1])
1198
        rs = RadioSetting("scan_settings.lcd_contrast", "LCD Contrast",
1199
                          val)
1200
        rs.set_apply_callback(self.apply_lcd_contrast, scan_settings)
1201
        menu.append(rs)
1202

    
1203
        val = RadioSettingValueList(
1204
            self._LAMP_KEY,
1205
            self._LAMP_KEY[scan_settings.lamp])
1206
        rs = RadioSetting("scan_settings.lamp", "Lamp", val)
1207
        menu.append(rs)
1208

    
1209
        beep_select = self._memobj.beep_select
1210

    
1211
        val = RadioSettingValueList(
1212
            self._BEEP_SELECT,
1213
            self._BEEP_SELECT[beep_select.beep])
1214
        rs = RadioSetting("beep_select.beep", "Beep Select", val)
1215
        menu.append(rs)
1216

    
1217
        opening_message = self._memobj.opening_message
1218

    
1219
        val = RadioSettingValueList(
1220
            self._OPENING_MESSAGE,
1221
            self._OPENING_MESSAGE[opening_message.flag])
1222
        rs = RadioSetting("opening_message.flag", "Opening Msg Mode",
1223
                          val)
1224
        menu.append(rs)
1225

    
1226
        msg = ""
1227
        for i in opening_message.message.padded_yaesu:
1228
            if i == 0xFF:
1229
                break
1230
            msg += CHARSET[i & 0x7F]
1231
        val = RadioSettingValueString(0, 16, msg)
1232
        rs = RadioSetting("opening_message.message.padded_yaesu",
1233
                          "Opening Message", val)
1234
        rs.set_apply_callback(self.apply_ff_padded_yaesu,
1235
                              opening_message.message)
1236
        menu.append(rs)
1237

    
1238
        return menu
1239

    
1240
    def _get_scan_settings(self):
1241
        menu = RadioSettingGroup("scan_settings", "Scan")
1242
        scan_settings = self._memobj.scan_settings
1243

    
1244
        val = RadioSettingValueList(
1245
            self._VOL_MODE,
1246
            self._VOL_MODE[scan_settings.vol_mode])
1247
        rs = RadioSetting("scan_settings.vol_mode", "Volume Mode", val)
1248
        menu.append(rs)
1249

    
1250
        vfoa = self._memobj.vfo_info[0]
1251
        val = RadioSettingValueList(
1252
            self._VOLUME,
1253
            self._VOLUME[vfoa.volume])
1254
        rs = RadioSetting("vfo_info[0].volume", "VFO A Volume", val)
1255
        rs.set_apply_callback(self.apply_volume, 0)
1256
        menu.append(rs)
1257

    
1258
        vfob = self._memobj.vfo_info[1]
1259
        val = RadioSettingValueList(
1260
            self._VOLUME,
1261
            self._VOLUME[vfob.volume])
1262
        rs = RadioSetting("vfo_info[1].volume", "VFO B Volume", val)
1263
        rs.set_apply_callback(self.apply_volume, 1)
1264
        menu.append(rs)
1265

    
1266
        squelch = self._memobj.squelch
1267
        val = RadioSettingValueList(
1268
            self._SQUELCH,
1269
            self._SQUELCH[squelch.vfo_a])
1270
        rs = RadioSetting("squelch.vfo_a", "VFO A Squelch", val)
1271
        menu.append(rs)
1272

    
1273
        val = RadioSettingValueList(
1274
            self._SQUELCH,
1275
            self._SQUELCH[squelch.vfo_b])
1276
        rs = RadioSetting("squelch.vfo_b", "VFO B Squelch", val)
1277
        menu.append(rs)
1278

    
1279
        val = RadioSettingValueList(
1280
            self._SCAN_RESTART,
1281
            self._SCAN_RESTART[scan_settings.scan_restart])
1282
        rs = RadioSetting("scan_settings.scan_restart", "Scan Restart", val)
1283
        menu.append(rs)
1284

    
1285
        val = RadioSettingValueList(
1286
            self._SCAN_RESUME,
1287
            self._SCAN_RESUME[scan_settings.scan_resume])
1288
        rs = RadioSetting("scan_settings.scan_resume", "Scan Resume", val)
1289
        menu.append(rs)
1290

    
1291
        val = RadioSettingValueList(
1292
            self._OFF_ON,
1293
            self._OFF_ON[scan_settings.busy_led])
1294
        rs = RadioSetting("scan_settings.busy_led", "Busy LED", val)
1295
        menu.append(rs)
1296

    
1297
        val = RadioSettingValueList(
1298
            self._OFF_ON,
1299
            self._OFF_ON[scan_settings.scan_lamp])
1300
        rs = RadioSetting("scan_settings.scan_lamp", "Scan Lamp", val)
1301
        menu.append(rs)
1302

    
1303
        val = RadioSettingValueList(
1304
            self._TOT_TIME,
1305
            self._TOT_TIME[scan_settings.tot])
1306
        rs = RadioSetting("scan_settings.tot", "Transmit Timeout (TOT)", val)
1307
        menu.append(rs)
1308

    
1309
        return menu
1310

    
1311
    def _get_settings(self):
1312
        top = RadioSettings(self._get_aprs_general_settings(),
1313
                            self._get_aprs_rx_settings(),
1314
                            self._get_aprs_tx_settings(),
1315
                            self._get_dtmf_settings(),
1316
                            self._get_misc_settings(),
1317
                            self._get_scan_settings())
1318
        return top
1319

    
1320
    def get_settings(self):
1321
        try:
1322
            return self._get_settings()
1323
        except:
1324
            import traceback
1325
            LOG.error("Failed to parse settings: %s", traceback.format_exc())
1326
            return None
1327

    
1328
    @staticmethod
1329
    def apply_custom_symbol(setting, obj):
1330
        # Ensure new value falls within known bounds, otherwise leave it as
1331
        # it's a custom value from the radio that's outside our list.
1332
        if setting.value.get_value() in chirp_common.APRS_SYMBOLS:
1333
            setattr(obj, "custom_symbol",
1334
                    chirp_common.APRS_SYMBOLS.index(setting.value.get_value()))
1335

    
1336
    @classmethod
1337
    def _apply_callsign(cls, callsign, obj, default_ssid=None):
1338
        ssid = default_ssid
1339
        dash_index = callsign.find("-")
1340
        if dash_index >= 0:
1341
            ssid = callsign[dash_index + 1:]
1342
            callsign = callsign[:dash_index]
1343
            try:
1344
                ssid = int(ssid) % 16
1345
            except ValueError:
1346
                ssid = default_ssid
1347
        setattr(obj, "callsign", cls._add_ff_pad(callsign, 6))
1348
        if ssid is not None:
1349
            setattr(obj, "ssid", ssid)
1350

    
1351
    def apply_beacon_type(cls, setting, obj):
1352
        beacon_type = str(setting.value.get_value())
1353
        beacon_index = cls._BEACON_TYPE.index(beacon_type)
1354
        tx_smartbeacon = beacon_index >> 1
1355
        tx_interval_beacon = beacon_index & 1
1356
        if tx_interval_beacon:
1357
            setattr(obj, "tx_interval_beacon", 1)
1358
            setattr(obj, "tx_smartbeacon", 0)
1359
        elif tx_smartbeacon:
1360
            setattr(obj, "tx_interval_beacon", 0)
1361
            setattr(obj, "tx_smartbeacon", 1)
1362
        else:
1363
            setattr(obj, "tx_interval_beacon", 0)
1364
            setattr(obj, "tx_smartbeacon", 0)
1365

    
1366
    @classmethod
1367
    def apply_callsign(cls, setting, obj, default_ssid=None):
1368
        # Uppercase, strip SSID then FF pad to max string length.
1369
        callsign = setting.value.get_value().upper()
1370
        cls._apply_callsign(callsign, obj, default_ssid)
1371

    
1372
    def apply_digi_path(self, setting, obj):
1373
        # Parse and map to aprs.digi_path_4_7[0-3] or aprs.digi_path_8
1374
        # and FF terminate.
1375
        path = str(setting.value.get_value())
1376
        callsigns = [c.strip() for c in path.split(",")]
1377
        for index in range(len(obj.entry)):
1378
            try:
1379
                self._apply_callsign(callsigns[index], obj.entry[index], 0)
1380
            except IndexError:
1381
                self._apply_callsign("", obj.entry[index], 0)
1382
        if len(callsigns) > len(obj.entry):
1383
            raise Exception("This path only supports %d entries" % (index + 1))
1384

    
1385
    @classmethod
1386
    def apply_ff_padded_string(cls, setting, obj):
1387
        # FF pad.
1388
        val = setting.value.get_value()
1389
        max_len = getattr(obj, "padded_string").size() / 8
1390
        val = str(val).rstrip()
1391
        setattr(obj, "padded_string", cls._add_ff_pad(val, max_len))
1392

    
1393
    @classmethod
1394
    def apply_lat_long(cls, setting, obj):
1395
        name = setting.get_name()
1396
        is_latitude = name.endswith("latitude")
1397
        lat_long = setting.value.get_value().strip()
1398
        sign, l_d, l_m, l_s = cls._str_to_latlong(lat_long, is_latitude)
1399
        LOG.debug("%s: %d %d %d %d" % (name, sign, l_d, l_m, l_s))
1400
        setattr(obj, "%s_sign" % name, sign)
1401
        setattr(obj, "%s_degree" % name, l_d)
1402
        setattr(obj, "%s_minute" % name, l_m)
1403
        setattr(obj, "%s_second" % name, l_s)
1404

    
1405
    def set_settings(self, settings):
1406
        _mem = self._memobj
1407
        for element in settings:
1408
            if not isinstance(element, RadioSetting):
1409
                self.set_settings(element)
1410
                continue
1411
            if not element.changed():
1412
                continue
1413
            try:
1414
                if element.has_apply_callback():
1415
                    LOG.debug("Using apply callback")
1416
                    try:
1417
                        element.run_apply_callback()
1418
                    except NotImplementedError as e:
1419
                        LOG.error("vx8.set_settings: %s", e)
1420
                    continue
1421

    
1422
                # Find the object containing setting.
1423
                obj = _mem
1424
                bits = element.get_name().split(".")
1425
                setting = bits[-1]
1426
                for name in bits[:-1]:
1427
                    if name.endswith("]"):
1428
                        name, index = name.split("[")
1429
                        index = int(index[:-1])
1430
                        obj = getattr(obj, name)[index]
1431
                    else:
1432
                        obj = getattr(obj, name)
1433

    
1434
                try:
1435
                    old_val = getattr(obj, setting)
1436
                    LOG.debug("Setting %s(%r) <= %s" % (
1437
                            element.get_name(), old_val, element.value))
1438
                    setattr(obj, setting, element.value)
1439
                except AttributeError as e:
1440
                    LOG.error("Setting %s is not in the memory map: %s" %
1441
                              (element.get_name(), e))
1442
            except Exception, e:
1443
                LOG.debug(element.get_name())
1444
                raise
1445

    
1446
    def apply_ff_padded_yaesu(cls, setting, obj):
1447
        # FF pad yaesus custom string format.
1448
        rawval = setting.value.get_value()
1449
        max_len = getattr(obj, "padded_yaesu").size() / 8
1450
        rawval = str(rawval).rstrip()
1451
        val = [CHARSET.index(x) for x in rawval]
1452
        for x in range(len(val), max_len):
1453
            val.append(0xFF)
1454
        obj.padded_yaesu = val
1455

    
1456
    def apply_volume(cls, setting, vfo):
1457
        val = setting.value.get_value()
1458
        cls._memobj.vfo_info[(vfo*2)].volume = val
1459
        cls._memobj.vfo_info[(vfo*2)+1].volume = val
1460

    
1461
    def apply_lcd_contrast(cls, setting, obj):
1462
        rawval = setting.value.get_value()
1463
        val = cls._LCD_CONTRAST.index(rawval) + 1
1464
        obj.lcd_contrast = val
1465

    
1466
    def apply_dtmf(cls, setting, i):
1467
        rawval = setting.value.get_value().upper().rstrip()
1468
        val = [VX8_DTMF_CHARS.index(x) for x in rawval]
1469
        for x in range(len(val), 16):
1470
            val.append(0xFF)
1471
        cls._memobj.dtmf[i].memory = val
1472

    
1473

    
1474
@directory.register
1475
class VX8DRadio(VX8Radio):
1476
    """Yaesu VX-8DR"""
1477
    MODEL = "VX-8DR"
1478
    _model = "AH29D"
1479
    _mem_params = (0xC128,  # APRS message macros
1480
                   7,       # Number of message macros
1481
                   0xC198,  # APRS2
1482
                   0xC24A,  # APRS beacon metadata address.
1483
                   50,      # Number of beacons stored.
1484
                   0xC6FA,  # APRS beacon content address.
1485
                   146,     # Length of beacon data stored.
1486
                   50)      # Number of beacons stored.
1487

    
1488
    _TX_DELAY = ("100ms", "150ms", "200ms", "250ms", "300ms",
1489
                 "400ms", "500ms", "750ms", "1000ms")
1490
    _BEACON_TYPE = ("Off", "Interval", "SmartBeaconing")
1491
    _SMARTBEACON_PROFILE = ("Off", "Type 1", "Type 2", "Type 3")
1492
    _POSITIONS = ("GPS", "Manual Latitude/Longitude",
1493
                  "Manual Latitude/Longitude", "P1", "P2", "P3", "P4",
1494
                  "P5", "P6", "P7", "P8", "P9")
1495
    _FLASH = ("OFF", "2 seconds", "4 seconds", "6 seconds", "8 seconds",
1496
              "10 seconds", "20 seconds", "30 seconds", "60 seconds",
1497
              "CONTINUOUS", "every 2 seconds", "every 3 seconds",
1498
              "every 4 seconds", "every 5 seconds", "every 6 seconds",
1499
              "every 7 seconds", "every 8 seconds", "every 9 seconds",
1500
              "every 10 seconds", "every 20 seconds", "every 30 seconds",
1501
              "every 40 seconds", "every 50 seconds", "every minute",
1502
              "every 2 minutes", "every 3 minutes", "every 4 minutes",
1503
              "every 5 minutes", "every 6 minutes", "every 7 minutes",
1504
              "every 8 minutes", "every 9 minutes", "every 10 minutes")
1505
    _LCD_CONTRAST = ["Level %d" % x for x in range(1, 16)]
1506
    _MY_SYMBOL = ("/[ Person", "/b Bike", "/> Car", "User selected")
1507

    
1508
    def _get_aprs_tx_settings(self):
1509
        menu = RadioSettingGroup("aprs_tx", "APRS Transmit")
1510
        aprs = self._memobj.aprs
1511
        aprs2 = self._memobj.aprs2
1512

    
1513
        beacon_type = (aprs.tx_smartbeacon << 1) | aprs.tx_interval_beacon
1514
        val = RadioSettingValueList(
1515
                self._BEACON_TYPE, self._BEACON_TYPE[beacon_type])
1516
        rs = RadioSetting("aprs.transmit", "TX Beacons", val)
1517
        rs.set_apply_callback(self.apply_beacon_type, aprs)
1518
        menu.append(rs)
1519

    
1520
        val = RadioSettingValueList(
1521
                self._TX_DELAY, self._TX_DELAY[aprs.tx_delay])
1522
        rs = RadioSetting("aprs.tx_delay", "TX Delay", val)
1523
        menu.append(rs)
1524

    
1525
        val = RadioSettingValueList(
1526
                self._BEACON_INT, self._BEACON_INT[aprs.beacon_interval])
1527
        rs = RadioSetting("aprs.beacon_interval", "Beacon Interval", val)
1528
        menu.append(rs)
1529

    
1530
        desc = []
1531
        status = [m.padded_string for m in self._memobj.aprs_beacon_status_txt]
1532
        status = self._strip_ff_pads(status)
1533
        for index, msg_text in enumerate(status):
1534
            val = RadioSettingValueString(0, 60, msg_text)
1535
            desc.append("Beacon Status Text %d" % (index + 1))
1536
            rs = RadioSetting("aprs_beacon_status_txt_%d" % index, desc[-1],
1537
                              val)
1538
            rs.set_apply_callback(self.apply_ff_padded_string,
1539
                                  self._memobj.aprs_beacon_status_txt[index])
1540
            menu.append(rs)
1541
        val = RadioSettingValueList(desc,
1542
                                    desc[aprs.selected_beacon_status_txt])
1543
        rs = RadioSetting("aprs.selected_beacon_status_txt",
1544
                          "Beacon Status Text", val)
1545
        menu.append(rs)
1546

    
1547
        message_macro = [m.padded_string for m in self._memobj.aprs_msg_macro]
1548
        message_macro = self._strip_ff_pads(message_macro)
1549
        for index, msg_text in enumerate(message_macro):
1550
            val = RadioSettingValueString(0, 16, msg_text)
1551
            rs = RadioSetting("aprs_msg_macro_%d" % index,
1552
                              "Message Macro %d" % (index + 1), val)
1553
            rs.set_apply_callback(self.apply_ff_padded_string,
1554
                                  self._memobj.aprs_msg_macro[index])
1555
            menu.append(rs)
1556

    
1557
        path_str = list(self._DIGI_PATHS)
1558
        path_str[3] = self._digi_path_to_str(aprs2.digi_path_3_6[0])
1559
        val = RadioSettingValueString(0, 22, path_str[3])
1560
        rs = RadioSetting("aprs2.digi_path_3", "Digi Path 4 (2 entries)", val)
1561
        rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[0])
1562
        menu.append(rs)
1563

    
1564
        path_str[4] = self._digi_path_to_str(aprs2.digi_path_3_6[1])
1565
        val = RadioSettingValueString(0, 22, path_str[4])
1566
        rs = RadioSetting("aprs2.digi_path_4", "Digi Path 5 (2 entries)", val)
1567
        rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[1])
1568
        menu.append(rs)
1569

    
1570
        path_str[5] = self._digi_path_to_str(aprs2.digi_path_3_6[2])
1571
        val = RadioSettingValueString(0, 22, path_str[5])
1572
        rs = RadioSetting("aprs2.digi_path_5", "Digi Path 6 (2 entries)", val)
1573
        rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[2])
1574
        menu.append(rs)
1575

    
1576
        path_str[6] = self._digi_path_to_str(aprs2.digi_path_3_6[3])
1577
        val = RadioSettingValueString(0, 22, path_str[6])
1578
        rs = RadioSetting("aprs2.digi_path_6", "Digi Path 7 (2 entries)", val)
1579
        rs.set_apply_callback(self.apply_digi_path, aprs2.digi_path_3_6[3])
1580
        menu.append(rs)
1581

    
1582
        path_str[7] = self._digi_path_to_str(aprs.digi_path_7)
1583
        val = RadioSettingValueString(0, 88, path_str[7])
1584
        rs = RadioSetting("aprs.digi_path_7", "Digi Path 8 (8 entries)", val)
1585
        rs.set_apply_callback(self.apply_digi_path, aprs.digi_path_7)
1586
        menu.append(rs)
1587

    
1588
        # Show friendly messages for empty slots rather than blanks.
1589
        # TODO: Rebuild this when digi_path_[34567] change.
1590
        # path_str[3] = path_str[3] or self._DIGI_PATHS[3]
1591
        # path_str[4] = path_str[4] or self._DIGI_PATHS[4]
1592
        # path_str[5] = path_str[5] or self._DIGI_PATHS[5]
1593
        # path_str[6] = path_str[6] or self._DIGI_PATHS[6]
1594
        # path_str[7] = path_str[7] or self._DIGI_PATHS[7]
1595
        path_str[3] = self._DIGI_PATHS[3]
1596
        path_str[4] = self._DIGI_PATHS[4]
1597
        path_str[5] = self._DIGI_PATHS[5]
1598
        path_str[6] = self._DIGI_PATHS[6]
1599
        path_str[7] = self._DIGI_PATHS[7]
1600
        val = RadioSettingValueList(path_str,
1601
                                    path_str[aprs2.selected_digi_path])
1602
        rs = RadioSetting("aprs2.selected_digi_path",
1603
                          "Selected Digi Path", val)
1604
        menu.append(rs)
1605

    
1606
        return menu
1607

    
1608
    def _get_aprs_smartbeacon(self):
1609
        menu = RadioSettingGroup("aprs_smartbeacon", "APRS SmartBeacon")
1610
        aprs2 = self._memobj.aprs2
1611

    
1612
        val = RadioSettingValueList(
1613
           self._SMARTBEACON_PROFILE,
1614
           self._SMARTBEACON_PROFILE[aprs2.active_smartbeaconing])
1615
        rs = RadioSetting("aprs2.active_smartbeaconing",
1616
                          "SmartBeacon profile", val)
1617
        menu.append(rs)
1618

    
1619
        for profile in range(3):
1620
            pfx = "type%d" % (profile + 1)
1621
            path = "aprs2.smartbeaconing_profile[%d]" % profile
1622
            prof = aprs2.smartbeaconing_profile[profile]
1623

    
1624
            low_val = RadioSettingValueInteger(2, 30, prof.low_speed_mph)
1625
            high_val = RadioSettingValueInteger(3, 70, prof.high_speed_mph)
1626
            low_val.get_max = lambda: min(30, int(high_val.get_value()) - 1)
1627

    
1628
            rs = RadioSetting("%s.low_speed_mph" % path,
1629
                              "%s Low Speed (mph)" % pfx, low_val)
1630
            menu.append(rs)
1631

    
1632
            rs = RadioSetting("%s.high_speed_mph" % path,
1633
                              "%s High Speed (mph)" % pfx, high_val)
1634
            menu.append(rs)
1635

    
1636
            val = RadioSettingValueInteger(1, 100, prof.slow_rate_min)
1637
            rs = RadioSetting("%s.slow_rate_min" % path,
1638
                              "%s Slow rate (minutes)" % pfx, val)
1639
            menu.append(rs)
1640

    
1641
            val = RadioSettingValueInteger(10, 180, prof.fast_rate_sec)
1642
            rs = RadioSetting("%s.fast_rate_sec" % path,
1643
                              "%s Fast rate (seconds)" % pfx, val)
1644
            menu.append(rs)
1645

    
1646
            val = RadioSettingValueInteger(5, 90, prof.turn_angle)
1647
            rs = RadioSetting("%s.turn_angle" % path,
1648
                              "%s Turn angle (degrees)" % pfx, val)
1649
            menu.append(rs)
1650

    
1651
            val = RadioSettingValueInteger(1, 255, prof.turn_slop)
1652
            rs = RadioSetting("%s.turn_slop" % path,
1653
                              "%s Turn slop" % pfx, val)
1654
            menu.append(rs)
1655

    
1656
            val = RadioSettingValueInteger(5, 180, prof.turn_time_sec)
1657
            rs = RadioSetting("%s.turn_time_sec" % path,
1658
                              "%s Turn time (seconds)" % pfx, val)
1659
            menu.append(rs)
1660

    
1661
        return menu
1662

    
1663
    def _get_settings(self):
1664
        top = RadioSettings(self._get_aprs_general_settings(),
1665
                            self._get_aprs_rx_settings(),
1666
                            self._get_aprs_tx_settings(),
1667
                            self._get_aprs_smartbeacon(),
1668
                            self._get_dtmf_settings(),
1669
                            self._get_misc_settings(),
1670
                            self._get_scan_settings())
1671
        return top
1672

    
1673

    
1674
@directory.register
1675
class VX8GERadio(VX8DRadio):
1676
    """Yaesu VX-8GE"""
1677
    MODEL = "VX-8GE"
1678
    _model = "AH041"
1679
    _has_vibrate = True
1680
    _has_af_dual = False
(5-5/5)