Project

General

Profile

Bug #10398 » tg_uv2p.py

8e25a14b - Dan Smith, 02/28/2023 01:42 AM

 
1
# Copyright 2013 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 2 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
# This driver was derived from the:
17
# Quansheng TG-UV2 Utility by Mike Nix <mnix@wanm.com.au>
18
# (So thanks Mike!)
19

    
20
import struct
21
import logging
22
import serial
23
from chirp import chirp_common, directory, bitwise, memmap, errors, util
24
from chirp.settings import RadioSetting, RadioSettingGroup, \
25
                RadioSettingValueBoolean, RadioSettingValueList, \
26
                RadioSettingValueInteger, RadioSettingValueString, \
27
                RadioSettingValueFloat, RadioSettingValueMap, RadioSettings
28
from textwrap import dedent
29

    
30
LOG = logging.getLogger(__name__)
31

    
32
mem_format = """
33
struct memory {
34
  bbcd freq[4];
35
  bbcd offset[4];
36
  u8 rxtone;
37
  u8 txtone;
38
  u8 unknown1:2,
39
     txtmode:2,
40
     unknown2:2,
41
     rxtmode:2;
42
  u8 duplex;
43
  u8 unknown3:3,
44
     isnarrow:1,
45
     unknown4:2,
46
     not_scramble:1,
47
     not_revfreq:1;
48
  u8 flag3;
49
  u8 step;
50
  u8 power;
51
};
52

    
53
struct bandflag {
54
    u8 scanadd:1,
55
        unknown1:3,
56
        band:4;
57
};
58

    
59
struct tguv2_config {
60
    u8 unknown1;
61
    u8 squelch;
62
    u8 time_out_timer;
63
    u8 priority_channel;
64

    
65
    u8 unknown2:7,
66
        keyunlocked:1;
67
    u8 busy_lockout;
68
    u8 vox;
69
    u8 unknown3;
70

    
71
    u8 beep_tone_disabled;
72
    u8 display;
73
    u8 step;
74
    u8 unknown4;
75

    
76
    u8 unknown5;
77
    u8 rxmode;
78
    u8 unknown6:7,
79
        not_end_tone_elim:1;
80
    u8 vfo_mode;
81
};
82

    
83
struct vfo {
84
    u8 current;
85
    u8 chan;
86
    u8 memno;
87
};
88

    
89
struct name {
90
  u8 name[6];
91
  u8 unknown1[10];
92
};
93

    
94
#seekto 0x0000;
95
char ident[32];
96
u8 blank[16];
97

    
98
struct memory channels[200];
99
struct memory bands[5];
100

    
101
#seekto 0x0D30;
102
struct bandflag bandflags[200];
103

    
104
#seekto 0x0E30;
105
struct tguv2_config settings;
106
struct vfo vfos[2];
107
u8 unk5;
108
u8 reserved2[9];
109
u8 band_restrict;
110
u8 txen350390;
111

    
112
#seekto 0x0F30;
113
struct name names[200];
114

    
115
"""
116

    
117

    
118
def do_ident(radio):
119
    radio.pipe.timeout = 3
120
    radio.pipe.stopbits = serial.STOPBITS_TWO
121
    radio.pipe.write(b"\x02PnOGdAM")
122
    for x in range(10):
123
        ack = radio.pipe.read(1)
124
        if ack == b'\x06':
125
            break
126
    else:
127
        raise errors.RadioError("Radio did not ack programming mode")
128
    radio.pipe.write(b"\x40\x02")
129
    ident = radio.pipe.read(8)
130
    LOG.debug(util.hexprint(ident))
131
    if not ident.startswith(b'P5555'):
132
        raise errors.RadioError("Unsupported model")
133
    radio.pipe.write(b"\x06")
134
    ack = radio.pipe.read(1)
135
    if ack != b"\x06":
136
        raise errors.RadioError("Radio did not ack ident")
137

    
138

    
139
def do_status(radio, direction, addr):
140
    status = chirp_common.Status()
141
    status.msg = "Cloning %s radio" % direction
142
    status.cur = addr
143
    status.max = 0x2000
144
    radio.status_fn(status)
145

    
146

    
147
def do_download(radio):
148
    do_ident(radio)
149
    data = b"TG-UV2+ Radio Program Data v1.0\x00"
150
    data += (b"\x00" * 16)
