Project

General

Profile

New Model #10023 » th_uv88 - qrz_1_poc4.py

Jim Unroe, 09/27/2022 05:19 PM

 
1
# Version 1.0 for TYT-UV88
2
# Initial radio protocol decode, channels and memory layout
3
# by James Berry <james@coppermoth.com>, Summer 2020
4
# Additional configuration and help, Jim Unroe <rock.unroe@gmail.com>
5
#
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program.
18

    
19
import time
20
import struct
21
import logging
22
import re
23
import math
24
from chirp import chirp_common, directory, memmap
25
from chirp import bitwise, errors, util
26
from chirp.settings import RadioSettingGroup, RadioSetting, \
27
    RadioSettingValueBoolean, RadioSettingValueList, \
28
    RadioSettingValueString, RadioSettingValueInteger, \
29
    RadioSettingValueFloat, RadioSettings, InvalidValueError
30
from textwrap import dedent
31

    
32
LOG = logging.getLogger(__name__)
33

    
34
MEM_FORMAT = """
35
struct chns {
36
  ul32 rxfreq;
37
  ul32 txfreq;
38
  ul16 scramble:4
39
       rxtone:12; //decode:12
40
  ul16 decodeDSCI:1
41
       encodeDSCI:1
42
       unk1:1
43
       unk2:1
44
       txtone:12; //encode:12
45
  u8   power:2
46
       wide:2
47
       b_lock:2
48
       unk3:2;
49
  u8   unk4:3
50
       signal:2
51
       displayName:1
52
       unk5:2;
53
  u8   unk6:2
54
       pttid:2
55
       step:4;               // not required
56
  u8   name[6];
57
};
58

    
59
struct vfo {
60
  ul32 rxfreq;
61
  ul32 txfreq;  // displayed as an offset
62
  ul16 scramble:4
63
       rxtone:12; //decode:12
64
  ul16 decodeDSCI:1
65
       encodeDSCI:1
66
       unk1:1
67
       unk2:1
68
       txtone:12; //encode:12
69
  u8   power:2
70
       wide:2
71
       b_lock:2
72
       unk3:2;
73
  u8   unk4:3
74
       signal:2
75
       displayName:1
76
       unk5:2;
77
  u8   unk6:2
78
       pttid:2
79
       step:4;
80
  u8   name[6];
81
};
82

    
83
struct chname {
84
  u8  extra_name[10];
85
};
86

    
87
#seekto 0x0000;
88
struct chns chan_mem[199];
89

    
90
#seekto 0x1960;
91
struct chname chan_name[199];
92

    
93
#seekto 0x1180;
94
struct {
95
  u8 bitmap[26];    // one bit for each channel marked in use
96
} chan_avail;
97

    
98
#seekto 0x11A0;
99
struct {
100
  u8 bitmap[26];    // one bit for each channel skipped
101
} chan_skip;
102

    
103
#seekto 0x1140;
104
struct {
105
  u8 autoKeylock:1,       // 0x1140 [18] *OFF, On
106
     unk_bit6_5:1,        //
107
     vfomrmodeb:1,        //        *VFO B, MR B
108
     vfomrmode:1,         //        *VFO, MR
109
     unk_bit3_0:4;        //
110
  u8 mrAch;               // 0x1141 MR A Channel #
111
  u8 mrBch;               // 0x1142 MR B Channel #
112
  u8 unk_bit7_3:5,        //
113
     ab:1,                //        * A, B
114
     unk_bit1_0:2;        //
115
} workmodesettings;
116

    
117
#seekto 0x1160;
118
struct {
119
  u8 introScreen1[12];    // 0x1160 *Intro Screen Line 1(truncated to 12 alpha
120
                          //         text characters)
121
  u8 offFreqVoltage : 3,  // 0x116C unknown referred to in code but not on
122
                          //        screen
123
     unk_bit4 : 1,        //
124
     sqlLevel : 4;        //        [05] *OFF, 1-9
125
  u8 beep : 1             // 0x116D [09] *OFF, On
126
     callKind : 2,        //        code says 1750,2100,1000,1450 as options
127
                          //        not on screen
128
     introScreen: 2,      //        [20] *OFF, Voltage, Char String
129
     unkstr2: 2,          //
130
     txChSelect : 1;      //        [02] *Last CH, Main CH
131
  u8 autoPowOff : 3,      // 0x116E not on screen? OFF, 30Min, 1HR, 2HR
132
     unk : 1,             //
133
     tot : 4;             //        [11] *OFF, 30 Second, 60 Second, 90 Second,
134
                          //              ... , 270 Second
135
  u8 unk_bit7:1,          // 0x116F
136
     roger:1,             //        [14] *OFF, On
137
     dailDef:1,           //        Unknown - 'Volume, Frequency'
138
     language:1,          //        ?Chinese, English (English only FW BQ1.38+)
139
     unk_bit3:1,          //
140
     endToneElim:1,       //        *OFF, Frequency
141
     unkCheckBox1:1,      //
142
     unkCheckBox2:1;      //
143
  u8 scanResumeTime : 2,  // 0x1170 2S, 5S, 10S, 15S (not on screen)
144
     disMode : 2,         //        [33] *Frequency, Channel, Name
145
     scanType: 2,         //        [17] *To, Co, Se
146
     ledMode: 2;          //        [07] *Off, On, Auto
147
  u8 unky;                // 0x1171
148
  u8 str6;                // 0x1172 Has flags to do with logging - factory
149
                          // enabled (bits 16,64,128)
150
  u8 unk;                 // 0x1173
151
  u8 swAudio : 1,         // 0x1174 [19] *OFF, On
152
     radioMoni : 1,       //        [34] *OFF, On
153
     keylock : 1,         //        [18] *OFF, On
154
     dualWait : 1,        //        [06] *OFF, On
155
     unk_bit3 : 1,        //
156
     light : 3;           //        [08] *1, 2, 3, 4, 5, 6, 7
157
  u8 voxSw : 1,           // 0x1175 [13] *OFF, On
158
     voxDelay: 4,         //        *0.5S, 1.0S, 1.5S, 2.0S, 2.5S, 3.0S, 3.5S,
159
                          //         4.0S, 4.5S, 5.0S
160
     voxLevel : 3;        //        [03] *1, 2, 3, 4, 5, 6, 7
161
  u8 str9 : 4,            // 0x1176
162
     saveMode : 2,        //        [16] *OFF, 1:1, 1:2, 1:4
163
     keyMode : 2;         //        [32] *ALL, PTT, KEY, Key & Side Key
164
  u8 unk2;                // 0x1177
165
  u8 unk3;                // 0x1178
166
  u8 unk4;                // 0x1179
167
  u8 name2[6];            // 0x117A unused
168
} basicsettings;
169

    
170
#seekto 0x191E;
171
struct {
172
  u8 unknown191e:4,       //
173
     region:4;            // 0x191E Radio Region (read only)
174
                          // 0 = Unlocked  TX: 136-174 MHz / 400-480 MHz
175
                          // 2-3 = Unknown
176
                          // 3 = EU        TX: 144-146 MHz / 430-440 MHz
177
                          // 4 = US        TX: 144-148 MHz / 420-450 MHz
178
                          // 5-15 = Unknown
179
} settings2;
180

    
181
#seekto 0x1940;
182
struct {
183
  char name1[15];         // Intro Screen Line 1 (16 alpha text characters)
184
  u8 unk1;
185
  char name2[15];         // Intro Screen Line 2 (16 alpha text characters)
186
  u8 unk2;
187
} openradioname;
188

    
189
//#seekto 0x2180;
190
//struct {
191
//  ul32 freq;
192
//} fm_preset[24];
193

    
194
struct fm_chn {
195
  ul32 rxfreq;
196
};
197

    
198
#seekto 0x2180;
199
struct fm_chn fm_stations[24];
200

    
201
#seekto 0x021E0;
202
struct {
203
  u8  fmset[4];
204
} fmmap;
205

    
206
#seekto 0x21E4;
207
struct {
208
  ul32 fmcur;
209
} fmfrqs;
210

    
211
"""
212

    
213
MEM_SIZE = 0x22A0
214
BLOCK_SIZE = 0x20
215
STIMEOUT = 2
216
BAUDRATE = 57600
217

    
218
# Channel power: 3 levels
219
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
220
                chirp_common.PowerLevel("Mid", watts=2.50),
