Project

General

Profile

Bug #10383 » th_uv88_192-194_fixed.py

Jim Unroe, 03/04/2023 12:53 AM

 
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
       unk7:1
56
       step:3;               // not required
57
  u8   name[6];
58
};
59

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

    
85
struct chname {
86
  u8  extra_name[10];
87
};
88

    
89
#seekto 0x0000;
90
struct chns chan_mem[199];
91

    
92
#seekto 0x1960;
93
struct chname chan_name[199];
94

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

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

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

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

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

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

    
191
struct fm_chn {
192
  ul32 rxfreq;
193
};
194

    
195
#seekto 0x2180;
196
struct fm_chn fm_stations[24];
197

    
198
#seekto 0x021E0;
199
struct {
200
  u8  fmset[4];
201
} fmmap;
202

    
203
#seekto 0x21E4;
204
struct {
205
  ul32 fmcur;
206
} fmfrqs;
207

    
208
"""
209

    
210
MEM_SIZE = 0x22A0
211
BLOCK_SIZE = 0x20
212
STIMEOUT = 2
213
BAUDRATE = 57600
214

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

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

    
227

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

    
235

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

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

    
251
    return data
252

    
253

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

    
261

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

    
267
    frame += b"\xFD"
268
    # Return the data
269
    return frame
270

    
271

    
272
def _make_write_frame(addr, length, data=""):
273
    frame = b"\xFE\xFE\xEE\xEF\xE4"
274

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

    
281
    frame += output
282
    frame += _calculate_checksum(output)
283

    
284
    frame += b"\xFD"
285
    # Return the data
286
    return frame
287

    
288

    
289
def _calculate_checksum(data):
290
    num = 0
291
    for x in range(0, len(data)):
292
        num = (num + data[x]) % 256
293

    
294
    if num == 0:
295
        return bytes([0])
296

    
297
    return bytes([256 - num])
298

    
299

    
300
def _recv(radio, addr, length):
301
    """Get data from the radio """
302

    
303
    data = _rawrecv(radio, length)
304

    
305
    # DEBUG
306
    LOG.info("Response:")
307
    LOG.debug(util.hexprint(data))
308

    
309
    return data
310

    
311

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

    
318
    # Flush input buffer
319
    _clean_buffer(radio)
320

    
321
    # Ident radio
322
    magic = radio._magic0
323
    _rawsend(radio, magic)
324
    ack = _rawrecv(radio, 36)
325

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

    
332
    return True
333

    
334

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

    
341

    
342
def _download(radio):
343
    """Get the memory map"""
344

    
345
    # Put radio in program mode and identify it
346
    _do_ident(radio)
347

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

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

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

    
371
        # Sending the read request
372
        _rawsend(radio, frame)
373

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

    
377
        LOG.debug("Response Data= " + util.hexprint(d))
378

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

    
385
        # Aggregate the data
386
        data += d[11:-2]
387

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

    
393
    _exit_program_mode(radio)
394

    
395
    return data
396

    
397

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

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

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

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

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

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

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

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

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

    
446
    _exit_program_mode(radio)
447

    
448

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

    
467

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

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

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

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

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

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

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

    
556
    def sync_out(self):
557
        """Upload to radio"""
558

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

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

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

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

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

    
585
        _do_map(memory.number, 1, self._memobj.chan_avail.bitmap)
586

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

    
592
        return self._set_memory(memory, _mem, _name)
593

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

    
601
        # Determine if channel is empty
602

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

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

    
612
        return self._get_memory(mem, _mem, _name)
613

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

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

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

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

    
643
        mem.name = mem.name.rstrip()    # remove trailing spaces
644

    
645
        # ########## TONE ##########
646

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

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

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

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

    
681
        # ########## TONE ##########
682

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

    
686
        rs = RadioSettingValueList(B_LOCK_LIST,
687
                                   B_LOCK_LIST[min(_mem.b_lock, 0x02)])
688
        b_lock = RadioSetting("b_lock", "B_Lock", rs)
689
        mem.extra.append(b_lock)
690

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

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

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

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

    
716
        return mem
717

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

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

    
733
        out_name = mem.name.ljust(16)
734

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

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

    
745
        rxmode = ""
746
        txmode = ""
747

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

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

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

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

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

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

    
790
        for element in mem.extra:
791
            setattr(_mem, element.get_name(), element.value)
792

    
793
        return
794

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

    
801
        basic = RadioSettingGroup("basic", "Basic Settings")
802
        group = RadioSettings(basic)
803

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
911
        advanced = RadioSettingGroup("advanced", "Advanced Settings")
912
        group.append(advanced)
913

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

    
920
        # software only
921
        name = ""
922
        for i in range(15):  # 0 - 15
923
            char = chr(int(self._memobj.openradioname.name1[i]))
924
            if char == "\x00":
925
                char = " "  # Other software may have 0x00 mid-name
926
            name += char
927
        name = name.rstrip()  # remove trailing spaces
928

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

    
933
        # software only
934
        name = ""
935
        for i in range(15):  # 0 - 15
936
            char = chr(int(self._memobj.openradioname.name2[i]))
937
            if char == "\x00":
938
                char = " "  # Other software may have 0x00 mid-name
939
            name += char
940
        name = name.rstrip()  # remove trailing spaces
941

    
942
        rx = RadioSettingValueString(0, 15, name)
943
        rset = RadioSetting("openradioname.name2", "Intro Line 2", rx)
944
        advanced.append(rset)
945

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

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

    
967
        workmode = RadioSettingGroup("workmode", "Work Mode Settings")
968
        group.append(workmode)
969

    
970
        # Toggle with [#] key
971
        options = ["Frequency", "Channel"]
972
        rx = RadioSettingValueList(options, options[_workmode.vfomrmode])
973
        rset = RadioSetting("workmodesettings.vfomrmode", "VFO/MR Mode", rx)
974
        workmode.append(rset)
975

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

    
983
        # Toggle with [A/B] key
984
        options = ["B", "A"]
985
        rx = RadioSettingValueList(options, options[_workmode.ab])
986
        rset = RadioSetting("workmodesettings.ab", "A/B Select", rx)
987
        workmode.append(rset)
988

    
989
        rx = RadioSettingValueInteger(1, 199, _workmode.mrAch + 1)
990
        rset = RadioSetting("workmodesettings.mrAch", "MR A Channel #", rx)
991
        workmode.append(rset)
992

    
993
        rx = RadioSettingValueInteger(1, 199, _workmode.mrBch + 1)
994
        rset = RadioSetting("workmodesettings.mrBch", "MR B Channel #", rx)
995
        workmode.append(rset)
996

    
997
        fmb = RadioSettingGroup("fmradioc", "FM Radio Settings")
998
        group.append(fmb)
999

    
1000
        def myset_mask(setting, obj, atrb, nx):
1001
            if bool(setting.value):     # Enabled = 1
1002
                vx = 1
1003
            else:
1004
                vx = 0
1005
            _do_map(nx + 1, vx, self._memobj.fmmap.fmset)
1006
            return
1007

    
1008
        def myset_fmfrq(setting, obj, atrb, nx):
1009
            """ Callback to set xx.x FM freq in memory as xx.x * 100000"""
1010
            # in-valid even KHz freqs are allowed; to satisfy run_tests
1011
            vx = float(str(setting.value))
1012
            vx = int(vx * 100000)
1013
            setattr(obj[nx], atrb, vx)
1014
            return
1015

    
1016
        def myset_freq(setting, obj, atrb, mult):
1017
            """ Callback to set frequency by applying multiplier"""
1018
            value = int(float(str(setting.value)) * mult)
1019
            setattr(obj, atrb, value)
1020
            return
1021

    
1022
        _fmx = self._memobj.fmfrqs
1023

    
1024
        # FM Broadcast Manual Settings
1025
        val = _fmx.fmcur
1026
        val = val / 100000.0
1027
        if val < 64.0 or val > 108.0:
1028
            val = 100.7
1029
        rx = RadioSettingValueFloat(64.0, 108.0, val, 0.1, 1)
1030
        rset = RadioSetting("fmfrqs.fmcur", "Manual FM Freq (MHz)", rx)
1031
        rset.set_apply_callback(myset_freq, _fmx, "fmcur", 100000)
1032
        fmb.append(rset)
1033

    
1034
        _fmfrq = self._memobj.fm_stations
1035
        _fmap = self._memobj.fmmap
1036

    
1037
        # FM Broadcast Presets Settings
1038
        for j in range(0, 24):
1039
            val = _fmfrq[j].rxfreq
1040
            if val < 6400000 or val > 10800000:
1041
                val = 88.0
1042
                fmset = False
1043
            else:
1044
                val = (float(int(val)) / 100000)
1045
                # get fmmap bit value: 1 = enabled
1046
                ndx = int(math.floor((j) / 8))
1047
                bv = j % 8
1048
                msk = 1 << bv
1049
                vx = _fmap.fmset[ndx]
1050
                fmset = bool(vx & msk)
1051
            rx = RadioSettingValueBoolean(fmset)
1052
            rset = RadioSetting("fmmap.fmset/%d" % j,
1053
                                "FM Preset %02d" % (j + 1), rx)
1054
            rset.set_apply_callback(myset_mask, _fmap, "fmset", j)
1055
            fmb.append(rset)
1056

    
1057
            rx = RadioSettingValueFloat(64.0, 108.0, val, 0.1, 1)
1058
            rset = RadioSetting("fm_stations/%d.rxfreq" % j,
1059
                                "    Preset %02d Freq" % (j + 1), rx)
1060
            # This callback uses the array index
1061
            rset.set_apply_callback(myset_fmfrq, _fmfrq, "rxfreq", j)
1062
            fmb.append(rset)
1063

    
1064
        return group       # END get_settings()
1065

    
1066
    def set_settings(self, settings):
1067
        _settings = self._memobj.basicsettings
1068
        _mem = self._memobj
1069
        for element in settings:
1070
            if not isinstance(element, RadioSetting):
1071
                self.set_settings(element)
1072
                continue
1073
            else:
1074
                try:
1075
                    name = element.get_name()
1076
                    if "." in name:
1077
                        bits = name.split(".")
1078
                        obj = self._memobj
1079
                        for bit in bits[:-1]:
1080
                            if "/" in bit:
1081
                                bit, index = bit.split("/", 1)
1082
                                index = int(index)
1083
                                obj = getattr(obj, bit)[index]
1084
                            else:
1085
                                obj = getattr(obj, bit)
1086
                        setting = bits[-1]
1087
                    else:
1088
                        obj = _settings
1089
                        setting = element.get_name()
1090

    
1091
                    if element.has_apply_callback():
1092
                        LOG.debug("Using apply callback")
1093
                        element.run_apply_callback()
1094
                    elif setting == "mrAch" or setting == "mrBch":
1095
                        setattr(obj, setting, int(element.value) - 1)
1096
                    elif setting == "voxLevel":
1097
                        setattr(obj, setting, int(element.value) - 1)
1098
                    elif element.value.get_mutable():
1099
                        LOG.debug("Setting %s = %s" % (setting, element.value))
1100
                        setattr(obj, setting, element.value)
1101
                except Exception as e:
1102
                    LOG.debug(element.get_name())
1103
                    raise
1104

    
1105

    
1106
@directory.register
1107
class RT85(THUV88Radio):
1108
    VENDOR = "Retevis"
1109
    MODEL = "RT85"
1110

    
1111

    
1112
@directory.register
1113
class QRZ1(THUV88Radio):
1114
    VENDOR = "Explorer"
1115
    MODEL = "QRZ-1"
1116

    
1117
    _magic0 = b"\xFE\xFE\xEE\xEF\xE0" + b"UV78" + b"\xFD"
1118
    _magic2 = b"\xFE\xFE\xEE\xEF\xE2" + b"UV78" + b"\xFD"
1119
    _magic3 = b"\xFE\xFE\xEE\xEF\xE3" + b"UV78" + b"\xFD"
1120
    _magic5 = b"\xFE\xFE\xEE\xEF\xE5" + b"UV78" + b"\xFD"
1121
    _fingerprint = b"\xFE\xFE\xEF\xEE\xE1" + b"UV78"
(3-3/4)