151

    
152
    firstack = None
153
    for i in range(0, 0x2000, 8):
154
        frame = struct.pack(">cHB", b"R", i, 8)
155
        radio.pipe.write(frame)
156
        result = radio.pipe.read(12)
157
        if not (result[0:1] == b"W" and frame[1:4] == result[1:4]):
158
            LOG.debug(util.hexprint(result))
159
            raise errors.RadioError("Invalid response for address 0x%04x" % i)
160
        radio.pipe.write(b"\x06")
161
        ack = radio.pipe.read(1)
162
        if not firstack:
163
            firstack = ack
164
        else:
165
            if not ack == firstack:
166
                LOG.debug("first ack: %s ack received: %s",
167
                          util.hexprint(firstack), util.hexprint(ack))
168
                raise errors.RadioError("Unexpected response")
169
        data += result[4:]
170
        do_status(radio, "from", i)
171

    
172
    return memmap.MemoryMapBytes(data)
173

    
174

    
175
def do_upload(radio):
176
    do_ident(radio)
177
    data = radio._mmap[0x0030:]
178

    
179
    for i in range(0, 0x2000, 8):
180
        frame = struct.pack(">cHB", b"W", i, 8)
181
        frame += data[i:i + 8]
182
        radio.pipe.write(frame)
183
        ack = radio.pipe.read(1)
184
        if ack != b"\x06":
185
            LOG.debug("Radio NAK'd block at address 0x%04x" % i)
186
            raise errors.RadioError(
187
                    "Radio NAK'd block at address 0x%04x" % i)
188
        LOG.debug("Radio ACK'd block at address 0x%04x" % i)
189
        do_status(radio, "to", i)
190

    
191
DUPLEX = ["", "+", "-"]
192
TGUV2P_STEPS = [5, 6.25, 10, 12.5, 15, 20, 25, 30, 50, 100]
193
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_|* +-"
194
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=10),
195
                chirp_common.PowerLevel("Med", watts=5),
196
                chirp_common.PowerLevel("Low", watts=1)]
197
POWER_LEVELS_STR = ["High", "Med", "Low"]
198
VALID_BANDS = [(88000000, 108000000),
199
               (136000000, 174000000),
200
               (350000000, 390000000),
201
               (400000000, 470000000),
202
               (470000000, 520000000)]
203

    
204

    
205
@directory.register
206
class QuanshengTGUV2P(chirp_common.CloneModeRadio,
207
                      chirp_common.ExperimentalRadio):
208
    """Quansheng TG-UV2+"""
209
    VENDOR = "Quansheng"
210
    MODEL = "TG-UV2+"
211
    BAUD_RATE = 9600
212
    NEEDS_COMPAT_SERIAL = False
213

    
214
    _memsize = 0x2000
215

    
216
    @classmethod
217
    def get_prompts(cls):
218
        rp = chirp_common.RadioPrompts()
219
        rp.experimental = \
220
            ('Experimental version for TG-UV2/2+ radios '
221
             'Proceed at your own risk!')
222
        rp.pre_download = _(dedent("""\
223
            1. Turn radio off.
224
            2. Connect cable to mic/spkr connector.
225
            3. Make sure connector is firmly connected.
226
            4. Turn radio on.
227
            5. Ensure that the radio is tuned to channel with no activity.
228
            6. Click OK to download image from device."""))
229
        rp.pre_upload = _(dedent("""\
230
            1. Turn radio off.
231
            2. Connect cable to mic/spkr connector.
232
            3. Make sure connector is firmly connected.
233
            4. Turn radio on.
234
            5. Ensure that the radio is tuned to channel with no activity.
235
            6. Click OK to upload image to device."""))
236
        return rp
237

    
238
    def get_features(self):
239
        rf = chirp_common.RadioFeatures()
240
        rf.has_settings = True
241
        rf.has_cross = True
242
        rf.has_rx_dtcs = True
243
        rf.has_dtcs_polarity = True
244
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
245
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
246
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
247
        rf.valid_duplexes = DUPLEX
248
        rf.can_odd_split = False
249
        rf.valid_skips = ["", "S"]
250
        rf.valid_characters = CHARSET
251
        rf.valid_name_length = 6
252
        rf.valid_tuning_steps = TGUV2P_STEPS
253
        rf.valid_bands = VALID_BANDS