221
                chirp_common.PowerLevel("Low", watts=0.50)]
222

    
223
SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"]
224
B_LOCK_LIST = ["OFF", "Sub", "Carrier"]
225
OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"]
226
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
227
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
228
LIST_STEPS = [str(x) for x in STEPS]
229

    
230

    
231
def _clean_buffer(radio):
232
    radio.pipe.timeout = 0.005
233
    junk = radio.pipe.read(256)
234
    radio.pipe.timeout = STIMEOUT
235
    if junk:
236
        LOG.debug("Got %i bytes of junk before starting" % len(junk))
237

    
238

    
239
def _rawrecv(radio, amount):
240
    """Raw read from the radio device"""
241
    data = ""
242
    try:
243
        data = radio.pipe.read(amount)
244
    except Exception:
245
        _exit_program_mode(radio)
246
        msg = "Generic error reading data from radio; check your cable."
247
        raise errors.RadioError(msg)
248

    
249
    if len(data) != amount:
250
        _exit_program_mode(radio)
251
        msg = "Error reading from radio: not the amount of data we want."
252
        raise errors.RadioError(msg)
253

    
254
    return data
255

    
256

    
257
def _rawsend(radio, data):
258
    """Raw send to the radio device"""
259
    try:
260
        radio.pipe.write(data)
261
    except Exception:
262
        raise errors.RadioError("Error sending data to radio")
263

    
264

    
265
def _make_read_frame(addr, length):
266
    frame = "\xFE\xFE\xEE\xEF\xEB"
267
    """Pack the info in the header format"""
268
    frame += struct.pack(">ih", addr, length)
269

    
270
    frame += "\xFD"
271
    # Return the data
272
    return frame
273

    
274

    
275
def _make_write_frame(addr, length, data=""):
276
    frame = "\xFE\xFE\xEE\xEF\xE4"
277

    
278
    """Pack the info in the header format"""
279
    output = struct.pack(">ih", addr, length)
280
    # Add the data if set
281
    if len(data) != 0:
282
        output += data
283

    
284
    frame += output
285
    frame += _calculate_checksum(output)
286

    
287
    frame += "\xFD"
288
    # Return the data
289
    return frame
