Project

General

Profile

Bug #10398 » tg_uv2p.py

Ran Katz, 02/28/2023 09:43 PM

 
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
                LOG.debug("At address 0x%04x" % i)
169
                raise errors.RadioError("Unexpected response")
170
        data += result[4:]
171
        do_status(radio, "from", i)
172

    
173
    return memmap.MemoryMapBytes(data)
174

    
175

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

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

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

    
205

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

    
215
    _memsize = 0x2000
216

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

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

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

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

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

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

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

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

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

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

    
308
        return mode, val, pol
309

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
386
        return mem
387

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

    
391
        _bf.set_raw("\xFF")
392

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
545
        # VFO mode
546

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

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

    
559
        #
560
        # VFO Settings
561
        #
562

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

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

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

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

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

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

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

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

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

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

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

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

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

    
687
        return group
688

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

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

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

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