254

    
255
        rf.valid_modes = ["FM", "NFM"]
256
        rf.valid_power_levels = POWER_LEVELS
257
        rf.has_ctone = True
258
        rf.has_bank = False
259
        rf.has_tuning_step = True
260
        rf.memory_bounds = (0, 199)
261
        return rf
262

    
263
    def sync_in(self):
264
        try:
265
            self._mmap = do_download(self)
266
        except errors.RadioError:
267
            raise
268
        except Exception as e:
269
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
270
        self.process_mmap()
271

    
272
    def sync_out(self):
273
        try:
274
            do_upload(self)
275
        except errors.RadioError:
276
            raise
277
        except Exception as e:
278
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
279

    
280
    def process_mmap(self):
281
        self._memobj = bitwise.parse(mem_format, self._mmap)
282

    
283
    def get_raw_memory(self, number):
284
        return repr(self._memobj.channels[number])
285

    
286
    def _decode_tone(self, _mem, which):
287
        def _get(field):
288
            return getattr(_mem, "%s%s" % (which, field))
289

    
290
        value = _get('tone')
291
        tmode = _get('tmode')
292

    
293
        if (value <= 104) and (tmode <= 3):
294
            if tmode == 0:
295
                mode = val = pol = None
296
            elif tmode == 1:
297
                mode = 'Tone'
298
                val = chirp_common.TONES[value]
299
                pol = None
300
            else:
301
                mode = 'DTCS'
302
                val = chirp_common.DTCS_CODES[value]
303
                pol = "N" if (tmode == 2) else "R"
304
        else:
305
            mode = val = pol = None
306

    
307
        return mode, val, pol
308

    
309
    def _encode_tone(self, _mem, which, mode, val, pol):
310
        def _set(field, value):
311
            setattr(_mem, "%s%s" % (which, field), value)
312

    
313
        if (mode == "Tone"):
314
            _set("tone", chirp_common.TONES.index(val))
315
            _set("tmode", 0x01)
316
        elif mode == "DTCS":
317
            _set("tone", chirp_common.DTCS_CODES.index(val))
318
            if pol == "N":
319
                _set("tmode", 0x02)
320
            else:
321
                _set("tmode", 0x03)
322
        else:
323
            _set("tone", 0)
324
            _set("tmode", 0)
325

    
326
    def _get_memobjs(self, number):
327
        if isinstance(number, str):
328
            return (getattr(self._memobj, number.lower()), None)
329

    
330
        else:
331
            return (self._memobj.channels[number],
332
                    self._memobj.bandflags[number],
333
                    self._memobj.names[number].name)
334

    
335
    def get_memory(self, number):
336
        _mem, _bf, _nam = self._get_memobjs(number)
337
        mem = chirp_common.Memory()
338
        if isinstance(number, str):
339
            mem.extd_number = number
340
        else:
341
            mem.number = number
342

    
343
        if (_mem.freq.get_raw()[0] == "\xFF") or (_bf.band == "\x0F"):
344
            mem.empty = True
345
            return mem
346

    
347
        mem.freq = int(_mem.freq) * 10
348

    
349
        if _mem.offset.get_raw()[0] == "\xFF":
350
            mem.offset = 0
351
        else:
352
            mem.offset = int(_mem.offset) * 10
353

    
354
        chirp_common.split_tone_decode(
355
            mem,
356
            self._decode_tone(_mem, "tx"),
357
            self._decode_tone(_mem, "rx"))
358

    
359
        if 'step' in _mem and _mem.step > len(TGUV2P_STEPS):
360
            _mem.step = 0x00
361
        mem.tuning_step = TGUV2P_STEPS[_mem.step]
362
        mem.duplex = DUPLEX[_mem.duplex]
363
        mem.mode = _mem.isnarrow and "NFM" or "FM"
364
        mem.skip = "" if bool(_bf.scanadd) else "S"
365
        mem.power = POWER_LEVELS[_mem.power]
366

    
367
        if _nam:
368
            for char in _nam:
369
                try:
370
                    mem.name += CHARSET[char]
371
                except IndexError:
372
                    break
373
            mem.name = mem.name.rstrip()
374

    
375
        mem.extra = RadioSettingGroup("Extra", "extra")
376

    
377
        rs = RadioSetting("not_scramble", "(not)SCRAMBLE",
378
                          RadioSettingValueBoolean(_mem.not_scramble))