290

    
291

    
292
def _calculate_checksum(data):
293
    num = 0
294
    for x in range(0, len(data)):
295
        num = (num + ord(data[x])) % 256
296

    
297
    if num == 0:
298
        return chr(0)
299

    
300
    return chr(256 - num)
301

    
302

    
303
def _recv(radio, addr, length):
304
    """Get data from the radio """
305

    
306
    data = _rawrecv(radio, length)
307

    
308
    # DEBUG
309
    LOG.info("Response:")
310
    LOG.debug(util.hexprint(data))
311

    
312
    return data
313

    
314

    
315
def _do_ident(radio):
316
    """Put the radio in PROGRAM mode & identify it"""
317
    radio.pipe.baudrate = BAUDRATE
318
    radio.pipe.parity = "N"
319
    radio.pipe.timeout = STIMEOUT
320

    
321
    # Flush input buffer
322
    _clean_buffer(radio)
323

    
324
    # Ident radio
325
    magic = radio._magic0
326
    _rawsend(radio, magic)
327
    ack = _rawrecv(radio, 36)
328

    
329
    if not ack.startswith(radio._fingerprint) or not ack.endswith("\xFD"):
330
        _exit_program_mode(radio)
331
        if ack:
332
            LOG.debug(repr(ack))
333
        raise errors.RadioError("Radio did not respond as expected (A)")
334

    
335
    return True
336

    
337

    
338
def _exit_program_mode(radio):
339
    # This may be the last part of a read
340
    magic = radio._magic5
341
    _rawsend(radio, magic)
342
    _clean_buffer(radio)
343

    
344
def _download(radio):
345
    """Get the memory map"""
346

    
347
    # Put radio in program mode and identify it
348
    _do_ident(radio)
349

    
350
    # Enter read mode
351
    magic = radio._magic2
352
    _rawsend(radio, magic)
353
    ack = _rawrecv(radio, 7)
354
    if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
355
        _exit_program_mode(radio)
356
        if ack:
357
            LOG.debug(repr(ack))
358
        raise errors.RadioError("Radio did not respond to enter read mode")
359

    
360
    # UI progress
361
    status = chirp_common.Status()
362
    status.cur = 0
363
    status.max = MEM_SIZE / BLOCK_SIZE
364
    status.msg = "Cloning from radio..."
365
    radio.status_fn(status)
366

    
367
    data = ""
368
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
369
        frame = _make_read_frame(addr, BLOCK_SIZE)
370
        # DEBUG
371
        LOG.debug("Frame=" + util.hexprint(frame))
372

    
373
        # Sending the read request
374
        _rawsend(radio, frame)
375

    
376
        # Now we read data
377
        d = _recv(radio, addr, BLOCK_SIZE + 13)
378

    
379
        LOG.debug("Response Data= " + util.hexprint(d))
380

    
381
        if not d.startswith("\xFE\xFE\xEF\xEE\xE4"):
382
            LOG.warning("Incorrect start")
383
        if not d.endswith("\xFD"):
384
            LOG.warning("Incorrect end")
385
        # could validate the block data
386

    
387
        # Aggregate the data
388
        data += d[11:-2]
389

    
390
        # UI Update
391
        status.cur = addr / BLOCK_SIZE
392
        status.msg = "Cloning from radio..."
393
        radio.status_fn(status)
394

    
395
    _exit_program_mode(radio)
396

    
397
    return data
398

    
399

    
400
def _upload(radio):
401
    """Upload procedure"""
402
    # Put radio in program mode and identify it
403
    _do_ident(radio)
404

    
405
    magic = radio._magic3
406
    _rawsend(radio, magic)
407
    ack = _rawrecv(radio, 7)
408
    if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
409
        _exit_program_mode(radio)
410
        if ack:
411
            LOG.debug(repr(ack))
412
        raise errors.RadioError("Radio did not respond to enter write mode")
413

    
414
    # UI progress
415
    status = chirp_common.Status()
416
    status.cur = 0
417
    status.max = MEM_SIZE / BLOCK_SIZE
418
    status.msg = "Cloning to radio..."
419
    radio.status_fn(status)
420

    
421
    # The fun starts here
422
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
423
        # Official programmer skips writing these memory locations
424
        if addr >= 0x1680 and addr < 0x1940:
425
            continue
426

    
427
        # Sending the data
428
        data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
429

    
430
        frame = _make_write_frame(addr, BLOCK_SIZE, data)
431
        LOG.warning("Frame:%s:" % util.hexprint(frame))
432
        _rawsend(radio, frame)
433

    
434
        ack = _rawrecv(radio, 7)
435
        LOG.debug("Response Data= " + util.hexprint(ack))
436

    
437
        if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
438
            LOG.warning("Unexpected response")
439
            _exit_program_mode(radio)
440
            msg = "Bad ack writing block 0x%04x" % addr
441
            raise errors.RadioError(msg)
442

    
443
        # UI Update
444
        status.cur = addr / BLOCK_SIZE
445
        status.msg = "Cloning to radio..."
446
        radio.status_fn(status)
447

    
448
    _exit_program_mode(radio)
449

    
450

    
451
def _do_map(chn, sclr, mary):
452
    """Set or Clear the chn (1-128) bit in mary[] word array map"""
453
    # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
454
    # mary[] is u8 array, but the map is by nibbles
455
    ndx = int(math.floor((chn - 1) / 8))
