th_uv88.py

updated driver to use while waiting for patch to be accepted. - Jim Unroe, 02/17/2021 04:47 pm

Download (30.7 kB)

 
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:2,        //
107
     vfomrmode:1,         //        *VFO, MR
108
     unk_bit3_0:4;        //
109
  u8 unk_1141;            // 0x1141
110
  u8 unk_1142;            // 0x1142
111
  u8 unk_bit7_3:5,        //
112
     ab:1,                //        * A, B
113
     unk_bit1_0:2;        //
114
} workmodesettings;
115

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

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

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

    
188
"""
189

    
190
MEM_SIZE = 0x22A0
191
BLOCK_SIZE = 0x20
192
STIMEOUT = 2
193
BAUDRATE = 57600
194

    
195
# Channel power: 3 levels
196
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
197
                chirp_common.PowerLevel("Mid", watts=2.50),
198
                chirp_common.PowerLevel("Low", watts=0.50)]
199

    
200
SCRAMBLE_LIST = ["OFF", "1", "2", "3", "4", "5", "6", "7", "8"]
201
B_LOCK_LIST = ["OFF", "Sub", "Carrier"]
202
OPTSIG_LIST = ["OFF", "DTMF", "2TONE", "5TONE"]
203
PTTID_LIST = ["Off", "BOT", "EOT", "Both"]
204
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0, 50.0, 100.0]
205
LIST_STEPS = [str(x) for x in STEPS]
206

    
207

    
208
def _clean_buffer(radio):
209
    radio.pipe.timeout = 0.005
210
    junk = radio.pipe.read(256)
211
    radio.pipe.timeout = STIMEOUT
212
    if junk:
213
        LOG.debug("Got %i bytes of junk before starting" % len(junk))
214

    
215

    
216
def _rawrecv(radio, amount):
217
    """Raw read from the radio device"""
218
    data = ""
219
    try:
220
        data = radio.pipe.read(amount)
221
    except Exception:
222
        _exit_program_mode(radio)
223
        msg = "Generic error reading data from radio; check your cable."
224
        raise errors.RadioError(msg)
225

    
226
    if len(data) != amount:
227
        _exit_program_mode(radio)
228
        msg = "Error reading from radio: not the amount of data we want."
229
        raise errors.RadioError(msg)
230

    
231
    return data
232

    
233

    
234
def _rawsend(radio, data):
235
    """Raw send to the radio device"""
236
    try:
237
        radio.pipe.write(data)
238
    except Exception:
239
        raise errors.RadioError("Error sending data to radio")
240

    
241

    
242
def _make_read_frame(addr, length):
243
    frame = "\xFE\xFE\xEE\xEF\xEB"
244
    """Pack the info in the header format"""
245
    frame += struct.pack(">ih", addr, length)
246

    
247
    frame += "\xFD"
248
    # Return the data
249
    return frame
250

    
251

    
252
def _make_write_frame(addr, length, data=""):
253
    frame = "\xFE\xFE\xEE\xEF\xE4"
254

    
255
    """Pack the info in the header format"""
256
    output = struct.pack(">ih", addr, length)
257
    # Add the data if set
258
    if len(data) != 0:
259
        output += data
260

    
261
    frame += output
262
    frame += _calculate_checksum(output)
263

    
264
    frame += "\xFD"
265
    # Return the data
266
    return frame
267

    
268

    
269
def _calculate_checksum(data):
270
    num = 0
271
    for x in range(0, len(data)):
272
        num = (num + ord(data[x])) % 256
273

    
274
    if num == 0:
275
        return chr(0)
276

    
277
    return chr(256 - num)
278

    
279

    
280
def _recv(radio, addr, length):
281
    """Get data from the radio """
282

    
283
    data = _rawrecv(radio, length)
284

    
285
    # DEBUG
286
    LOG.info("Response:")
287
    LOG.debug(util.hexprint(data))
288

    
289
    return data
290

    
291

    
292
def _do_ident(radio):
293
    """Put the radio in PROGRAM mode & identify it"""
294
    radio.pipe.baudrate = BAUDRATE
295
    radio.pipe.parity = "N"
296
    radio.pipe.timeout = STIMEOUT
297

    
298
    # Flush input buffer
299
    _clean_buffer(radio)
300

    
301
    # Ident radio
302
    magic = "\xFE\xFE\xEE\xEF\xE0\x55\x56\x38\x38\xFD"
303
    _rawsend(radio, magic)
304
    ack = _rawrecv(radio, 36)
305

    
306
    if not ack.startswith("\xFE\xFE\xEF\xEE\xE1\x55\x56\x38\x38"
307
                          ) or not ack.endswith("\xFD"):
308
        _exit_program_mode(radio)
309
        if ack:
310
            LOG.debug(repr(ack))
311
        raise errors.RadioError("Radio did not respond as expected (A)")
312

    
313
    return True
314

    
315

    
316
def _exit_program_mode(radio):
317
    # This may be the last part of a read
318
    magic = "\xFE\xFE\xEE\xEF\xE5\x55\x56\x38\x38\xFD"
319
    _rawsend(radio, magic)
320
    ack = _rawrecv(radio, 7)
321
    if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
322
        _exit_program_mode(radio)
323
        if ack:
324
            LOG.debug(repr(ack))
325
        raise errors.RadioError("Radio did not respond as expected (B)")
326

    
327

    
328
def _download(radio):
329
    """Get the memory map"""
330

    
331
    # Put radio in program mode and identify it
332
    _do_ident(radio)
333

    
334
    # Enter read mode
335
    magic = "\xFE\xFE\xEE\xEF\xE2\x55\x56\x38\x38\xFD"
336
    _rawsend(radio, magic)
337
    ack = _rawrecv(radio, 7)
338
    if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
339
        _exit_program_mode(radio)
340
        if ack:
341
            LOG.debug(repr(ack))
342
        raise errors.RadioError("Radio did not respond to enter read mode")
343

    
344
    # UI progress
345
    status = chirp_common.Status()
346
    status.cur = 0
347
    status.max = MEM_SIZE / BLOCK_SIZE
348
    status.msg = "Cloning from radio..."
349
    radio.status_fn(status)
350

    
351
    data = ""
352
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
353
        frame = _make_read_frame(addr, BLOCK_SIZE)
354
        # DEBUG
355
        LOG.debug("Frame=" + util.hexprint(frame))
356

    
357
        # Sending the read request
358
        _rawsend(radio, frame)
359

    
360
        # Now we read data
361
        d = _recv(radio, addr, BLOCK_SIZE + 13)
362

    
363
        LOG.debug("Response Data= " + util.hexprint(d))
364

    
365
        if not d.startswith("\xFE\xFE\xEF\xEE\xE4"):
366
            LOG.warning("Incorrect start")
367
        if not d.endswith("\xFD"):
368
            LOG.warning("Incorrect end")
369
        # could validate the block data
370

    
371
        # Aggregate the data
372
        data += d[11:-2]
373

    
374
        # UI Update
375
        status.cur = addr / BLOCK_SIZE
376
        status.msg = "Cloning from radio..."
377
        radio.status_fn(status)
378

    
379
    _exit_program_mode(radio)
380

    
381
    return data
382

    
383

    
384
def _upload(radio):
385
    """Upload procedure"""
386
    # Put radio in program mode and identify it
387
    _do_ident(radio)
388

    
389
    magic = "\xFE\xFE\xEE\xEF\xE3\x55\x56\x38\x38\xFD"
390
    _rawsend(radio, magic)
391
    ack = _rawrecv(radio, 7)
392
    if ack != "\xFE\xFE\xEF\xEE\xE6\x00\xFD":
393
        _exit_program_mode(radio)
394
        if ack:
395
            LOG.debug(repr(ack))
396
        raise errors.RadioError("Radio did not respond to enter write mode")
397

    
398
    # UI progress
399
    status = chirp_common.Status()
400
    status.cur = 0
401
    status.max = MEM_SIZE / BLOCK_SIZE
402
    status.msg = "Cloning to radio..."
403
    radio.status_fn(status)
404

    
405
    # The fun starts here
406
    for addr in range(0, MEM_SIZE, BLOCK_SIZE):
407
        # Official programmer skips writing these memory locations
408
        if addr >= 0x1680 and addr < 0x1940:
409
            continue
410

    
411
        # Sending the data
412
        data = radio.get_mmap()[addr:addr + BLOCK_SIZE]
413

    
414
        frame = _make_write_frame(addr, BLOCK_SIZE, data)
415
        LOG.warning("Frame:%s:" % util.hexprint(frame))
416
        _rawsend(radio, frame)
417

    
418
        ack = _rawrecv(radio, 7)
419
        LOG.debug("Response Data= " + util.hexprint(ack))
420

    
421
        if not ack.startswith("\xFE\xFE\xEF\xEE\xE6\x00\xFD"):
422
            LOG.warning("Unexpected response")
423
            _exit_program_mode(radio)
424
            msg = "Bad ack writing block 0x%04x" % addr
425
            raise errors.RadioError(msg)
426

    
427
        # UI Update
428
        status.cur = addr / BLOCK_SIZE
429
        status.msg = "Cloning to radio..."
430
        radio.status_fn(status)
431

    
432
    _exit_program_mode(radio)
433

    
434

    
435
def _do_map(chn, sclr, mary):
436
    """Set or Clear the chn (1-128) bit in mary[] word array map"""
437
    # chn is 1-based channel, sclr:1 = set, 0= = clear, 2= return state
438
    # mary[] is u8 array, but the map is by nibbles
439
    ndx = int(math.floor((chn - 1) / 8))
440
    bv = (chn - 1) % 8
441
    msk = 1 << bv
442
    mapbit = sclr
443
    if sclr == 1:    # Set the bit
444
        mary[ndx] = mary[ndx] | msk
445
    elif sclr == 0:  # clear
446
        mary[ndx] = mary[ndx] & (~ msk)     # ~ is complement
447
    else:       # return current bit state
448
        mapbit = 0
449
        if (mary[ndx] & msk) > 0:
450
            mapbit = 1
451
    return mapbit
452

    
453

    
454
@directory.register
455
class THUV88Radio(chirp_common.CloneModeRadio):
456
    """TYT UV88 Radio"""
457
    VENDOR = "TYT"
458
    MODEL = "TH-UV88"
459
    MODES = ['WFM', 'FM', 'NFM']
460
    TONES = chirp_common.TONES
461
    DTCS_CODES = chirp_common.DTCS_CODES
462
    NAME_LENGTH = 10
463
    DTMF_CHARS = list("0123456789ABCD*#")
464
    # 136-174, 400-480
465
    VALID_BANDS = [(136000000, 174000000), (400000000, 480000000)]
466

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

    
471
    @classmethod
472
    def get_prompts(cls):
473
        rp = chirp_common.RadioPrompts()
474
        rp.info = \
475
            (cls.VENDOR + ' ' + cls.MODEL + '\n')
476

    
477
        rp.pre_download = _(dedent("""\