379
        mem.extra.append(rs)
380

    
381
        rs = RadioSetting("not_revfreq", "(not)Reverse Duplex",
382
                          RadioSettingValueBoolean(_mem.not_revfreq))
383
        mem.extra.append(rs)
384

    
385
        return mem
386

    
387
    def set_memory(self, mem):
388
        _mem, _bf, _nam = self._get_memobjs(mem.number)
389

    
390
        _bf.set_raw("\xFF")
391

    
392
        if mem.empty:
393
            _mem.set_raw("\xFF" * 16)
394
            return
395

    
396
        _mem.set_raw("\x00" * 12 + "\xFF" * 2 + "\x00"*2)
397

    
398
        _bf.scanadd = int(mem.skip != "S")
399
        _bf.band = 0x0F
400
        for idx, ele in enumerate(VALID_BANDS):
401
            if mem.freq >= ele[0] and mem.freq <= ele[1]:
402
                _bf.band = idx
403

    
404
        _mem.freq = mem.freq / 10
405
        _mem.offset = mem.offset / 10
406

    
407
        tx, rx = chirp_common.split_tone_encode(mem)
408
        self._encode_tone(_mem, 'tx', *tx)
409
        self._encode_tone(_mem, 'rx', *rx)
410

    
411
        _mem.duplex = DUPLEX.index(mem.duplex)
412
        _mem.isnarrow = mem.mode == "NFM"
413
        _mem.step = TGUV2P_STEPS.index(mem.tuning_step)
414

    
415
        if mem.power is None:
416
            _mem.power = 0
417
        else:
418
            _mem.power = POWER_LEVELS.index(mem.power)
419

    
420
        if _nam:
421
            for i in range(0, 6):
422
                try:
423
                    _nam[i] = CHARSET.index(mem.name[i])
424
                except IndexError:
425
                    _nam[i] = 0xFF
426

    
427
        for setting in mem.extra:
428
            setattr(_mem, setting.get_name(), setting.value)
429

    
430
    def get_settings(self):
431
        _settings = self._memobj.settings
432
        _vfoa = self._memobj.vfos[0]
433
        _vfob = self._memobj.vfos[1]
434
        _bandsettings = self._memobj.bands
435

    
436
        cfg_grp = RadioSettingGroup("cfg_grp", "Configuration")
437
        vfoa_grp = RadioSettingGroup(
438
            "vfoa_grp", "VFO A Settings\n  (Current Status, Read Only)")
439
        vfob_grp = RadioSettingGroup(
440
            "vfob_grp", "VFO B Settings\n  (Current Status, Read Only)")
441

    
442
        group = RadioSettings(cfg_grp, vfoa_grp, vfob_grp)
443
        #
444
        # Configuration Settings
445
        #
446

    
447
        # TX time out timer:
448
        options = ["Off"] + ["%s min" % x for x in range(1, 10)]
449
        rs = RadioSetting("time_out_timer", "TX Time Out Timer",
450
                          RadioSettingValueList(
451
                              options, options[_settings.time_out_timer]))
452
        cfg_grp.append(rs)
453

    
454
        # Display mode
455
        options = ["Frequency", "Channel", "Name"]
456
        rs = RadioSetting("display", "Channel Display Mode",
457
                          RadioSettingValueList(
458
                              options, options[_settings.display]))
459
        cfg_grp.append(rs)
460

    
461
        # Squelch level
462
        rs = RadioSetting("squelch", "Squelch Level",
463
                          RadioSettingValueInteger(0, 9, _settings.squelch))
464
        cfg_grp.append(rs)
465

    
466
        # Vox level
467
        mem_vals = list(range(10))
468
        user_options = [str(x) for x in mem_vals]
469
        user_options[0] = "Off"
470
        options_map = list(zip(user_options, mem_vals))
471

    
472
        rs = RadioSetting("vox", "VOX Level",
473
                          RadioSettingValueMap(options_map, _settings.vox))
474
        cfg_grp.append(rs)
475

    
476
        # Keypad beep
477
        rs = RadioSetting("beep_tone_disabled", "Beep Prompt",
478
                          RadioSettingValueBoolean(
479
                               not _settings.beep_tone_disabled))
480
        cfg_grp.append(rs)
481

    
482
        # Dual watch/crossband