456
    bv = (chn - 1) % 8
457
    msk = 1 << bv
458
    mapbit = sclr
459
    if sclr == 1:    # Set the bit
460
        mary[ndx] = mary[ndx] | msk
461
    elif sclr == 0:  # clear
462
        mary[ndx] = mary[ndx] & (~ msk)     # ~ is complement
463
    else:       # return current bit state
464
        mapbit = 0
465
        if (mary[ndx] & msk) > 0:
466
            mapbit = 1
467
    return mapbit
468

    
469

    
470
@directory.register
471
class THUV88Radio(chirp_common.CloneModeRadio):
472
    """TYT UV88 Radio"""
473
    VENDOR = "TYT"
474
    MODEL = "TH-UV88"
475
    MODES = ['WFM', 'FM', 'NFM']
476
    TONES = chirp_common.TONES
477
    DTCS_CODES = chirp_common.DTCS_CODES
478
    NAME_LENGTH = 10
479
    DTMF_CHARS = list("0123456789ABCD*#")
480
    # 136-174, 400-480
481
    VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)]
482

    
483
    # Valid chars on the LCD
484
    VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
485
        "`!\"#$%&'()*+,-./:;<=>?@[]^_"
486

    
487
    _magic0 = "\xFE\xFE\xEE\xEF\xE0" + "UV88" + "\xFD"
488
    _magic2 = "\xFE\xFE\xEE\xEF\xE2" + "UV88" + "\xFD"
489
    _magic3 = "\xFE\xFE\xEE\xEF\xE3" + "UV88" + "\xFD"
490
    _magic5 = "\xFE\xFE\xEE\xEF\xE5" + "UV88" + "\xFD"
491
    _fingerprint = "\xFE\xFE\xEF\xEE\xE1" + "UV88"
492

    
493

    
494
    @classmethod
495
    def get_prompts(cls):
496
        rp = chirp_common.RadioPrompts()
497
        rp.info = \
498
            (cls.VENDOR + ' ' + cls.MODEL + '\n')
499

    
500
        rp.pre_download = _(dedent("""\
501
            This is an early stage beta driver
502
            """))
503
        rp.pre_upload = _(dedent("""\
504
            This is an early stage beta driver - upload at your own risk
505
            """))
506
        return rp
507

    
508
    def get_features(self):
509
        rf = chirp_common.RadioFeatures()
510
        rf.has_settings = True
511
        rf.has_bank = False
512
        rf.has_comment = False
513
        rf.has_tuning_step = False      # Not as chan feature
514
        rf.valid_tuning_steps = STEPS
515
        rf.can_odd_split = True
516
        rf.has_name = True
517
        rf.has_offset = True
518
        rf.has_mode = True
519
        rf.has_dtcs = True
520
        rf.has_rx_dtcs = True
521
        rf.has_dtcs_polarity = True
522
        rf.has_ctone = True
523
        rf.has_cross = True
524
        rf.has_sub_devices = False
525
        rf.valid_name_length = self.NAME_LENGTH
526
        rf.valid_modes = self.MODES
527
        rf.valid_characters = self.VALID_CHARS
528
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
529
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
530
        rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
531
                                "Tone->DTCS", "DTCS->Tone", "->Tone",
532
                                "DTCS->DTCS"]
533
        rf.valid_skips = []
534
        rf.valid_power_levels = POWER_LEVELS
535
        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES  # this is just to
536
        # get it working, not sure this is right
537
        rf.valid_bands = self.VALID_BANDS
538
        rf.memory_bounds = (1, 199)
539
        rf.valid_skips = ["", "S"]
540
        return rf
541

    
542
    def sync_in(self):
543
        """Download from radio"""
544
        try:
545
            data = _download(self)
546
        except errors.RadioError:
547
            # Pass through any real errors we raise
548
            raise
549
        except Exception:
550
            # If anything unexpected happens, make sure we raise
551
            # a RadioError and log the problem
552
            LOG.exception('Unexpected error during download')
553
            raise errors.RadioError('Unexpected error communicating '
554
                                    'with the radio')
555
        self._mmap = memmap.MemoryMap(data)
556
        self.process_mmap()
557

    
558
    def sync_out(self):
559
        """Upload to radio"""
560

    
561
        try:
562
            _upload(self)
563
        except Exception:
564
            # If anything unexpected happens, make sure we raise
565
            # a RadioError and log the problem
566
            LOG.exception('Unexpected error during upload')
567
            raise errors.RadioError('Unexpected error communicating '
568
                                    'with the radio')
569

    
570
    def process_mmap(self):
571
        """Process the mem map into the mem object"""
572
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
573

    
574
    def get_raw_memory(self, number):
575
        return repr(self._memobj.memory[number - 1])
576

    
577
    def set_memory(self, memory):
578
        """A value in a UI column for chan 'number' has been modified."""
579
        # update all raw channel memory values (_mem) from UI (mem)
580
        _mem = self._memobj.chan_mem[memory.number - 1]
581
        _name = self._memobj.chan_name[memory.number - 1]
582

    
583
        if memory.empty:
584
            _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
585
            return
586

    
587
        _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
588

    
589
        if memory.skip == "":
590
            _do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
591
        else:
592
            _do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
593

    
594
        return self._set_memory(memory, _mem, _name)
595

    
596
    def get_memory(self, number):