478
            This is an early stage beta driver
479
            """))
480
        rp.pre_upload = _(dedent("""\
481
            This is an early stage beta driver - upload at your own risk
482
            """))
483
        return rp
484

    
485
    def get_features(self):
486
        rf = chirp_common.RadioFeatures()
487
        rf.has_settings = True
488
        rf.has_bank = False
489
        rf.has_comment = False
490
        rf.has_tuning_step = False      # Not as chan feature
491
        rf.valid_tuning_steps = STEPS
492
        rf.can_odd_split = True
493
        rf.has_name = True
494
        rf.has_offset = True
495
        rf.has_mode = True
496
        rf.has_dtcs = True
497
        rf.has_rx_dtcs = True
498
        rf.has_dtcs_polarity = True
499
        rf.has_ctone = True
500
        rf.has_cross = True
501
        rf.has_sub_devices = False
502
        rf.valid_name_length = self.NAME_LENGTH
503
        rf.valid_modes = self.MODES
504
        rf.valid_characters = self.VALID_CHARS
505
        rf.valid_duplexes = ["", "-", "+", "split", "off"]
506
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
507
        rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS",
508
                                "Tone->DTCS", "DTCS->Tone", "->Tone",
509
                                "DTCS->DTCS"]
510
        rf.valid_skips = []
511
        rf.valid_power_levels = POWER_LEVELS
512
        rf.valid_dtcs_codes = chirp_common.ALL_DTCS_CODES  # this is just to
513
        # get it working, not sure this is right
514
        rf.valid_bands = self.VALID_BANDS
515
        rf.memory_bounds = (1, 199)
516
        rf.valid_skips = ["", "S"]
517
        return rf
518

    
519
    def sync_in(self):
520
        """Download from radio"""
521
        try:
522
            data = _download(self)
523
        except errors.RadioError:
524
            # Pass through any real errors we raise
525
            raise
526
        except Exception:
527
            # If anything unexpected happens, make sure we raise
528
            # a RadioError and log the problem
529
            LOG.exception('Unexpected error during download')
530
            raise errors.RadioError('Unexpected error communicating '
531
                                    'with the radio')
532
        self._mmap = memmap.MemoryMap(data)
533
        self.process_mmap()
534

    
535
    def sync_out(self):
536
        """Upload to radio"""
537

    
538
        try:
539
            _upload(self)
540
        except Exception:
541
            # If anything unexpected happens, make sure we raise
542
            # a RadioError and log the problem
543
            LOG.exception('Unexpected error during upload')
544
            raise errors.RadioError('Unexpected error communicating '
545
                                    'with the radio')
546

    
547
    def process_mmap(self):
548
        """Process the mem map into the mem object"""
549
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
550

    
551
    def get_raw_memory(self, number):
552
        return repr(self._memobj.memory[number - 1])
553

    
554
    def set_memory(self, memory):
555
        """A value in a UI column for chan 'number' has been modified."""
556
        # update all raw channel memory values (_mem) from UI (mem)
557
        _mem = self._memobj.chan_mem[memory.number - 1]
558
        _name = self._memobj.chan_name[memory.number - 1]
559

    
560
        if memory.empty:
561
            _do_map(memory.number, 0, self._memobj.chan_avail.bitmap)
562
            return
563

    
564
        _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
565

    
566
        if memory.skip == "":
567
            _do_map(memory.number, 1, self._memobj.chan_skip.bitmap)
568
        else:
569
            _do_map(memory.number, 0, self._memobj.chan_skip.bitmap)
570

    
571
        return self._set_memory(memory, _mem, _name)
572

    
573
    def get_memory(self, number):
574
        # radio first channel is 1, mem map is base 0
575
        _mem = self._memobj.chan_mem[number - 1]
576
        _name = self._memobj.chan_name[number - 1]
577
        mem = chirp_common.Memory()
578
        mem.number = number
579

    
580
        # Determine if channel is empty
581

    
582
        if _do_map(number, 2, self._memobj.chan_avail.bitmap) == 0:
583
            mem.empty = True
584
            return mem
585

    
586
        if _do_map(mem.number, 2, self._memobj.chan_skip.bitmap) > 0:
587
            mem.skip = ""
588
        else:
589
            mem.skip = "S"
590

    
591
        return self._get_memory(mem, _mem, _name)
592

    
593
    def _get_memory(self, mem, _mem, _name):
594
        """Convert raw channel memory data into UI columns"""
595
        mem.extra = RadioSettingGroup("extra", "Extra")
596

    
597
        mem.empty = False
598
        # This function process both 'normal' and Freq up/down' entries
599
        mem.freq = int(_mem.rxfreq) * 10
600

    
601
        if _mem.txfreq == 0xFFFFFFFF:
602
            # TX freq not set
603
            mem.duplex = "off"
604
            mem.offset = 0
605
        elif abs(int(_mem.rxfreq) * 10 - int(_mem.txfreq) * 10) > 25000000:
606
            mem.duplex = "split"
607
            mem.offset = int(_mem.txfreq) * 10
608
        elif int(_mem.rxfreq) == int(_mem.txfreq):
609
            mem.duplex = ""
610
            mem.offset = 0
611
        else:
612
            mem.duplex = int(_mem.rxfreq) > int(_mem.txfreq) \
613
                and "-" or "+"
614
            mem.offset = abs(int(_mem.rxfreq) - int(_mem.txfreq)) * 10
615

    
616
        mem.name = ""
617
        for i in range(6):   # 0 - 6
618
            mem.name += chr(_mem.name[i])
619
        for i in range(10):
620
            mem.name += chr(_name.extra_name[i])
621

    
622
        mem.name = mem.name.rstrip()    # remove trailing spaces
623

    
624
        # ########## TONE ##########
625

    
626
        if _mem.txtone > 2600:
627
            # All off
628
            txmode = ""
629
        elif _mem.txtone > 511:
630
            txmode = "Tone"
631
            mem.rtone = int(_mem.txtone) / 10.0
632
        else:
633
            # DTSC
634
            txmode = "DTCS"
635
            mem.dtcs = int(format(int(_mem.txtone), 'o'))
636

    
637
        if _mem.rxtone > 2600:
638
            rxmode = ""
639
        elif _mem.rxtone > 511:
640
            rxmode = "Tone"
641
            mem.ctone = int(_mem.rxtone) / 10.0
642
        else:
643
            rxmode = "DTCS"
644
            mem.rx_dtcs = int(format(int(_mem.rxtone), 'o'))
645

    
646
        mem.dtcs_polarity = ("N", "R")[_mem.encodeDSCI] + (
647
                             "N", "R")[_mem.decodeDSCI]
648

    
649
        mem.tmode = ""
650
        if txmode == "Tone" and not rxmode:
651
            mem.tmode = "Tone"
652
        elif txmode == rxmode and txmode == "Tone" and mem.rtone == mem.ctone:
653
            mem.tmode = "TSQL"
654
        elif txmode == rxmode and txmode == "DTCS" and mem.dtcs == mem.rx_dtcs:
655
            mem.tmode = "DTCS"
656
        elif rxmode or txmode:
657
            mem.tmode = "Cross"
658
            mem.cross_mode = "%s->%s" % (txmode, rxmode)
659

    
660
        # ########## TONE ##########
661

    
662
        mem.mode = self.MODES[_mem.wide]
663
        mem.power = POWER_LEVELS[int(_mem.power)]
664

    
665
        b_lock = RadioSetting("b_lock", "B_Lock",
666
                              RadioSettingValueList(B_LOCK_LIST,
667
                                                    B_LOCK_LIST[_mem.b_lock]))
668
        mem.extra.append(b_lock)
669

    
670
        b_lock = RadioSetting("step", "Step",
671
                              RadioSettingValueList(LIST_STEPS,
672
                                                    LIST_STEPS[_mem.step]))
673
        mem.extra.append(b_lock)
674

    
675
        scramble_value = _mem.scramble
676
        if scramble_value >= 8:     # Looks like OFF is 0x0f ** CONFIRM
677
            scramble_value = 0
678
        scramble = RadioSetting("scramble", "Scramble",
679
                                RadioSettingValueList(SCRAMBLE_LIST,
680
                                                      SCRAMBLE_LIST[
681
                                                          scramble_value]))
682
        mem.extra.append(scramble)
683

    
684
        optsig = RadioSetting("signal", "Optional signaling",
685
                              RadioSettingValueList(
686
                                  OPTSIG_LIST,
687
                                  OPTSIG_LIST[_mem.signal]))
688
        mem.extra.append(optsig)
689

    
690
        rs = RadioSetting("pttid", "PTT ID",
691
                          RadioSettingValueList(PTTID_LIST,
692
                                                PTTID_LIST[_mem.pttid]))
693
        mem.extra.append(rs)
694

    
695
        return mem
696

    
697
    def _set_memory(self, mem, _mem, _name):
698
        # """Convert UI column data (mem) into MEM_FORMAT memory (_mem)."""
699

    
700
        _mem.rxfreq = mem.freq / 10
701
        if mem.duplex == "off":
702
            _mem.txfreq = 0xFFFFFFFF
703
        elif mem.duplex == "split":
704
            _mem.txfreq = mem.offset / 10
705
        elif mem.duplex == "+":
706
            _mem.txfreq = (mem.freq + mem.offset) / 10
707
        elif mem.duplex == "-":
708
            _mem.txfreq = (mem.freq - mem.offset) / 10
709
        else:
710
            _mem.txfreq = _mem.rxfreq
711

    
712
        out_name = mem.name.ljust(16)
713

    
714
        for i in range(6):   # 0 - 6
715
            _mem.name[i] = ord(out_name[i])
716
        for i in range(10):
717
            _name.extra_name[i] = ord(out_name[i+6])
718

    
719
        if mem.name != "":
720
            _mem.displayName = 1    # Name only displayed if this is set on
721
        else:
722
            _mem.displayName = 0
723

    
724
        rxmode = ""
725
        txmode = ""
726

    
727
        if mem.tmode == "Tone":
728
            txmode = "Tone"
729
        elif mem.tmode == "TSQL":
730
            rxmode = "Tone"
731
            txmode = "TSQL"
732
        elif mem.tmode == "DTCS":
733
            rxmode = "DTCSSQL"
734
            txmode = "DTCS"
735
        elif mem.tmode == "Cross":
736
            txmode, rxmode = mem.cross_mode.split("->", 1)
737

    
738
        if mem.dtcs_polarity[1] == "N":
739
            _mem.decodeDSCI = 0
740
        else:
741
            _mem.decodeDSCI = 1
742

    
743
        if rxmode == "":
744
            _mem.rxtone = 0xFFF
745
        elif rxmode == "Tone":
746
            _mem.rxtone = int(float(mem.ctone) * 10)
747
        elif rxmode == "DTCSSQL":
748
            _mem.rxtone = int(str(mem.dtcs), 8)
749
        elif rxmode == "DTCS":
750
            _mem.rxtone = int(str(mem.rx_dtcs), 8)
751

    
752
        if mem.dtcs_polarity[0] == "N":
753
            _mem.encodeDSCI = 0
754
        else:
755
            _mem.encodeDSCI = 1
756

    
757
        if txmode == "":
758
            _mem.txtone = 0xFFF
759
        elif txmode == "Tone":
760
            _mem.txtone = int(float(mem.rtone) * 10)
761
        elif txmode == "TSQL":
762
            _mem.txtone = int(float(mem.ctone) * 10)
763
        elif txmode == "DTCS":
764
            _mem.txtone = int(str(mem.dtcs), 8)
765

    
766
        _mem.wide = self.MODES.index(mem.mode)
767
        _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
768

    
769
        for element in mem.extra:
770
            setattr(_mem, element.get_name(), element.value)
771

    
772
        return
773

    
774
    def get_settings(self):
775
        """Translate the MEM_FORMAT structs into setstuf in the UI"""
776
        _settings = self._memobj.basicsettings
777
        _settings2 = self._memobj.settings2
778
        _workmode = self._memobj.workmodesettings
779

    
780
        basic = RadioSettingGroup("basic", "Basic Settings")
781
        group = RadioSettings(basic)
782

    
783
        # Menu 02 - TX Channel Select
784
        options = ["Last Channel", "Main Channel"]
785
        rx = RadioSettingValueList(options, options[_settings.txChSelect])
786
        rset = RadioSetting("basicsettings.txChSelect",
787
                            "Priority Transmit", rx)
788
        basic.append(rset)
789

    
790
        # Menu 03 - VOX Level
791
        rx = RadioSettingValueInteger(1, 7, _settings.voxLevel - 1)
792
        rset = RadioSetting("basicsettings.voxLevel", "Vox Level", rx)
793
        basic.append(rset)
794

    
795
        # Menu 05 - Squelch Level
796
        options = ["OFF"] + ["%s" % x for x in range(1, 10)]
797
        rx = RadioSettingValueList(options, options[_settings.sqlLevel])
798
        rset = RadioSetting("basicsettings.sqlLevel", "Squelch Level", rx)
799
        basic.append(rset)
800

    
801
        # Menu 06 - Dual Wait
802
        rx = RadioSettingValueBoolean(_settings.dualWait)
803
        rset = RadioSetting("basicsettings.dualWait", "Dual Wait/Standby", rx)
804
        basic.append(rset)
805

    
806
        # Menu 07 - LED Mode
807
        options = ["Off", "On", "Auto"]
808
        rx = RadioSettingValueList(options, options[_settings.ledMode])
809
        rset = RadioSetting("basicsettings.ledMode", "LED Display Mode", rx)
810
        basic.append(rset)
811

    
812
        # Menu 08 - Light
813
        options = ["%s" % x for x in range(1, 8)]
814
        rx = RadioSettingValueList(options, options[_settings.light])
815
        rset = RadioSetting("basicsettings.light",
816
                            "Background Light Color", rx)
817
        basic.append(rset)
818

    
819
        # Menu 09 - Beep
820
        rx = RadioSettingValueBoolean(_settings.beep)
821
        rset = RadioSetting("basicsettings.beep", "Keypad Beep", rx)
822
        basic.append(rset)
823

    
824
        # Menu 11 - TOT
825
        options = ["Off"] + ["%s seconds" % x for x in range(30, 300, 30)]
826
        rx = RadioSettingValueList(options, options[_settings.tot])
827
        rset = RadioSetting("basicsettings.tot",
828
                            "Transmission Time-out Timer", rx)
829
        basic.append(rset)
830

    
831
        # Menu 13 - VOX Switch
832
        rx = RadioSettingValueBoolean(_settings.voxSw)
833
        rset = RadioSetting("basicsettings.voxSw", "Vox Switch", rx)
834
        basic.append(rset)
835

    
836
        # Menu 14 - Roger
837
        rx = RadioSettingValueBoolean(_settings.roger)
838
        rset = RadioSetting("basicsettings.roger", "Roger Beep", rx)
839
        basic.append(rset)
840

    
841
        # Menu 16 - Save Mode
842
        options = ["Off", "1:1", "1:2", "1:4"]
843
        rx = RadioSettingValueList(options, options[_settings.saveMode])
844
        rset = RadioSetting("basicsettings.saveMode", "Battery Save Mode", rx)
845
        basic.append(rset)
846

    
847
        # Menu 33 - Display Mode
848
        options = ['Frequency', 'Channel', 'Name']
849
        rx = RadioSettingValueList(options, options[_settings.disMode])
850
        rset = RadioSetting("basicsettings.disMode", "LED Display Mode", rx)
851
        basic.append(rset)
852

    
853
        advanced = RadioSettingGroup("advanced", "Advanced Settings")
854
        group.append(advanced)
855

    
856
        # software only
857
        options = ['0.5S', '1.0S', '1.5S', '2.0S', '2.5S', '3.0S', '3.5S',
858
                   '4.0S', '4.5S', '5.0S']
859
        rx = RadioSettingValueList(options, options[_settings.voxDelay])
860
        rset = RadioSetting("basicsettings.voxDelay", "VOX Delay", rx)
861
        advanced.append(rset)
862

    
863
        # software only
864
        name = ""
865
        for i in range(15):  # 0 - 15
866
            name += chr(self._memobj.openradioname.name1[i])
867
        name = name.rstrip()  # remove trailing spaces
868

    
869
        rx = RadioSettingValueString(0, 15, name)
870
        rset = RadioSetting("openradioname.name1", "Intro Line 1", rx)
871
        advanced.append(rset)
872

    
873
        # software only
874
        name = ""
875
        for i in range(15):  # 0 - 15
876
            name += chr(self._memobj.openradioname.name2[i])
877
        name = name.rstrip()  # remove trailing spaces
878

    
879
        rx = RadioSettingValueString(0, 15, name)
880
        rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
881
        advanced.append(rset)
882

    
883
        options = ['Unlocked', 'Unknown 1', 'Unknown 2', 'EU', 'US']
884
        # extend option list with unknown description for values 5 - 15.
885
        for ix in range(len(options), _settings2.region + 1):
886
            item_to_add = 'Unknown {region_code}'.format(region_code=ix)
887
            options.append(item_to_add)
888
        # log unknown region codes greater than 4
889
        if _settings2.region > 4:
890
            LOG.debug("Unknown region code: {value}".
891
                      format(value=_settings2.region))
892
        rx = RadioSettingValueList(options, options[_settings2.region])
893
        rx.set_mutable(False)
894
        rset = RadioSetting("settings2.region", "Region", rx)
895
        advanced.append(rset)
896

    
897
        workmode = RadioSettingGroup("workmode", "Work Mode Settings")
898
        group.append(workmode)
899

    
900
        # Toggle with [#] key
901
        options = ["Frequency", "Channel"]
902
        rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
903
        rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
904
        workmode.append(rset)
905

    
906
        # Toggle with [A/B] key
907
        options = ["A", "B"]
908
        rx = RadioSettingValueList(options, options[_workmode.ab])
909
        rset = RadioSetting("workmodesettings.ab", "A/B Select", rx)
910
        workmode.append(rset)
911

    
912
        return group       # END get_settings()
913

    
914
    def set_settings(self, settings):
915
        _settings = self._memobj.basicsettings
916
        _mem = self._memobj
917
        for element in settings:
918
            if not isinstance(element, RadioSetting):
919
                self.set_settings(element)
920
                continue
921
            else:
922
                try:
923
                    name = element.get_name()
924
                    if "." in name:
925
                        bits = name.split(".")
926
                        obj = self._memobj
927
                        for bit in bits[:-1]:
928
                            if "/" in bit:
929
                                bit, index = bit.split("/", 1)
930
                                index = int(index)
931
                                obj = getattr(obj, bit)[index]
932
                            else:
933
                                obj = getattr(obj, bit)
934
                        setting = bits[-1]
935
                    else:
936
                        obj = _settings
937
                        setting = element.get_name()
938

    
939
                    if element.has_apply_callback():
940
                        LOG.debug("Using apply callback")
941
                        element.run_apply_callback()
942
                    elif setting == "voxLevel":
943
                        setattr(obj, setting, int(element.value) + 1)
944
                    elif element.value.get_mutable():
945
                        LOG.debug("Setting %s = %s" % (setting, element.value))
946
                        setattr(obj, setting, element.value)
947
                except Exception, e:
948
                    LOG.debug(element.get_name())
949
                    raise
950

    
951

    
952
@directory.register
953
class RT85(THUV88Radio):
954
    VENDOR = "Retevis"
955
    MODEL = "RT85"