483
        options = ["Dual Watch", "CrossBand", "Normal"]
484
        if _settings.rxmode >= 2:
485
            _rxmode = 2
486
        else:
487
            _rxmode = _settings.rxmode
488
        rs = RadioSetting("rxmode", "Dual Watch/CrossBand Monitor",
489
                          RadioSettingValueList(
490
                            options, options[_rxmode]))
491
        cfg_grp.append(rs)
492

    
493
        # Busy channel lock
494
        rs = RadioSetting("busy_lockout", "Busy Channel Lock",
495
                          RadioSettingValueBoolean(
496
                             not _settings.busy_lockout))
497
        cfg_grp.append(rs)
498

    
499
        # Keypad lock
500
        rs = RadioSetting("keyunlocked", "Keypad Lock",
501
                          RadioSettingValueBoolean(
502
                              not _settings.keyunlocked))
503
        cfg_grp.append(rs)
504

    
505
        # Priority channel
506
        mem_vals = list(range(200))
507
        user_options = [str(x) for x in mem_vals]
508
        mem_vals.insert(0, 0xFF)
509
        user_options.insert(0, "Not Set")
510
        options_map = list(zip(user_options, mem_vals))
511
        if _settings.priority_channel >= 200:
512
            _priority_ch = 0xFF
513
        else:
514
            _priority_ch = _settings.priority_channel
515
        rs = RadioSetting(
516
            "priority_channel",
517
            "Priority Channel \n"
518
            "Note: Unused channels,\nor channels "
519
            "in the\nbroadcast FM band,\nwill not be set",
520
            RadioSettingValueMap(options_map, _priority_ch))
521
        cfg_grp.append(rs)
522

    
523
        # Step
524
        mem_vals = list(range(0, len(TGUV2P_STEPS)))
525
        mem_vals.append(0xFF)
526
        user_options = [(str(x) + " kHz") for x in TGUV2P_STEPS]
527
        user_options.append("Unknown")
528
        options_map = list(zip(user_options, mem_vals))
529

    
530
        rs = RadioSetting("step", "Current (VFO?) step size",
531
                          RadioSettingValueMap(options_map, _settings.step))
532
        cfg_grp.append(rs)
533

    
534
        # End (Tail) tone elimination
535
        mem_vals = [0, 1]
536
        user_options = ["Tone Elimination On", "Tone Elimination Off"]
537
        options_map = list(zip(user_options, mem_vals))
538

    
539
        rs = RadioSetting("not_end_tone_elim", "Tx End Tone Elimination",
540
                          RadioSettingValueMap(options_map,
541
                                               _settings.not_end_tone_elim))
542
        cfg_grp.append(rs)
543

    
544
        # VFO mode
545

    
546
        if _settings.vfo_mode >= 1:
547
            _vfo_mode = 0xFF
548
        else:
549
            _vfo_mode = _settings.vfo_mode
550
        mem_vals = [0xFF, 0]
551
        user_options = ["VFO Mode Enabled", "VFO Mode Disabled"]
552
        options_map = list(zip(user_options, mem_vals))
553

    
554
        rs = RadioSetting("vfo_mode", "VFO (CH only) mode",
555
                          RadioSettingValueMap(options_map, _vfo_mode))
556
        cfg_grp.append(rs)
557

    
558
        #
559
        # VFO Settings
560
        #
561

    
562
        vfo_groups = [vfoa_grp, vfob_grp]
563
        vfo_mem = [_vfoa, _vfob]
564
        vfo_lower = ["vfoa", "vfob"]
565
        vfo_upper = ["VFOA", "VFOB"]
566

    
567
        for idx, vfo_group in enumerate(vfo_groups):
568

    
569
            options = ["Channel", "Frequency"]
570
            tempvar = 0 if (vfo_mem[idx].current < 200) else 1
571
            rs = RadioSetting(vfo_lower[idx] + "_mode", vfo_upper[idx]+" Mode",
572
                              RadioSettingValueList(
573
                                  options, options[tempvar]))
574
            vfo_group.append(rs)
575

    
576
            if tempvar == 0:
577
                rs = RadioSetting(vfo_lower[idx] + "_ch",
578
                                  vfo_upper[idx] + " Channel",
579
                                  RadioSettingValueInteger(
580
                                      0, 199, vfo_mem[idx].current))
581
                vfo_group.append(rs)