597
        # radio first channel is 1, mem map is base 0
598
        _mem = self._memobj.chan_mem[number - 1]
599
        _name = self._memobj.chan_name[number - 1]
600
        mem = chirp_common.Memory()
601
        mem.number = number
602

    
603
        # Determine if channel is empty
604

    
605
        if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0:
606
            mem.empty = True
607
            return mem
608

    
609
        if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0:
610
            mem.skip = ""
611
        else:
612
            mem.skip = "S"
613

    
614
        return self._get_memory(mem, _mem, _name)
615

    
616
    def _get_memory(self, mem, _mem, _name):
617
        """Convert raw channel memory data into UI columns"""
618
        mem.extra = RadioSettingGroup("extra", "Extra")
619

    
620
        mem.empty = False
621
        # This function process both 'normal' and Freq up/down' entries
622
        mem.freq = int(_mem.rxfreq) * 10
623

    
624
        if _mem.txfreq == 0xFFFFFFFF:
625
            # TX freq not set
626
            mem.duplex = "off"
627
            mem.offset = 0
628
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 25000000:
629
            mem.duplex = "split"
630
            mem.offset = int(_mem.txfreq) * 10
631
        elif int(_mem.rxfreq) == int(_mem.txfreq):
632
            mem.duplex = ""
633
            mem.offset = 0
634
        else:
635
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \
636
                and "-" or "+"
637
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
638

    
639
        mem.name = ""
640
        for i in range(6):   # 0 - 6
641
            mem.name += chr(_mem.name[i])
642
        for i in range(10):
643
            mem.name += chr(_name.extra_name[i])
644

    
645
        mem.name = mem.name.rstrip()    # remove trailing spaces
646

    
647
        # ########## TONE ##########
648

    
649
        if _mem.txtone > 2600:
650
            # All off
651
            txmode = ""
652
        elif _mem.txtone > 511:
653
            txmode = "Tone"
654
            mem.rtone = int(_mem.txtone) / 10.0
655
        else:
656
            # DTSC
657
            txmode = "DTCS"
658
            mem.dtcs = int(format(int(_mem.txtone), 'o'))
659

    
660
        if _mem.rxtone > 2600:
661
            rxmode = ""
662
        elif _mem.rxtone > 511:
663
            rxmode = "Tone"
664
            mem.ctone = int(_mem.rxtone) / 10.0
665
        else:
666
            rxmode = "DTCS"
667
            mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
668

    
669
        mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + (
670
                             "N", "R")[_mem.decodeDSCI]
671

    
672
        mem.tmode = ""
673
        if txmode == "Tone" and not rxmode:
674
            mem.tmode = "Tone"
675
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
676
            mem.tmode = "TSQL"
677
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
678
            mem.tmode = "DTCS"
679
        elif rxmode or txmode:
680
            mem.tmode = "Cross"
681
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
682

    
683
        # ########## TONE ##########
684

    
685
        mem.mode = self.MODES[_mem.wide]
686
        mem.power = POWER_LEVELS[int(_mem.power)]
687

    
688
        b_lock = RadioSetting("b_lock", "B_Lock",
689
                              RadioSettingValueList(B_LOCK_LIST,
690
                                                    B_LOCK_LIST[_mem.b_lock]))
691
        mem.extra.append(b_lock)
692

    
693
        step = RadioSetting("step", "Step",
694
                            RadioSettingValueList(LIST_STEPS,
695
                                                  LIST_STEPS[_mem.step]))
696
        mem.extra.append(step)
697

    
698
        scramble_value = _mem.scramble
699
        if scramble_value >= 8:     # Looks like OFF is 0x0f ** CONFIRM
700
            scramble_value = 0
701
        scramble = RadioSetting("scramble", "Scramble",
702
                                RadioSettingValueList(SCRAMBLE_LIST,
703
                                                      SCRAMBLE_LIST[
704
                                                          scramble_value]))
705
        mem.extra.append(scramble)
706

    
707
        optsig = RadioSetting("signal", "Optional signaling",
708
                              RadioSettingValueList(
709
                                  OPTSIG_LIST,
710
                                  OPTSIG_LIST[_mem.signal]))
711
        mem.extra.append(optsig)
712

    
713
        rs = RadioSetting("pttid", "PTT ID",
714
                          RadioSettingValueList(PTTID_LIST,
715
                                                PTTID_LIST[_mem.pttid]))
716
        mem.extra.append(rs)
717

    
718
        return mem
719

    
720
    def _set_memory(self, mem, _mem, _name):
721
        # """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
722

    
723
        _mem.rxfreq = mem.freq / 10
724
        if mem.duplex == "off":
725
            _mem.txfreq = 0xFFFFFFFF
726
        elif mem.duplex == "split":
727
            _mem.txfreq = mem.offset / 10
728
        elif mem.duplex == "+":
729
            _mem.txfreq = (mem.freq + mem.offset) / 10
730
        elif mem.duplex == "-":
731
            _mem.txfreq = (mem.freq - mem.offset) / 10
732
        else:
733
            _mem.txfreq = _mem.rxfreq
734

    
735
        out_name = mem.name.ljust(16)
736

    
737
        for i in range(6):   # 0 - 6
738
            _mem.name[i] = ord(out_name[i])
739
        for i in range(10):