582
            else:
583
                band_num = vfo_mem[idx].current - 200
584
                freq = int(_bandsettings[band_num].freq) * 10
585
                offset = int(_bandsettings[band_num].offset) * 10
586
                txtmode = _bandsettings[band_num].txtmode
587
                rxtmode = _bandsettings[band_num].rxtmode
588

    
589
                rs = RadioSetting(vfo_lower[idx] + "_freq",
590
                                  vfo_upper[idx] + " Frequency",
591
                                  RadioSettingValueFloat(
592
                                      0.0, 520.0, freq / 1000000.0,
593
                                      precision=6))
594
                vfo_group.append(rs)
595

    
596
                if offset > 70e6:
597
                    offset = 0
598
                rs = RadioSetting(vfo_lower[idx] + "_offset",
599
                                  vfo_upper[idx] + " Offset",
600
                                  RadioSettingValueFloat(
601
                                      0.0, 69.995, offset / 100000.0,
602
                                      resolution=0.005))
603
                vfo_group.append(rs)
604

    
605
                rs = RadioSetting(vfo_lower[idx] + "_duplex",
606
                                  vfo_upper[idx] + " Shift",
607
                                  RadioSettingValueList(
608
                                      DUPLEX,
609
                                      DUPLEX[_bandsettings[band_num].duplex]))
610
                vfo_group.append(rs)
611

    
612
                rs = RadioSetting(
613
                    vfo_lower[idx] + "_step",
614
                    vfo_upper[idx] + " Step",
615
                    RadioSettingValueFloat(
616
                        0.0, 1000.0,
617
                        TGUV2P_STEPS[_bandsettings[band_num].step],
618
                        resolution=0.25))
619
                vfo_group.append(rs)
620

    
621
                rs = RadioSetting(
622
                    vfo_lower[idx] + "_pwr",
623
                    vfo_upper[idx] + " Power",
624
                    RadioSettingValueList(
625
                        POWER_LEVELS_STR,
626
                        POWER_LEVELS_STR[_bandsettings[band_num].power]))
627
                vfo_group.append(rs)
628

    
629
                options = ["None", "Tone", "DTCS-N", "DTCS-I"]
630
                rs = RadioSetting(vfo_lower[idx] + "_ttmode",
631
                                  vfo_upper[idx]+" TX tone mode",
632
                                  RadioSettingValueList(
633
                                      options, options[txtmode]))
634
                vfo_group.append(rs)
635
                if txtmode == 1:
636
                    rs = RadioSetting(
637
                        vfo_lower[idx] + "_ttone",
638
                        vfo_upper[idx] + " TX tone",
639
                        RadioSettingValueFloat(
640
                            0.0, 1000.0,
641
                            chirp_common.TONES[_bandsettings[band_num].txtone],
642
                            resolution=0.1))
643
                    vfo_group.append(rs)
644
                elif txtmode >= 2:
645
                    txtone = _bandsettings[band_num].txtone
646
                    rs = RadioSetting(
647
                        vfo_lower[idx] + "_tdtcs",
648
                        vfo_upper[idx] + " TX DTCS",
649
                        RadioSettingValueInteger(
650
                            0, 1000, chirp_common.DTCS_CODES[txtone]))
651
                    vfo_group.append(rs)
652

    
653
                options = ["None", "Tone", "DTCS-N", "DTCS-I"]
654
                rs = RadioSetting(vfo_lower[idx] + "_rtmode",
655
                                  vfo_upper[idx] + " RX tone mode",
656
                                  RadioSettingValueList(options,
657
                                                        options[rxtmode]))
658
                vfo_group.append(rs)
659

    
660
                if rxtmode == 1:
661
                    rs = RadioSetting(
662
                        vfo_lower[idx] + "_rtone",
663
                        vfo_upper[idx] + " RX tone",
664
                        RadioSettingValueFloat(
665
                            0.0, 1000.0,
666
                            chirp_common.TONES[_bandsettings[band_num].rxtone],
667
                            resolution=0.1))
668
                    vfo_group.append(rs)
669
                elif rxtmode >= 2:
670
                    rxtone = _bandsettings[band_num].rxtone
671
                    rs = RadioSetting(vfo_lower[idx] + "_rdtcs",
672
                                      vfo_upper[idx] + " TX rTCS",
673
                                      RadioSettingValueInteger(
674
                                          0, 1000,
675
                                          chirp_common.DTCS_CODES[rxtone]))