740
            _name.extra_name[i] = ord(out_name[i+6])
741

    
742
        if mem.name != "":
743
            _mem.displayName = 1    # Name only displayed if this is set on
744
        else:
745
            _mem.displayName = 0
746

    
747
        rxmode = ""
748
        txmode = ""
749

    
750
        if mem.tmode == "Tone":
751
            txmode = "Tone"
752
        elif mem.tmode == "TSQL":
753
            rxmode = "Tone"
754
            txmode = "TSQL"
755
        elif mem.tmode == "DTCS":
756
            rxmode = "DTCSSQL"
757
            txmode = "DTCS"
758
        elif mem.tmode == "Cross":
759
            txmode, rxmode = mem.cross_mode.split("->", 1)
760

    
761
        if mem.dtcs_polarity[1] == "N":
762
            _mem.decodeDSCI = 0
763
        else:
764
            _mem.decodeDSCI = 1
765

    
766
        if rxmode == "":
767
            _mem.rxtone = 0xFFF
768
        elif rxmode == "Tone":
769
            _mem.rxtone = int(float(mem.ctone) * 10)
770
        elif rxmode == "DTCSSQL":
771
            _mem.rxtone = int(str(mem.dtcs), 8)
772
        elif rxmode == "DTCS":
773
            _mem.rxtone = int(str(mem.rx_dtcs), 8)
774

    
775
        if mem.dtcs_polarity[0] == "N":
776
            _mem.encodeDSCI = 0
777
        else:
778
            _mem.encodeDSCI = 1
779

    
780
        if txmode == "":
781
            _mem.txtone = 0xFFF
782
        elif txmode == "Tone":
783
            _mem.txtone = int(float(mem.rtone) * 10)
784
        elif txmode == "TSQL":
785
            _mem.txtone = int(float(mem.ctone) * 10)
786
        elif txmode == "DTCS":
787
            _mem.txtone = int(str(mem.dtcs), 8)
788

    
789
        _mem.wide = self.MODES.index(mem.mode)
790
        _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
791

    
792
        for element in mem.extra:
793
            setattr(_mem, element.get_name(), element.value)
794

    
795
        return
796

    
797
    def get_settings(self):
798
        """Translate the MEM_FORMAT structs into setstuf in the UI"""
799
        _settings = self._memobj.basicsettings
800
        _settings2 = self._memobj.settings2
801
        _workmode = self._memobj.workmodesettings
802

    
803
        basic = RadioSettingGroup("basic", "Basic Settings")
804
        group = RadioSettings(basic)
805

    
806
        # Menu 02 - TX Channel Select
807
        options = ["Last Channel", "Main Channel"]
808
        rx = RadioSettingValueList(options, options[_settings.txChSelect])
809
        rset = RadioSetting("basicsettings.txChSelect",
810
                            "Priority Transmit", rx)
811
        basic.append(rset)
812

    
813
        # Menu 03 - VOX Level
814
        rx = RadioSettingValueInteger(1, 7, _settings.voxLevel + 1)
815
        rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx)
816
        basic.append(rset)
817

    
818
        # Menu 05 - Squelch Level
819
        options = ["OFF"] + ["%s" % x for x in range(1, 10)]
820
        rx = RadioSettingValueList(options, options[_settings.sqlLevel])
821
        rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
822
        basic.append(rset)
823

    
824
        # Menu 06 - Dual Wait
825
        rx = RadioSettingValueBoolean(_settings.dualWait)
826
        rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx)
827
        basic.append(rset)
828

    
829
        # Menu 07 - LED Mode
830
        options = ["Off", "On", "Auto"]
831
        rx = RadioSettingValueList(options, options[_settings.ledMode])
832
        rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
833
        basic.append(rset)
834

    
835
        # Menu 08 - Light
836
        options = ["%s" % x for x in range(1, 8)]
837
        rx = RadioSettingValueList(options, options[_settings.light])
838
        rset = RadioSetting("basicsettings.light",
839
                            "Background Light Color", rx)
840
        basic.append(rset)
841

    
842
        # Menu 09 - Beep
843
        rx = RadioSettingValueBoolean(_settings.beep)
844
        rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
845
        basic.append(rset)
846

    
847
        # Menu 11 - TOT
848
        options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)]
849
        rx = RadioSettingValueList(options, options[_settings.tot])
850
        rset = RadioSetting("basicsettings.tot",
851
                            "Transmission Time-out Timer", rx)
852
        basic.append(rset)
853

    
854
        # Menu 13 - VOX Switch
855
        rx = RadioSettingValueBoolean(_settings.voxSw)
856
        rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx)
857
        basic.append(rset)
858

    
859
        # Menu 14 - Roger
860
        rx = RadioSettingValueBoolean(_settings.roger)
861
        rset = RadioSetting("basicsettings.roger", "Roger Beep", rx)
862
        basic.append(rset)
863

    
864
        # Menu 16 - Save Mode
865
        options = ["Off", "1:1", "1:2", "1:4"]
866
        rx = RadioSettingValueList(options, options[_settings.saveMode])
867
        rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx)
868
        basic.append(rset)
869

    
870
        # Menu 17 - Scan Type
871
        if self.MODEL == "QRZ-1":
872
            options = ["Time", "Carrier", "Stop"]
873
        else:
874
            options = ["CO", "TO", "SE"]
875
        rx = RadioSettingValueList(options, options[_settings.scanType])
876
        rset = RadioSetting("basicsettings.scanType", "Scan Type", rx)
877
        basic.append(rset)
878

    
879
        # Menu 18 - Key Lock
880
        rx = RadioSettingValueBoolean(_settings.keylock)
881
        rset = RadioSetting("basicsettings.keylock", "Auto Key Lock", rx)
882
        basic.append(rset)
883

    
884
        if self.MODEL != "QRZ-1":
885
            # Menu 19 - SW Audio
886
            rx = RadioSettingValueBoolean(_settings.swAudio)
887
            rset = RadioSetting("basicsettings.swAudio", "Voice Prompts", rx)
888
            basic.append(rset)
889

    
890
        # Menu 20 - Intro Screen
891
        options = ["Off", "Voltage", "Character String"]
892
        rx = RadioSettingValueList(options, options[_settings.introScreen])
893
        rset = RadioSetting("basicsettings.introScreen", "Intro Screen", rx)
894
        basic.append(rset)
895

    
896
        # Menu 32 - Key Mode
897
        options = ["ALL", "PTT", "KEY", "Key & Side Key"]
898
        rx = RadioSettingValueList(options, options[_settings.keyMode])
899
        rset = RadioSetting("basicsettings.keyMode", "Key Lock Mode", rx)
900
        basic.append(rset)
901

    
902
        # Menu 33 - Display Mode
903
        options = ['Frequency', 'Channel #', 'Name']
904
        rx = RadioSettingValueList(options, options[_settings.disMode])
905
        rset = RadioSetting("basicsettings.disMode", "Display Mode", rx)
906
        basic.append(rset)
907

    
908
        # Menu 34 - FM Dual Wait
909
        rx = RadioSettingValueBoolean(_settings.radioMoni)
910
        rset = RadioSetting("basicsettings.radioMoni", "Radio Monitor", rx)
911
        basic.append(rset)
912

    
913
        advanced = RadioSettingGroup("advanced", "Advanced Settings")
914
        group.append(advanced)
915

    
916
        # software only
917
        options = ['Off', 'Frequency']
918
        rx = RadioSettingValueList(options, options[_settings.endToneElim])
919
        rset = RadioSetting("basicsettings.endToneElim", "End Tone Elim", rx)
920
        advanced.append(rset)
921

    
922
        # software only
923
        name = ""
924
        for i in range(15):  # 0 - 15
925
            name += chr(self._memobj.openradioname.name1[i])
926
        name = name.rstrip()  # remove trailing spaces
927

    
928
        rx = RadioSettingValueString(0, 15, name)
929
        rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
930
        advanced.append(rset)
931

    
932
        # software only
933
        name = ""
934
        for i in range(15):  # 0 - 15
935
            name += chr(self._memobj.openradioname.name2[i])
936
        name = name.rstrip()  # remove trailing spaces
937

    
938
        rx = RadioSettingValueString(0, 15, name)
939
        rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
940
        advanced.append(rset)
941

    
942
        # software only
943
        options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S',
944
                   '4.0S', '4.5S', '5.0S']
945
        rx = RadioSettingValueList(options, options[_settings.voxDelay])
946
        rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx)
947
        advanced.append(rset)
948

    
949
        options = ['Unlocked', 'Unknown 1', 'Unknown 2', 'EU', 'US']
950
        # extend option list with unknown description for values 5 - 15.
951
        for ix in range(len(options), _settings2.region + 1):
952
            item_to_add = 'Unknown {region_code}'.format(region_code=ix)
953
            options.append(item_to_add)
954
        # log unknown region codes greater than 4
955
        if _settings2.region > 4:
956
            LOG.debug("Unknown region code: {value}".
957
                      format(value=_settings2.region))
958
        rx = RadioSettingValueList(options, options[_settings2.region])
959
        rx.set_mutable(False)
960
        rset = RadioSetting("settings2.region", "Region", rx)
961
        advanced.append(rset)
962

    
963
        workmode = RadioSettingGroup("workmode", "Work Mode Settings")
964
        group.append(workmode)
965

    
966
        # Toggle with [#] key
967
        options = ["Frequency", "Channel"]
968
        rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
969
        rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
970
        workmode.append(rset)
971

    
972
        # Toggle with [#] key
973
        options = ["Frequency", "Channel"]
974
        rx = RadioSettingValueList(options, options[_workmode.vfomrmodeb])
975
        rset = RadioSetting("workmodesettings.vfomrmodeb",
976
                            "VFO/MR Mode B (BQ1.41+)", rx)
977
        workmode.append(rset)
978

    
979
        rx = RadioSettingValueInteger(1, 199, _workmode.mrAch + 1)
980
        rset = RadioSetting("workmodesettings.mrAch", "MR A Channel #", rx)
981
        workmode.append(rset)
982

    
983
        rx = RadioSettingValueInteger(1, 199, _workmode.mrBch + 1)
984
        rset = RadioSetting("workmodesettings.mrBch", "MR B Channel #", rx)
985
        workmode.append(rset)
986

    
987
        fmb = RadioSettingGroup("fmradioc", "FM Radio Settings")
988
        group.append(fmb)
989

    
990
        def myset_mask(setting, obj, atrb, nx):
991
            if bool(setting.value):     # Enabled = 1
992
                vx = 1