676
                    vfo_group.append(rs)
677

    
678
                options = ["FM", "NFM"]
679
                rs = RadioSetting(
680
                    vfo_lower[idx] + "_fm",
681
                    vfo_upper[idx] + " FM BW ",
682
                    RadioSettingValueList(
683
                        options, options[_bandsettings[band_num].isnarrow]))
684
                vfo_group.append(rs)
685

    
686
        return group
687

    
688
    def _validate_priority_ch(self, ch_num):
689
        if ch_num == 0xFF:
690
            return True
691
        _mem, _bf, _nam = self._get_memobjs(ch_num)
692
        if (_mem.freq.get_raw()[0] == "\xFF") or (_bf.band == "\x0F"):
693
            return False
694
        elif _bf.band == 0x00:
695
            return False
696
        else:
697
            return True
698

    
699
    def set_settings(self, settings):
700
        for element in settings:
701
            if not isinstance(element, RadioSetting):
702
                self.set_settings(element)
703
                continue
704
            else:
705
                try:
706
                    if "vfoa" in element.get_name():
707
                        continue
708
                    if "vfob" in element.get_name():
709
                        continue
710
                    elif "." in element.get_name():
711
                        bits = element.get_name().split(".")
712
                        obj = self._memobj
713
                        for bit in bits[:-1]:
714
                            obj = getattr(obj, bit)
715
                        setting = bits[-1]
716
                    else:
717
                        obj = self._memobj.settings
718
                        setting = element.get_name()
719

    
720
                    if element.has_apply_callback():
721
                        LOG.debug("using apply callback")
722
                        element.run_apply_callback()
723
                    elif setting == "beep_tone_disabled":
724
                        LOG.debug("Setting %s = %s" % (setting,
725
                                                       not int(element.value)))
726
                        setattr(obj, setting, not int(element.value))
727
                    elif setting == "busy_lockout":
728
                        LOG.debug("Setting %s = %s" % (setting,
729
                                                       not int(element.value)))
730
                        setattr(obj, setting, not int(element.value))
731
                    elif setting == "keyunlocked":
732
                        # keypad currently unlocked being set to locked
733
                        # and rx_mode is currently not "Normal":
734
                        if getattr(obj, "keyunlocked") and int(element.value) \
735
                                and (getattr(obj, "rxmode") != 0x02):
736
                            raise errors.InvalidValueError(
737
                                "Keypad lock not allowed in "
738
                                "Dual-Watch or CrossBand")
739
                        LOG.debug("Setting %s = %s" % (setting,
740
                                                       not int(element.value)))
741
                        setattr(obj, setting, not int(element.value))
742
                    elif setting == "rxmode":
743
                        # rx_mode was normal, now being set otherwise
744
                        # and keypad is locked:
745
                        if (getattr(obj, "rxmode") == 0x02) \
746
                                and (int(element.value) != 2) \
747
                                and not (getattr(obj, "keyunlocked")):
748
                            raise errors.InvalidValueError(
749
                                "Dual-Watch or CrossBand can not be set "
750
                                "when keypad is locked")
751
                        LOG.debug("Setting %s = %s" % (setting, element.value))
752
                        setattr(obj, setting, element.value)
753
                    elif setting == "priority_channel":
754
                        _check = self._validate_priority_ch(int(element.value))
755
                        if _check:
756
                            LOG.debug("Setting %s = %s" % (setting,
757
                                                           element.value))
758
                            setattr(obj, setting, element.value)
759
                        else:
760
                            raise errors.InvalidValueError(
761
                                "Please select a valid priority channel:\n"
762
                                "A used memory channel which is not "
763
                                "in the Broadcast FM band (88-108MHz),\n"
764
                                "Or select 'Not Used'")
765
                    elif element.value.get_mutable():
766
                        LOG.debug("Setting %s = %s" % (setting, element.value))
767
                        setattr(obj, setting, element.value)
768
                except Exception as e:
769
                    LOG.debug(element.get_name())
770
                    raise
771

    
772
    @classmethod
773
    def match_model(cls, filedata, filename):
774
        return (filedata.startswith(b"TG-UV2+ Radio Program Data") and
775
                len(filedata) == (cls._memsize + 0x30))
(6-6/12)