993
            else:
994
                vx = 0
995
            _do_map(nx + 1, vx, self._memobj.fmmap.fmset)
996
            return
997

    
998
        def myset_fmfrq(setting, obj, atrb, nx):
999
            """ Callback to set xx.x FM freq in memory as xx.x * 100000"""
1000
            # in-valid even KHz freqs are allowed; to satisfy run_tests
1001
            vx = float(str(setting.value))
1002
            vx = int(vx * 100000)
1003
            setattr(obj[nx], atrb, vx)
1004
            return
1005

    
1006
        def myset_freq(setting, obj, atrb, mult):
1007
            """ Callback to set frequency by applying multiplier"""
1008
            value = int(float(str(setting.value)) * mult)
1009
            setattr(obj, atrb, value)
1010
            return
1011

    
1012
        _fmx = self._memobj.fmfrqs
1013

    
1014
        # FM Broadcast Manual Settings
1015
        val = _fmx.fmcur
1016
        val = val / 100000.0
1017
        if val < 64.0 or val > 108.0:
1018
            val = 100.7
1019
        rx = RadioSettingValueFloat(64.0, 108.0, val, 0.1, 1)
1020
        rset = RadioSetting("fmfrqs.fmcur", "Manual FM Freq (MHz)", rx)
1021
        rset.set_apply_callback(myset_freq, _fmx, "fmcur", 100000)
1022
        fmb.append(rset)
1023

    
1024
        _fmfrq = self._memobj.fm_stations
1025
        _fmap = self._memobj.fmmap
1026

    
1027
        # FM Broadcast Presets Settings
1028
        for j in range(0, 24):
1029
            val = _fmfrq[j].rxfreq
1030
            if val < 6400000 or val > 10800000:
1031
                val = 88.0
1032
                fmset = False
1033
            else:
1034
                val = (float(int(val)) / 100000)
1035
                # get fmmap bit value: 1 = enabled
1036
                ndx = int(math.floor((j) / 8))
1037
                bv = j % 8
1038
                msk = 0 << bv
1039
                vx = _fmap.fmset[ndx]
1040
                fmset = not bool(vx & msk)
1041
            rx = RadioSettingValueBoolean(fmset)
1042
            rset = RadioSetting("fmmap.fmset/%d" % j,
1043
                                "FM Preset %02d" % (j + 1), rx)
1044
            rset.set_apply_callback(myset_mask, _fmap, "fmset", j)
1045
            fmb.append(rset)
1046

    
1047
            rx = RadioSettingValueFloat(87.5, 107.9, val, 0.1, 1)
1048
            rset = RadioSetting("fm_stations/%d.rxfreq" % j,
1049
                                "    Preset %02d Freq" % (j + 1), rx)
1050
            # This callback uses the array index
1051
            rset.set_apply_callback(myset_fmfrq, _fmfrq, "rxfreq", j)
1052
            fmb.append(rset)
1053

    
1054
        return group       # END get_settings()
1055

    
1056
    def set_settings(self, settings):
1057
        _settings = self._memobj.basicsettings
1058
        _mem = self._memobj
1059
        for element in settings:
1060
            if not isinstance(element, RadioSetting):
1061
                self.set_settings(element)
1062
                continue
1063
            else:
1064
                try:
1065
                    name = element.get_name()
1066
                    if "." in name:
1067
                        bits = name.split(".")
1068
                        obj = self._memobj
1069
                        for bit in bits[:-1]:
1070
                            if "/" in bit:
1071
                                bit, index = bit.split("/", 1)
1072
                                index = int(index)
1073
                                obj = getattr(obj, bit)[index]
1074
                            else:
1075
                                obj = getattr(obj, bit)
1076
                        setting = bits[-1]
1077
                    else:
1078
                        obj = _settings
1079
                        setting = element.get_name()
1080

    
1081
                    if element.has_apply_callback():
1082
                        LOG.debug("Using apply callback")
1083
                        element.run_apply_callback()
1084
                    elif setting == "mrAch" or setting == "mrBch":
1085
                        setattr(obj, setting, int(element.value) - 1)
1086
                    elif setting == "voxLevel":
1087
                        setattr(obj, setting, int(element.value) - 1)
1088
                    elif element.value.get_mutable():
1089
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1090
                        setattr(obj, setting, element.value)
1091
                except Exception, e:
1092
                    LOG.debug(element.get_name())
1093
                    raise
1094

    
1095

    
1096
@directory.register
1097
class RT85(THUV88Radio):
1098
    VENDOR = "Retevis"
1099
    MODEL = "RT85"
1100

    
1101

    
1102
@directory.register
1103
class QRZ1(THUV88Radio):
1104
    VENDOR = "Explorer"
1105
    MODEL = "QRZ-1"
1106

    
1107
    _magic0 = "\xFE\xFE\xEE\xEF\xE0" + "UV78" + "\xFD"
1108
    _magic2 = "\xFE\xFE\xEE\xEF\xE2" + "UV78" + "\xFD"
1109
    _magic3 = "\xFE\xFE\xEE\xEF\xE3" + "UV78" + "\xFD"
1110
    _magic5 = "\xFE\xFE\xEE\xEF\xE5" + "UV78" + "\xFD"
1111
    _fingerprint = "\xFE\xFE\xEF\xEE\xE1" + "UV78"
(50-50/57)